-
Notifications
You must be signed in to change notification settings - Fork 344
Compare release artifacts with jardiff before publishing to Maven Central #11856
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
Comment on lines
+6
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a blocker, just thinking out loud.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While it's doable, I would not advise to do so in that case. I think having the custom plugin invoking the tool is already good enough. |
||
| */ | ||
| 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("'", "'\\''") + "'" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package datadog.gradle.plugin.jardiff | ||
|
|
||
| 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> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| 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()) | ||
|
|
||
| // 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) | ||
| // 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), | ||
| ) | ||
| reportFile.convention(project.layout.buildDirectory.file("reports/jardiff/comparison.txt")) | ||
| referenceJar.convention( | ||
| 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" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to include the link to the incident publicly; the logic stands on its own.