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
15 changes: 13 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1279,12 +1279,21 @@ deploy_to_maven_central:
- export MAVEN_CENTRAL_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-trace-java.central_password --with-decryption --query "Parameter.Value" --out text)
- export GPG_PRIVATE_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-trace-java.signing.gpg_private_key --with-decryption --query "Parameter.Value" --out text)
- export GPG_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-trace-java.signing.gpg_passphrase --with-decryption --query "Parameter.Value" --out text)
- ./gradlew publishToSonatype closeSonatypeStagingRepository -PskipTests $GRADLE_ARGS
# 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).
- mkdir -p reference-artifacts
- cp workspace/dd-java-agent/build/libs/*.jar reference-artifacts/
- cp workspace/dd-trace-api/build/libs/*.jar reference-artifacts/
- cp workspace/dd-trace-ot/build/libs/*.jar reference-artifacts/
- ./gradlew compareToReferenceJar publishToSonatype closeSonatypeStagingRepository -PjardiffReferenceDir="$CI_PROJECT_DIR/reference-artifacts" -PskipTests $GRADLE_ARGS
artifacts:
when: always
paths:
- 'workspace/dd-java-agent/build/libs/*.jar'
- 'workspace/dd-trace-api/build/libs/*.jar'
- 'workspace/dd-trace-ot/build/libs/*.jar'
- 'workspace/**/reports/jardiff/*.txt'

deploy_snapshot_with_ddprof_snapshot:
extends: .gradle_build
Expand All @@ -1304,7 +1313,9 @@ deploy_snapshot_with_ddprof_snapshot:
- export GPG_PRIVATE_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.dd-trace-java.signing.gpg_private_key --with-decryption --query "Parameter.Value" --out text)
- export GPG_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name ci.dd-trace-java.signing.gpg_passphrase --with-decryption --query "Parameter.Value" --out text)
- echo "Publishing dd-trace-java snapshot with ddprof snapshot dependency"
- ./gradlew -PbuildInfo.build.number=$CI_JOB_ID -PddprofUseSnapshot publishToSonatype -PskipTests $GRADLE_ARGS
# This snapshot bundles a ddprof SNAPSHOT, so its jar differs from the build-job artifact by
# design — exclude the reference comparison gate (there is no matching reference to compare to).
- ./gradlew -PbuildInfo.build.number=$CI_JOB_ID -PddprofUseSnapshot publishToSonatype -x compareToReferenceJar -PskipTests $GRADLE_ARGS
artifacts:
paths:
- 'workspace/dd-java-agent/build/libs/*.jar'
Expand Down
5 changes: 5 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ gradlePlugin {
id = "dd-trace-java.sca-enrichments"
implementationClass = "datadog.gradle.plugin.sca.ScaEnrichmentsPlugin"
}

create("jardiff-plugin") {
id = "dd-trace-java.jardiff"
implementationClass = "datadog.gradle.plugin.jardiff.JardiffPlugin"
}
}
}

Expand Down
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("'", "'\\''") + "'"
}
}
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"
}
}
Loading