-
Notifications
You must be signed in to change notification settings - Fork 344
Catch build issue by comparing release artifacts with jardiff after build job
#11959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gh-worker-dd-mergequeue-cf854d
merged 6 commits into
master
from
bdu/jardiff-task-after-build
Jul 17, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
845c485
ci: Catch build issue by comparing release artifacts with jardiff aft…
bric3 8ee7345
ci: Reword and task api tweaks
bric3 eb21a1f
feat: skip jardiff when jar hashes match
bric3 09f3fb6
feat: write jardiff reports per jar
bric3 69c5fe6
style: Tweak log messages
bric3 0fb87f4
chore(gradle): PR review
bric3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
buildSrc/src/main/kotlin/datadog/gradle/plugin/jardiff/JardiffComparison.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package datadog.gradle.plugin.jardiff | ||
|
|
||
| import java.io.File | ||
|
|
||
| /** | ||
| * 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. | ||
| */ | ||
| object JardiffComparison { | ||
| /** Outcome of a jardiff run, derived from its process exit value. */ | ||
| enum class Outcome { | ||
| /** Exit 0 — the two jars are identical (for the selected include/exclude set). */ | ||
| IDENTICAL, | ||
|
|
||
| /** Exit 1 — jardiff reported differences (behaves like `diff(1)` under `--exit-code`). */ | ||
| DIFFERENT, | ||
|
|
||
| /** Any other exit value — jardiff itself failed (bad arguments, unreadable jar, ...). */ | ||
| ERROR, | ||
| } | ||
|
|
||
| /** | ||
| * Builds the jardiff argument list comparing [reference] (left) against [candidate] (right). | ||
| * | ||
| * [mode] selects the output mode (e.g. `--stat` for a `git diff --stat`-like summary, `--status`, | ||
| * or blank for the default full diff). `--exit-code` is always added. | ||
| * The [JardiffTask] relies on the process exit value. | ||
| * [includes]/[excludes] are comma-joined glob patterns (empty means comparing every entry), and | ||
| * [additionalOptions] are passed through verbatim, right before the two jars. | ||
| */ | ||
| fun buildArguments( | ||
| reference: File, | ||
| candidate: File, | ||
| mode: String, | ||
| includes: List<String> = emptyList(), | ||
| excludes: List<String> = emptyList(), | ||
| additionalOptions: List<String> = emptyList(), | ||
| ): List<String> = buildList { | ||
| if (mode.isNotBlank()) { | ||
| add(mode) | ||
| } | ||
| add("--exit-code") | ||
| add("--color=never") | ||
| if (includes.isNotEmpty()) { | ||
| add("--include=" + includes.joinToString(",")) | ||
| } | ||
| if (excludes.isNotEmpty()) { | ||
| add("--exclude=" + excludes.joinToString(",")) | ||
| } | ||
| addAll(additionalOptions) | ||
| // jardiff positional arguments: <left> <right>. | ||
| // The reference (the artifact validated by the build job) is the left/baseline side, | ||
| // the freshly built candidate is the right side. | ||
| add(reference.absolutePath) | ||
| add(candidate.absolutePath) | ||
| } | ||
|
|
||
| /** Maps a jardiff process exit value to an [Outcome]. */ | ||
| fun outcomeOf(exitValue: Int): Outcome = when (exitValue) { | ||
| 0 -> Outcome.IDENTICAL | ||
| 1 -> Outcome.DIFFERENT | ||
| else -> Outcome.ERROR | ||
| } | ||
|
|
||
| /** | ||
| * Renders the equivalent `java -cp … <mainClass> <arguments>` shell command, so the comparison | ||
| * can be copy-pasted and reproduced outside Gradle. Tokens containing shell-significant | ||
| * characters (spaces, globs, commas, ...) are single-quoted. | ||
| */ | ||
| fun shellCommandLine(classpath: Iterable<File>, mainClass: String, arguments: List<String>): String { | ||
| val classpathValue = classpath.joinToString(File.pathSeparator) { it.absolutePath } | ||
| return (listOf("java", "-cp", classpathValue, mainClass) + arguments) | ||
| .joinToString(" ", transform = ::shellQuote) | ||
| } | ||
|
|
||
| private fun shellQuote(token: String): String = | ||
| if (token.isNotEmpty() && token.all { it.isLetterOrDigit() || it in "/._-=:" }) { | ||
| token | ||
| } else { | ||
| "'" + token.replace("'", "'\\''") + "'" | ||
| } | ||
| } |
43 changes: 43 additions & 0 deletions
43
buildSrc/src/main/kotlin/datadog/gradle/plugin/jardiff/JardiffExtension.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package datadog.gradle.plugin.jardiff | ||
|
|
||
| import org.gradle.api.file.DirectoryProperty | ||
| import org.gradle.api.provider.ListProperty | ||
| import org.gradle.api.provider.Property | ||
|
|
||
| /** Configuration for the [JardiffPlugin]. */ | ||
| interface JardiffExtension { | ||
| /** | ||
| * Maven coordinate of the jardiff CLI resolved to run the comparison. | ||
| * Defaults to [JardiffPlugin.DEFAULT_TOOL_COORDINATE]. | ||
| */ | ||
| val toolCoordinate: Property<String> | ||
|
|
||
| /** | ||
| * Fully qualified main class of the jardiff CLI. | ||
| * Defaults to [JardiffPlugin.DEFAULT_MAIN_CLASS]. | ||
| */ | ||
| val mainClass: Property<String> | ||
|
|
||
| /** | ||
| * jardiff output mode flag, e.g. `--stat` or `--status`; blank selects the default full diff. | ||
| * Defaults to [JardiffPlugin.DEFAULT_MODE]. | ||
| */ | ||
| val mode: Property<String> | ||
|
|
||
| /** | ||
| * Extra jardiff options passed verbatim, right before the two jars (e.g. `--ignore-member-order` | ||
| * or `--class-text-producer=javap`). Empty by default. | ||
| */ | ||
| val additionalOptions: ListProperty<String> | ||
|
|
||
| /** | ||
| * Directory receiving jardiff reports. Defaults to `build/reports/jardiff` in the target project. | ||
| */ | ||
| val reportDir: DirectoryProperty | ||
|
|
||
| /** | ||
| * When true, tolerate mismatching candidate and reference jar hashes if jardiff reports no | ||
| * differences. Defaults to false. | ||
| */ | ||
| val ignoreHashCheck: Property<Boolean> | ||
| } |
100 changes: 100 additions & 0 deletions
100
buildSrc/src/main/kotlin/datadog/gradle/plugin/jardiff/JardiffPlugin.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| package datadog.gradle.plugin.jardiff | ||
|
|
||
| import org.gradle.api.Plugin | ||
| import org.gradle.api.Project | ||
| import org.gradle.api.tasks.bundling.AbstractArchiveTask | ||
| import org.gradle.kotlin.dsl.create | ||
| import org.gradle.kotlin.dsl.named | ||
| import org.gradle.kotlin.dsl.register | ||
|
|
||
| /** | ||
| * Registers the `compareToReferenceJar` task ([JardiffTask]) and wires its reusable inputs: | ||
| * - the jardiff CLI classpath (resolved from [JardiffExtension.toolCoordinate]), | ||
| * - the candidate archive — the module's main publishable jar (the shadow jar when the shadow | ||
| * plugin is applied, otherwise the plain jar), | ||
| * - the reference jar, resolved from the `-PjardiffReferenceDir` project property by matching the | ||
| * candidate's file name inside that directory. | ||
| * | ||
| * Apply it with `id("dd-trace-java.jardiff")`. | ||
| */ | ||
| class JardiffPlugin : Plugin<Project> { | ||
| override fun apply(project: Project) { | ||
| val extension = project.extensions.create<JardiffExtension>("jardiff") | ||
| extension.toolCoordinate.convention(DEFAULT_TOOL_COORDINATE) | ||
| extension.mainClass.convention(DEFAULT_MAIN_CLASS) | ||
| extension.mode.convention(DEFAULT_MODE) | ||
| extension.additionalOptions.convention(emptyList()) | ||
| extension.reportDir.convention(project.layout.buildDirectory.dir("reports/jardiff")) | ||
| extension.ignoreHashCheck.convention(false) | ||
|
|
||
| // Use a detached configuration (created here, resolved only when the task runs) | ||
| // | ||
| // This keeps the jardiff artifact out of `lockAllConfigurations()` dependency locking. | ||
| // The dependency is added lazily, so overriding `jardiff.toolCoordinate` still takes effect. | ||
| // The `@jar` requests the artifact only, because jardiff ships a self-contained "fat" CLI jar | ||
| // under a non-default Gradle Module Metadata variant, the default resolution misses it. | ||
| // Appending `@jar` ignores metadata and fetches that jar. | ||
| val toolClasspath = project.configurations.detachedConfiguration().apply { | ||
| dependencies.addLater( | ||
| extension.toolCoordinate.map { coordinate -> | ||
| val artifactOnly = if ('@' in coordinate) coordinate else "$coordinate@jar" | ||
| project.dependencies.create(artifactOnly) | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| val projectDirectory = project.layout.projectDirectory | ||
| val referenceDirProperty = | ||
| project.providers.gradleProperty("jardiffReferenceDir").filter { it.isNotBlank() } | ||
|
|
||
| val compare = project.tasks.register<JardiffTask>(COMPARE_TASK_NAME) { | ||
| group = "verification" | ||
| description = "Compares the built jar against a reference jar (typically the CI `build` " + | ||
| "job artifact) using jardiff, failing if they differ. Set the reference with " + | ||
| "--reference-jar=<path> or -PjardiffReferenceDir=<dir>." | ||
| jardiffClasspath.convention(toolClasspath) | ||
| mainClass.convention(extension.mainClass) | ||
| mode.convention(extension.mode) | ||
| additionalOptions.convention(extension.additionalOptions) | ||
| reportDir.convention(extension.reportDir) | ||
| ignoreHashCheck.convention(extension.ignoreHashCheck) | ||
| // Ignore **/*.version by default, except under CI where the build and deploy | ||
| // jobs share the same commit. | ||
| ignoreVersionFiles.convention( | ||
| project.providers.environmentVariable("CI").map { false }.orElse(true), | ||
| ) | ||
| referenceJar.convention( | ||
| // Use the same name as the candidate jar | ||
| referenceDirProperty.flatMap { dir -> | ||
| candidateJar.map { candidate -> projectDirectory.dir(dir).file(candidate.asFile.name) } | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| // candidateJar = the module's main publishable archive | ||
| project.pluginManager.withPlugin("java") { | ||
| compare.configure { | ||
| candidateJar.convention( | ||
| project.tasks.named<AbstractArchiveTask>("jar").flatMap { it.archiveFile }, | ||
| ) | ||
| } | ||
| } | ||
| project.pluginManager.withPlugin("com.gradleup.shadow") { | ||
| compare.configure { | ||
| candidateJar.set( | ||
| project.tasks.named<AbstractArchiveTask>("shadowJar").flatMap { it.archiveFile }, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| const val DEFAULT_TOOL_COORDINATE = "io.github.bric3.jardiff:jardiff-cli:0.2.0" | ||
|
|
||
| const val DEFAULT_MAIN_CLASS = "io.github.bric3.jardiff.app.Main" | ||
|
|
||
| const val DEFAULT_MODE = "--stat" | ||
|
|
||
| const val COMPARE_TASK_NAME = "compareToReferenceJar" | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.