From d59042b69705d9b3c20ddd9ded7c3db3b0032b71 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 01:09:55 -0700 Subject: [PATCH 1/2] Fix build-ordering race by making run task depend on javaagent artifacts JavaagentApplicationRunPlugin configured the run task's -javaagent arguments via a CommandLineArgumentProvider that resolves the javaagent configuration's files at execution time. That provider is not a tracked task input, so Gradle established no dependency on the tasks that build the agent jars. Under parallel execution the run task could launch with -javaagent: before that jar had been produced, failing with "Error opening zip file or JAR manifest missing" (reproducible 100% of the time locally via `--rerun-tasks`, and intermittently on CI). Use `dependsOn(javaagentConfiguration)`: a Configuration is Buildable, so this adds the artifact-building task dependencies and fixes the ordering. Unlike `inputs.files(...)`, it does not register the agent jars as tracked inputs, so it avoids tripping Gradle's strict implicit-dependency validation for agent jars that are added to the configuration without a declared builtBy (e.g. the otel example's :app:extendedAgent jar, which is added via project.files(singleFile) and loses its task provenance). A run task is never up-to-date, so input tracking would provide no benefit here anyway. Add a deterministic regression test that asserts, via --dry-run, that :simple-agent:jar is scheduled before :hello-world:run. The existing `can attach to application run task` test also reproduces the race, but only intermittently depending on task scheduling; the new test catches the ordering regression independent of timing (it fails without this fix, passes with it). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../JavaagentPluginFunctionalTest.kt | 23 +++++++++++++++++++ .../JavaagentApplicationRunPlugin.kt | 13 +++++++++++ 2 files changed, 36 insertions(+) diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index a7640dc7..84369c4a 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -74,6 +74,29 @@ class JavaagentPluginFunctionalTest { assertTrue(ccResult.output.contains("Reusing configuration cache.")) } + @Test fun `run task depends on the tasks that build the javaagent artifacts`() { + val dependencies = """ + javaagent project(':simple-agent') + """ + + createJavaagentProject(dependencies) + + // The run task adds the agent jars via a CommandLineArgumentProvider that is not a tracked input, so + // only an explicit task dependency makes Gradle schedule :simple-agent:jar before :hello-world:run. + // Without it the run task can launch with -javaagent: before that jar has been produced, failing + // with "Error opening zip file or JAR manifest missing" (the `can attach to application run task` + // test above reproduces that intermittently under parallel scheduling). --dry-run prints the task + // execution graph in order without running anything, so asserting the ordering here catches the + // regression deterministically, independent of scheduling timing. + val result = runBuild(listOf("--dry-run", ":hello-world:run")) + + val agentJarIndex = result.output.indexOf(":simple-agent:jar") + val runIndex = result.output.indexOf(":hello-world:run") + assertTrue(agentJarIndex >= 0, "expected :simple-agent:jar to be scheduled ahead of :hello-world:run") + assertTrue(runIndex >= 0, "expected :hello-world:run to be scheduled") + assertTrue(agentJarIndex < runIndex, "expected :simple-agent:jar to be scheduled before :hello-world:run") + } + @Test fun `can attach to test task`() { // A real third-party javaagent used to verify the plugin attaches it and it logs its version banner. val otelVersion = "2.29.0" diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt index 0c645702..9dbeeb0e 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt @@ -21,6 +21,19 @@ class JavaagentApplicationRunPlugin : ) { // configure the run task to use the `javaagent` flag pointing to the dependency stored in the local Maven repository project.tasks.named(ApplicationPlugin.TASK_RUN_NAME, JavaExec::class.java).configure { + // The agent jars are passed to the JVM via a CommandLineArgumentProvider (see + // JavaForkOptionsConfigurer), which resolves the configuration's files at execution time but is + // not a tracked task input. Without an explicit dependency, Gradle establishes no ordering + // between the run task and the tasks that build the agent jars, so under parallel execution the + // run task can launch with -javaagent: before that jar has been produced, failing with + // "Error opening zip file or JAR manifest missing". + // + // A Configuration is Buildable, so dependsOn adds the artifact-building task dependencies and + // fixes the ordering. Unlike inputs.files(...), it does NOT register the agent jars as tracked + // inputs, which avoids tripping Gradle's implicit-dependency validation for agent jars added to + // the configuration without a declared builtBy (e.g. the otel example's extendedAgent jar). The + // run task is never up-to-date, so input tracking would provide no benefit here anyway. + it.dependsOn(javaagentConfiguration) JavaForkOptionsConfigurer.configureJavaForkOptions(it, javaagentConfiguration.map { configuration -> configuration.files }) } } From 4dafe32d336e77c67b409570a2fa3c45da12e870 Mon Sep 17 00:00:00 2001 From: Ryan Dens Date: Mon, 6 Jul 2026 01:42:06 -0700 Subject: [PATCH 2/2] Address review: use inputs.files and fix otel builtBy propagation Per review feedback, register the javaagent configuration as a tracked input of the run task (inputs.files) rather than a bare dependsOn, matching how JavaagentApplicationDistributionPlugin wires the start-script task. Input tracking requires every artifact in the javaagent configuration to carry its producing-task dependency. Fix the otel example at the source: JavaagentOTelModificationPlugin added the extendedAgent output via extendedAgent.outputs.files.singleFile, which resolves to a plain File and drops the builtBy provenance, so tracking the configuration as a run-task input tripped Gradle's implicit-dependency validation (":app:run uses this output of task ':app:extendedAgent' without declaring ..."). Add it via project.files(extendedAgent) instead, so the producing task is carried as a builtBy dependency and consumers that track the configuration establish the dependency automatically. Verified: :otel:functionalTest --rerun-tasks green, the run-task race tests green across repeated --rerun-tasks runs, and ./gradlew build green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../otel/JavaagentOTelModificationPlugin.kt | 7 ++++++- .../JavaagentPluginFunctionalTest.kt | 15 +++++++------- .../JavaagentApplicationRunPlugin.kt | 20 +++++++++---------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/otel/src/main/kotlin/com/ryandens/javaagent/otel/JavaagentOTelModificationPlugin.kt b/otel/src/main/kotlin/com/ryandens/javaagent/otel/JavaagentOTelModificationPlugin.kt index 3ea724f5..2a944948 100644 --- a/otel/src/main/kotlin/com/ryandens/javaagent/otel/JavaagentOTelModificationPlugin.kt +++ b/otel/src/main/kotlin/com/ryandens/javaagent/otel/JavaagentOTelModificationPlugin.kt @@ -89,6 +89,11 @@ class JavaagentOTelModificationPlugin : Plugin { it.into("inst") } } - project.dependencies.add("javaagent", extendedAgent.map { project.files(it.outputs.files.singleFile) }) + // Add the extendedAgent output via the task provider (not extendedAgent.outputs.files.singleFile, + // which resolves to a plain File and loses provenance). project.files(TaskProvider) carries the + // producing task as a builtBy dependency, so consumers of the javaagent configuration that track it + // as an input — e.g. the run task in JavaagentApplicationRunPlugin — establish a dependency on + // :extendedAgent instead of failing Gradle's implicit-dependency validation. + project.dependencies.add("javaagent", project.files(extendedAgent)) } } diff --git a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt index 84369c4a..ef8adb94 100644 --- a/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt +++ b/plugin/src/functionalTest/kotlin/com/ryandens/javaagent/JavaagentPluginFunctionalTest.kt @@ -81,13 +81,14 @@ class JavaagentPluginFunctionalTest { createJavaagentProject(dependencies) - // The run task adds the agent jars via a CommandLineArgumentProvider that is not a tracked input, so - // only an explicit task dependency makes Gradle schedule :simple-agent:jar before :hello-world:run. - // Without it the run task can launch with -javaagent: before that jar has been produced, failing - // with "Error opening zip file or JAR manifest missing" (the `can attach to application run task` - // test above reproduces that intermittently under parallel scheduling). --dry-run prints the task - // execution graph in order without running anything, so asserting the ordering here catches the - // regression deterministically, independent of scheduling timing. + // The run task adds the agent jars via a CommandLineArgumentProvider that is not itself a tracked + // input, so the plugin registers the javaagent configuration as a task input to make Gradle schedule + // :simple-agent:jar before :hello-world:run. Without that, the run task can launch with + // -javaagent: before that jar has been produced, failing with "Error opening zip file or JAR + // manifest missing" (the `can attach to application run task` test above reproduces that + // intermittently under parallel scheduling). --dry-run prints the task execution graph in order + // without running anything, so asserting the ordering here catches the regression deterministically, + // independent of scheduling timing. val result = runBuild(listOf("--dry-run", ":hello-world:run")) val agentJarIndex = result.output.indexOf(":simple-agent:jar") diff --git a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt index 9dbeeb0e..d446e04c 100644 --- a/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt +++ b/plugin/src/main/kotlin/com/ryandens/javaagent/JavaagentApplicationRunPlugin.kt @@ -23,17 +23,17 @@ class JavaagentApplicationRunPlugin : project.tasks.named(ApplicationPlugin.TASK_RUN_NAME, JavaExec::class.java).configure { // The agent jars are passed to the JVM via a CommandLineArgumentProvider (see // JavaForkOptionsConfigurer), which resolves the configuration's files at execution time but is - // not a tracked task input. Without an explicit dependency, Gradle establishes no ordering - // between the run task and the tasks that build the agent jars, so under parallel execution the - // run task can launch with -javaagent: before that jar has been produced, failing with - // "Error opening zip file or JAR manifest missing". + // not a tracked task input. Without registering the configuration as an input, Gradle + // establishes no ordering between the run task and the tasks that build the agent jars, so under + // parallel execution the run task can launch with -javaagent: before that jar has been + // produced, failing with "Error opening zip file or JAR manifest missing". // - // A Configuration is Buildable, so dependsOn adds the artifact-building task dependencies and - // fixes the ordering. Unlike inputs.files(...), it does NOT register the agent jars as tracked - // inputs, which avoids tripping Gradle's implicit-dependency validation for agent jars added to - // the configuration without a declared builtBy (e.g. the otel example's extendedAgent jar). The - // run task is never up-to-date, so input tracking would provide no benefit here anyway. - it.dependsOn(javaagentConfiguration) + // A Configuration is Buildable, so registering it as an input makes the run task depend on the + // tasks that build the agent jars, the same way JavaagentApplicationDistributionPlugin does for + // the start-script task. This requires every artifact added to the javaagent configuration to + // carry its producing-task dependency (builtBy); consumers that synthesize a plain File must + // preserve it (see the otel example's extendedAgent wiring). + it.inputs.files(javaagentConfiguration) JavaForkOptionsConfigurer.configureJavaForkOptions(it, javaagentConfiguration.map { configuration -> configuration.files }) } }