Compare release artifacts with jardiff before publishing to Maven Central#11856
Compare release artifacts with jardiff before publishing to Maven Central#11856bric3 wants to merge 1 commit into
Conversation
|
🎯 Code Coverage (details) 🔗 Commit SHA: 23288b0 | Docs | Datadog PR Page | Give us feedback! |
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
c8a141d to
2b41fdf
Compare
`deploy_to_maven_central` rebuilds the artifacts (reusing the build cache) before publishing, so a fault in the build script could make the rebuild to produce a faulty jar that differs from the one the build job. Which was validated across the whole test chain. This add a `compareToReferenceJar` task (via a new `dd-trace-java.jardiff` plugin) that runs jardiff `--stat --exit-code` against a reference jar (the one from `build` job) and fails on any difference. This allows to gate maven publication (and github releases).
2b41fdf to
23288b0
Compare
Is the motivation here that if the build was faulty, then it would result in a cache miss during the deploy to maven step, and that's why we're comparing the two artifacts? .. Or would the maven deployment step use the faulty build cache anyways? Because that would result in the same build, no? |
Not necessarily a cache miss, but anything that alters the output in an unintended way, that could include how gradle uses it's build cache and if the build infrastructure we have has "bugs" that surface in these conditions.
It's supposed to result in the same, however, if Gradle detects out-of-date items, it can re-execute parts of the build to account for that, this is the whole idea behind that feature, only rebuild what changed. It's unlikely to happen, but if our customization to the build script have an error this can lead to unintended bad interaction. This is what happened during 1.60.0 release where parts of the classes disappeared. |
sarahchen6
left a comment
There was a problem hiding this comment.
Cool idea to check the artifacts in a separate Gradle task! I wonder if it'd be better to extract this logic to a separate job?
For example, we could have a separate verify_artifacts job that runs after the build job and before the publishing jobs. This way, we decouple the artifact check logic from the publish command itself, and we can test on master and catch failures before the release itself.
Actually no, because the goal is to catch build issues that can happen in this job, because this job is running Gradle and can exercise faulty execution paths that could happen during this job specifically. I'm planning to have a job that does that on each pipeline though. But if it isn't done in this job the issue could still surface there, in |
mcculls
left a comment
There was a problem hiding this comment.
LGTM - can you get a second approval from the platform team before merging
sarahchen6
left a comment
There was a problem hiding this comment.
Sorry I may be a bit late for this review to be included in the release..... I think it'd be nice to try this out on PRs first, but otherwise pre-approving !
| * [jardiff](https://github.com/bric3/jardiff) CLI and fails the build if they differ. | ||
| * | ||
| * This guards Maven Central publication against non-deterministic rebuilds: the artifact about to | ||
| * be published (that maybe rebuilt with faults in a later job) is compared |
There was a problem hiding this comment.
| * be published (that maybe rebuilt with faults in a later job) is compared | |
| * be published (that was possibly rebuilt with faults) is compared |
Is this correct? The "reference artifact" is what was published by the build job, and we want to ensure that "the artifact about to be published" wasn't rebuilt with faults?
| JardiffComparison.Outcome.IDENTICAL -> | ||
| logger.lifecycle("✓ ${candidate.name} is identical to the reference jar ${reference.name}") | ||
|
|
||
| JardiffComparison.Outcome.DIFFERENT -> |
There was a problem hiding this comment.
I think it'd help to include instructions in the logs on how to proceed in this case. For example, I assume that if the jars are different, the build and then publish jobs should be re-run.
Same for the ERROR case?
| assertThat(buildFile("reports/jardiff/comparison.txt")).content() | ||
| .doesNotContain("**/*.version") | ||
| } | ||
|
|
There was a problem hiding this comment.
could help to include another test (maybe I missed it) that covers the case when compareToReferenceJar is disabled and no check is done
| # Preserve the `build` job artifacts (validated across the whole test chain) before Gradle | ||
| # rebuilds and overwrites them under workspace/**/build/libs. jardiff then compares the jars | ||
| # about to be published against these references (see https://app.datadoghq.com/incidents/50455). |
There was a problem hiding this comment.
I don't think we need to include the link to the incident publicly; the logic stands on its own.
| # Preserve the `build` job artifacts (validated across the whole test chain) before Gradle | |
| # rebuilds and overwrites them under workspace/**/build/libs. jardiff then compares the jars | |
| # about to be published against these references (see https://app.datadoghq.com/incidents/50455). | |
| # Preserve the `build` job artifacts (validated across the whole test chain) before Gradle | |
| # rebuilds and overwrites them under workspace/**/build/libs. | |
| # jardiff then compares the jars about to be published against these references to ensure artifacts match. |
| * Pure, Gradle-free helpers for driving the [jardiff](https://github.com/bric3/jardiff) CLI. | ||
| * | ||
| * Kept separate from [JardiffTask] so the argument construction and exit-code interpretation can | ||
| * be unit-tested without spinning up a Gradle build or resolving the jardiff jar. |
There was a problem hiding this comment.
Not a blocker, just thinking out loud.
What if instead of using external tool, keep it as part of dd-trace-java?
Just take only needed part? WDYT?
|
@bric3 what would be the remediation steps after detecting that a previous build does not match the one being released? If we could actually fall into this state and I suppose on a very rare occasion then we should probably update the release docs with a runbook and bake it into the CI failure logs. |
What Does This Do
Adds a jar comparison between the artifacts produced by the
buildjob and those produced by thedeploy_to_maven_centraljob before the jar gets published to maven central, i.e. beforepublishMavenPublicationToSonatypeRepositoryexecutes.If a difference is found the
deploy_to_maven_centralfails. It will also prevent to publish the jar to github releases.This PR introduces a
dd-trace-java.jardiffplugin that uses under the hood the jardiff. The tool is run with--stat --exit-codeto compare a freshly built jar against a reference jar frombuild.Failing the build if they differ.
Trimmed task graph (
--task-graph) when invokingpublishToSonatype:Motivation
The
buildjob produces deliverable jar artifacts, those are used during the whole pipeline for all tests, system-tests, benchmark included. It is also used for OCI images. The build job also pushes to thelibgitlab cache, the Gradle build cache.However, the
deploy_to_maven_centraldepends onbuildjob and reuse the build cache rehydrated from thelibgitlab cache.That allows to reuse what was computed in the
buildjob, including the produced artifact, and usually that works fine.However, if for any reason the build script has a bug, it might result in a different artifact being built, possibly a faulty artifact.
That artifact is the one that gets published to customer of maven central or github release.
Note
The above description iswhat happened in release 1.60.0: a faulty jar reached Maven Central and GitHub.
In order to prevent such a faulty artifact to be published, this PR introduces an intermediate tasks whose role is to compare the artifacts produced from the
buildjob with the one from the currentdeploy_to_maven_centralGradle invocation.Additional Notes
feature-flagging-apidd-openfeaturein particular disable the task at this point because build do not build these jarsdeploy_snapshot_with_ddprof_snapshotexcludes it (-x compareToReferenceJar) since its ddprof-snapshot jar differs by designpublishToMavenLocalstays ungated (it is aPublishToMavenLocal, not aPublishToMavenRepository), so local installs are unaffected.Simple tests to compare to another jar
Run it directly, no credentials or network:
Build a "reference" jar (stand-in for the build job's validated artifact)
Compare the freshly built jar against it, if identical this succeeds
The current wiring do not handle
publishToMavenLocal. E.g../gradlew :dd-trace-api:publishToMavenLocalinstalls to~/.m2without triggering it.Simulating a divergent rebuild
Reproducing a second build that produces a different jar, with a real subproject change flowing into a published shadow jar.
dd-trace-otbundles the:dd-trace-ot:correlation-id-injectionsubproject (relocated underddtrot/):Reference: the shadow jar as it stands today.
Patch a class in the bundled subproject, e.g. add a method to
dd-trace-ot/correlation-id-injection/src/main/java/datadog/trace/correlation/Slf4jCorrelationIdInjector.javaAny change to a bundled class works: add/remove/rename a method or class)
Rebuild and compare, the changed class is recompiled and re-bundled, as compareToReferenceJar has the jar task as dependency.
The comparison should fail and should pinpoint what's changed.
Other alternatives
During the previous attempt in #11733, an idea emerged to use a non Gradle tool to make the publication, like jreleaser. However, this require a deeper change (build has to make all the necessary artifacts, they need to be in a proper shape to be handed over to jreleaser, configuring jreleaser, make team aware of jreleaser failure modes).