Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,25 @@ micronautBuild {
```

You can use [the same DSL as in Gradle](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html).

## Gradle Problems API diagnostics

On the Gradle 9 line, Micronaut Build reports selected internal validation failures through Gradle's Problems API. No extra configuration is required. The build still fails at the same point and keeps the existing failure message, but Gradle also records a structured problem in the generated report at `build/reports/problems/problems-report.html`. Gradle prints a link to that report unless the build runs with `--no-problems-report`.

The stable Micronaut Build problem group is `micronaut-build > validation`. Covered problem IDs include:

* `enforced-platform-not-supported`
* `micronaut-version-mismatch`
* `unsupported-test-framework`
* `invalid-pom-coordinates`
* `pom-verification-failed`
* `asciidoc-output-validation-failed`
* `maven-central-deployment-failed`

For example, if a dependency upgrades `io.micronaut:micronaut-core` away from the Micronaut version declared by the build, the failure remains actionable and includes the existing diagnostic command:

```shell
./gradlew --dependencyInsight --configuration compileClasspath --dependency io.micronaut:micronaut-core
```

Problem details are intentionally bounded to validation context such as versions, configurations, and report paths. Credentials, signing data, Maven Central bearer tokens, raw environment dumps, and unbounded HTTP responses must not be added to problem details or reports.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.micronaut.build

import java.nio.file.Files

class ProblemsApiFunctionalTest extends AbstractFunctionalTest {

void "reports Micronaut version mismatch as a Gradle problem"() {
given:
withSample("test-micronaut-module")

file("subproject1/build.gradle") << """
dependencies {
implementation("io.micronaut:micronaut-core:4.8.4")
}
"""

when:
fails 'compileJava'

then:
errorOutputContains "Micronaut version mismatch: project declares 4.6.3 but resolved version is 4.8.4. You probably have a dependency which triggered an upgrade of micronaut-core. In order to determine where it comes from, you can run ./gradlew --dependencyInsight --configuration compileClasspath --dependency io.micronaut:micronaut-core"
problemsReportContains("micronaut-version-mismatch")
problemsReportContains("Micronaut Build validation")
}

void "reports generated Asciidoc output validation failure as a Gradle problem"() {
given:
settingsFile << """
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}

rootProject.name = "asciidoc-validation-problems"
"""
buildFile << """
plugins {
id "io.micronaut.build.internal.docs"
}

tasks.register("validateBrokenDocs", io.micronaut.build.docs.ValidateAsciidocOutputTask) {
inputDirectory = layout.projectDirectory.dir("broken-docs")
report = layout.buildDirectory.file("reports/broken-docs.txt")
failOnError = true
}
"""
file("broken-docs/index.html").text = "<html><body>Unresolved directive in index.adoc - include::missing.adoc[]</body></html>"

when:
fails 'validateBrokenDocs'

then:
errorOutputContains "Validation of generated asciidoctor files failed. See the report at"
problemsReportContains("asciidoc-output-validation-failed")
problemsReportContains("Micronaut Build validation")
}

private void problemsReportContains(String text) {
def report = testDirectory.resolve("build/reports/problems/problems-report.html")
assert Files.exists(report): "Expected Gradle Problems report at $report"
assert report.text.contains(text): "Expected Gradle Problems report to contain '$text'"
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.micronaut.build;

import io.micronaut.build.problems.MicronautBuildProblems;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.problems.Problems;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
Expand All @@ -13,6 +15,7 @@
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
Expand All @@ -22,8 +25,12 @@
import java.time.Duration;
import java.util.Base64;
import java.util.UUID;
import java.util.regex.Pattern;

import javax.inject.Inject;

public abstract class MavenCentralPublishTask extends DefaultTask {
private static final Pattern DEPLOYMENT_ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._:-]{0,255}");

public enum PublishingType {
AUTOMATIC,
Expand All @@ -44,6 +51,9 @@ public enum PublishingType {
@Option(option = "publishing-type", description = "Configures the Maven Central publishing type.")
public abstract Property<PublishingType> getPublishingType();

@Inject
public abstract Problems getProblems();

public MavenCentralPublishTask() {
super();
setDescription("Publishes a bundle using Maven Central's Publisher API");
Expand Down Expand Up @@ -90,23 +100,24 @@ public void uploadBundle() throws URISyntaxException, IOException, InterruptedEx
.build();

var response = client.send(request, HttpResponse.BodyHandlers.ofString());
var sanitizedBody = MicronautBuildProblems.sanitizeDiagnosticText(response.body());

getLogger().lifecycle("Upload response: {} {}", response.statusCode(), response.body());
getLogger().lifecycle("Upload response: {} {}", response.statusCode(), sanitizedBody);

if (response.statusCode() >= 200 && response.statusCode() < 300) {
var deploymentId = response.body();
var deploymentId = extractDeploymentId(response.body());
if (deploymentId != null && !deploymentId.isEmpty()) {
verifyDeploymentStatus(client, deploymentId);
} else {
throw new GradleException("Could not extract deploymentId from response: " + response.body());
throw deploymentFailure("Could not extract deploymentId from response: " + sanitizedBody, "Maven Central returned a successful upload response without a valid deployment id.");
}
} else {
throw new GradleException("Unexpected status code: " + response.statusCode() + " (" + response.body() + ")");
throw deploymentFailure("Unexpected status code: " + response.statusCode() + " (" + sanitizedBody + ")", "Maven Central returned HTTP " + response.statusCode() + " while uploading the publication bundle.");
}
}

private void verifyDeploymentStatus(HttpClient client, String deploymentId) throws IOException, InterruptedException {
var statusUrl = "https://central.sonatype.com/api/v1/publisher/status?id=" + deploymentId;
var statusUrl = buildStatusUrl(deploymentId);
getLogger().lifecycle("Checking deployment status for {}", deploymentId);
int maxLookups = 100;
while (--maxLookups >= 0) {
Expand All @@ -117,8 +128,9 @@ private void verifyDeploymentStatus(HttpClient client, String deploymentId) thro
.build();

var response = client.send(request, HttpResponse.BodyHandlers.ofString());
var sanitizedBody = MicronautBuildProblems.sanitizeDiagnosticText(response.body());

getLogger().lifecycle("Status check: {} {}", response.statusCode(), response.body());
getLogger().lifecycle("Status check: {} {}", response.statusCode(), sanitizedBody);

var body = response.body();
if (response.statusCode() == 200) {
Expand All @@ -127,14 +139,39 @@ private void verifyDeploymentStatus(HttpClient client, String deploymentId) thro
return;
}
if (body.contains("\"deploymentState\":\"FAILED\"")) {
throw new GradleException("Deployment " + deploymentId + " failed: " + body);
throw deploymentFailure("Deployment " + deploymentId + " failed: " + sanitizedBody, "Maven Central reported a failed deployment state for deployment " + deploymentId + ".");
}
} else if (response.statusCode() < 200 || response.statusCode() > 300) {
getLogger().warn("Status check for deployment " + deploymentId + " failed with: " + body + ". This doesn't necessarily mean that deployment failed, please check status on https://central.sonatype.com/publishing");
} else if (response.statusCode() < 200 || response.statusCode() >= 300) {
getLogger().warn("Status check for deployment " + deploymentId + " failed with: " + sanitizedBody + ". This doesn't necessarily mean that deployment failed, please check status on https://central.sonatype.com/publishing");
break;
}

Thread.sleep(30_000);
}
}

private RuntimeException deploymentFailure(String message, String details) {
return MicronautBuildProblems.throwing(getProblems(), new GradleException(message), MicronautBuildProblems.MAVEN_CENTRAL_DEPLOYMENT_FAILED, spec -> spec
.contextualLabel("Maven Central deployment failed")
.details(details)
.solution("Check the sanitized Maven Central response and verify the publication bundle and deployment status at https://central.sonatype.com/publishing."));
}

static String extractDeploymentId(String responseBody) {
if (responseBody == null) {
return null;
}
var deploymentId = responseBody.trim();
if (!MicronautBuildProblems.sanitizeDiagnosticText(deploymentId).equals(deploymentId)) {
return null;
}
if (DEPLOYMENT_ID.matcher(deploymentId).matches()) {
return deploymentId;
}
return null;
}

static String buildStatusUrl(String deploymentId) {
return "https://central.sonatype.com/api/v1/publisher/status?id=" + URLEncoder.encode(deploymentId, StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
package io.micronaut.build

import com.diffplug.gradle.spotless.SpotlessTask
import io.micronaut.build.problems.MicronautBuildProblems
import io.micronaut.build.utils.DefaultVersions
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.ResolvableDependencies
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.problems.Problems
import org.gradle.api.tasks.compile.GroovyCompile
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.diagnostics.DependencyReportTask
import org.gradle.api.tasks.javadoc.Groovydoc
import org.gradle.api.tasks.testing.Test
import org.gradle.jvm.tasks.Jar

import javax.inject.Inject

import static io.micronaut.build.BomSupport.coreBomArtifactId
import static io.micronaut.build.utils.VersionHandling.versionProviderOrDefault
/**
* Micronaut internal Gradle plugin. Not intended to be used in user's projects.
*/
class MicronautBuildCommonPlugin implements Plugin<Project> {
private final Problems problems

@Inject
MicronautBuildCommonPlugin(Problems problems) {
this.problems = problems
}

void apply(Project project) {
project.pluginManager.apply(MicronautBasePlugin)
project.pluginManager.apply(MicronautQualityChecksParticipantPlugin)
Expand Down Expand Up @@ -63,7 +74,12 @@ class MicronautBuildCommonPlugin implements Plugin<Project> {
project.configurations.globalBoms.dependencies.addAllLater(micronautBuild.enableBom.zip(micronautVersionProvider) { enabled, micronautVersion ->
if (enabled) {
if (micronautBuild.enforcedPlatform.get()) {
throw new GradleException("Do not use enforcedPlatform. Please remove the micronautBuild.enforcedPlatform setting")
String message = "Do not use enforcedPlatform. Please remove the micronautBuild.enforcedPlatform setting"
throw MicronautBuildProblems.throwing(problems, new GradleException(message), MicronautBuildProblems.ENFORCED_PLATFORM_NOT_SUPPORTED) {
it.contextualLabel(message)
.details("micronautBuild.enforcedPlatform is not supported by the Micronaut Build plugins.")
.solution("Remove the micronautBuild.enforcedPlatform setting and let Micronaut Build add the standard platform dependency.")
}
}
String artifactId = coreBomArtifactId(micronautVersion)
[project.dependencies.platform("io.micronaut:$artifactId:$micronautVersion")]
Expand Down Expand Up @@ -154,7 +170,13 @@ class MicronautBuildCommonPlugin implements Plugin<Project> {
if (id.group == 'io.micronaut' && id.module == 'micronaut-core') {
def (resolvedMajor, resolvedMinor, resolvedPatch) = id.version.tokenize('.')
if (resolvedMajor != major || resolvedMinor != minor) {
throw new GradleException("Micronaut version mismatch: project declares $micronautVersion but resolved version is ${id.version}. You probably have a dependency which triggered an upgrade of micronaut-core. In order to determine where it comes from, you can run ./gradlew --dependencyInsight --configuration $configName --dependency io.micronaut:micronaut-core")
String dependencyInsight = "./gradlew --dependencyInsight --configuration $configName --dependency io.micronaut:micronaut-core"
String message = "Micronaut version mismatch: project declares $micronautVersion but resolved version is ${id.version}. You probably have a dependency which triggered an upgrade of micronaut-core. In order to determine where it comes from, you can run $dependencyInsight"
throw MicronautBuildProblems.throwing(problems, new GradleException(message), MicronautBuildProblems.MICRONAUT_VERSION_MISMATCH) {
it.contextualLabel("Micronaut version mismatch on $configName")
.details("Project declares Micronaut $micronautVersion but $configName resolved io.micronaut:micronaut-core:${id.version}.")
.solution("Run $dependencyInsight to determine which dependency selected the resolved micronaut-core version.")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package io.micronaut.build

import groovy.transform.CompileStatic
import io.micronaut.build.problems.MicronautBuildProblems
import org.gradle.api.Action
import io.micronaut.build.utils.DefaultVersions
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.problems.ProblemSpec
import org.gradle.api.problems.Problems

import javax.inject.Inject

import static io.micronaut.build.utils.VersionHandling.versionProviderOrDefault

Expand All @@ -16,14 +22,20 @@ import static io.micronaut.build.utils.VersionHandling.versionProviderOrDefault
*/
@CompileStatic
class MicronautModulePlugin implements Plugin<Project> {
private final Problems problems

@Inject
MicronautModulePlugin(Problems problems) {
this.problems = problems
}

@Override
void apply(Project project) {
project.pluginManager.apply(MicronautBaseModulePlugin)
configureStandardDependencies(project)
}

private static void configureStandardDependencies(Project project) {
private void configureStandardDependencies(Project project) {
var micronautBuild = project.extensions.getByType(MicronautBuildExtension)
var deps = project.dependencies
deps.with {
Expand All @@ -44,7 +56,7 @@ class MicronautModulePlugin implements Plugin<Project> {
deps.create(versionProviderOrDefault(project, 'micronaut-test', DefaultVersions.MICRONAUT_TEST_VERSION).map { "io.micronaut.test:micronaut-test-junit5:${it}" }.get())
)
} else {
throw new GradleException("Unsupported test framework: $it")
return unsupportedTestFramework(it)
}
}
)
Expand All @@ -58,9 +70,19 @@ class MicronautModulePlugin implements Plugin<Project> {
deps.create(versionProviderOrDefault(project, 'junit6', DefaultVersions.JUNIT6_VERSION).map { "org.junit.jupiter:junit-jupiter-engine:${it}" }.get()),
)
} else {
throw new GradleException("Unsupported test framework: $it")
return unsupportedTestFramework(it)
}
}
)
}

private List<Dependency> unsupportedTestFramework(TestFramework testFramework) {
String message = "Unsupported test framework: $testFramework"
throw MicronautBuildProblems.throwing(problems, new GradleException(message), MicronautBuildProblems.UNSUPPORTED_TEST_FRAMEWORK, {
ProblemSpec spec ->
spec.contextualLabel(message)
.details("The Micronaut Build module plugin only supports ${TestFramework.SPOCK} and ${TestFramework.JUNIT6} test framework defaults.")
.solution("Configure micronautBuild.testFramework to ${TestFramework.SPOCK} or ${TestFramework.JUNIT6}.")
} as Action<? super ProblemSpec>)
}
}
Loading
Loading