Skip to content

fix(build): remove Spring Boot 2 Gradle plugin for Gradle 9 compatibility#5263

Merged
romtsn merged 36 commits intodeps/scripts/update-gradle.shfrom
fix/spring-boot2-gradle9
Apr 14, 2026
Merged

fix(build): remove Spring Boot 2 Gradle plugin for Gradle 9 compatibility#5263
romtsn merged 36 commits intodeps/scripts/update-gradle.shfrom
fix/spring-boot2-gradle9

Conversation

@adinauer
Copy link
Copy Markdown
Member

@adinauer adinauer commented Apr 2, 2026

📜 Description

Shadow 9.x DuplicatesStrategy fix

Shadow 9.x enforces DuplicatesStrategy before transformers (like mergeServiceFiles and AppendingTransformer) can process duplicates. This means FAIL or EXCLUDE prevents service files and Spring metadata from being merged. All shadow JAR tasks now use DuplicatesStrategy.INCLUDE to let transformers see all duplicate entries.

Spring Boot 2 Gradle plugin removal

Remove the Spring Boot 2 Gradle plugin (org.springframework.boot v2.7.18) which is incompatible with Gradle 9 due to use of removed API LenientConfiguration.getFiles().

Library modules (sentry-spring, sentry-spring-boot, sentry-spring-boot-starter):

  • These only used the plugin with apply false to access SpringBootPlugin.BOM_COORDINATES
  • Replaced with a direct BOM reference via version catalog (libs.springboot2.bom)

Sample apps (spring-boot, webflux, otel, netflix-dgs):

  • Replaced Spring Boot plugin + io.spring.dependency-management with Shadow plugin (com.gradleup.shadow) for fat JAR creation
  • Added application plugin for main class configuration
  • Uses platform(libs.springboot2.bom) for dependency version management
  • Shadow JAR configured with DuplicatesStrategy.INCLUDE + append() to merge Spring metadata files (spring.factories, spring.handlers, spring.schemas, spring-autoconfigure-metadata.properties)
  • BootRun task in otel sample replaced with standard JavaExec

OTel agent shadow JAR fix

  • sentry-opentelemetry-agent shadowJar switched from DuplicatesStrategy.FAIL to INCLUDE so mergeServiceFiles can merge inst/META-INF/services/ from the upstream OTel agent and the distro libs

System test runner

  • Auto-detects shadowJar vs bootJar build task per module
  • Fallback HTTP readiness check for shadow JAR apps (actuator web endpoints don't work in flat JARs)

Note: Spring Boot 3 and 4 plugins are unaffected — they are already Gradle 9 compatible.

💡 Motivation and Context

Stacked on top of #5063. The Spring Boot 2.7.x Gradle plugin uses LenientConfiguration.getFiles() which was removed in Gradle 9, causing bootJar and build tasks to fail. Shadow 9.x changed duplicate handling behavior requiring DuplicatesStrategy.INCLUDE for transformer-based merging to work.

💚 How did you test it?

  • ./gradlew :sentry-spring:compileJava :sentry-spring-boot:compileJava :sentry-spring-boot-starter:compileJava — passes
  • ./gradlew :sentry-spring:test — passes
  • All 5 sample shadow JARs build successfully with properly merged spring.factories (170+ lines)
  • Sample app starts, serves HTTP, detected as ready by system test runner
  • ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:shadowJar — passes, service files properly merged
  • Verified merged service file contains entries from both upstream OTel agent and distro

📝 Checklist

  • No breaking change or entry added to the changelog.
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.

🔮 Next steps

Other Gradle 9 compatibility fixes in the parent PR.

#skip-changelog

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


This PR will not appear in the changelog.


🤖 This preview updates automatically when you update the PR.

@sentry
Copy link
Copy Markdown

sentry bot commented Apr 2, 2026

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.37.1 (1) release

⚙️ sentry-android Build Distribution Settings

adinauer added 2 commits April 2, 2026 12:49
…lity

The Spring Boot 2.7.x Gradle plugin uses removed Gradle APIs
(LenientConfiguration.getFiles()) that are incompatible with Gradle 9.

Library modules (sentry-spring, sentry-spring-boot, sentry-spring-boot-starter):
- Replace SpringBootPlugin.BOM_COORDINATES with direct BOM reference
  via version catalog (libs.springboot2.bom)
- Remove the 'apply false' plugin declaration entirely

Sample apps (spring-boot, webflux, otel, netflix-dgs):
- Replace Spring Boot plugin with Shadow plugin for fat JAR creation
- Add application plugin for main class configuration
- Use platform(libs.springboot2.bom) for dependency version management
- Configure shadow JAR to merge Spring metadata files
- Replace BootRun task with JavaExec in otel sample
…erge

Shadow plugin 9.x defaults to DuplicatesStrategy.EXCLUDE, which drops
duplicate META-INF/spring.factories entries before transformers can
merge them. Setting INCLUDE allows the AppendingTransformer to see all
entries and properly concatenate spring.factories from all JARs.

Without this, the shadow JAR only contains spring.factories from a
single dependency, causing Spring Boot auto-configuration to fail
(e.g. missing RestTemplateBuilder, no embedded web server).
@adinauer adinauer force-pushed the fix/spring-boot2-gradle9 branch from 52ced43 to 9dd4f2f Compare April 2, 2026 10:49
adinauer and others added 12 commits April 2, 2026 12:50
- Auto-detect shadowJar vs bootJar build task based on build.gradle.kts
- Add fallback HTTP readiness check for shadow JAR apps that lack
  actuator endpoints (actuator web endpoints don't work in flat JARs)
- Append spring-autoconfigure-metadata.properties in shadow JAR config
Shadow 9.x enforces duplicatesStrategy before transformers run, so
DuplicatesStrategy.FAIL prevents mergeServiceFiles from merging
inst/META-INF/services/ files that exist in both the upstream OTel
agent JAR and the isolated distro libs. Switching to INCLUDE lets
the transformer see all duplicates and merge them correctly.
Shadow 9.x's ServiceFileTransformer strips the `inst/` prefix when using
`include("inst/META-INF/services/*")`, placing merged service files under
`META-INF/services/` instead of `inst/META-INF/services/`. This breaks
the OTel agent's classloader which expects isolated services under `inst/`.

Using `path = "inst/META-INF/services"` preserves the correct output path.

Also add missing `duplicatesStrategy = DuplicatesStrategy.INCLUDE` to
console-otlp, log4j2, and console-opentelemetry-noagent shadow JARs
so that mergeServiceFiles and Log4j2 transformers can see duplicates
before they are deduplicated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

Shadow 9.x only applies package relocations to service files that are
claimed by a ServiceFileTransformer. The ContextStorageProvider service
file at META-INF/services/ was not being relocated because it wasn't
handled by any transformer — only the inst/META-INF/services/ files were.

Adding a default mergeServiceFiles() call ensures bootstrap service files
(like ContextStorageProvider) go through the transformer and get properly
relocated to their shaded paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lity

Shadow 9.x enforces DuplicatesStrategy before transformers run, which
breaks the `append` transformer for spring.factories and other Spring
metadata files. Only the last copy survives instead of being concatenated.

Replace `append` calls with a pre-merge task that manually concatenates
Spring metadata files (spring.factories, spring.handlers, spring.schemas,
spring-autoconfigure-metadata.properties) from the runtime classpath
before the shadow JAR is built. The merged files are included first in
the shadow JAR so they take precedence over duplicates from dependency
JARs.

This fixes the PersonSystemTest failure where @SentrySpan AOP and JDBC
instrumentation weren't working because SentryAutoConfiguration wasn't
properly registered in the merged spring.factories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lint 8.13.1 (set via android.experimental.lint.version) expects targetSdk 37
but we target 36. This is a test-only module so suppressing is safe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 314.21 ms 346.67 ms 32.45 ms
Size 0 B 0 B 0 B

Baseline results on branch: deps/scripts/update-gradle.sh

Startup times

Revision Plain With Sentry Diff
7a32266 305.50 ms 364.17 ms 58.67 ms

App size

Revision Plain With Sentry Diff
7a32266 0 B 0 B 0 B

Previous results on branch: fix/spring-boot2-gradle9

Startup times

Revision Plain With Sentry Diff
73551a0 319.50 ms 363.84 ms 44.34 ms
cba6cf7 347.26 ms 404.00 ms 56.74 ms
3fe59c8 314.31 ms 316.73 ms 2.41 ms
99a62d0 338.20 ms 392.86 ms 54.66 ms
3caac95 272.74 ms 354.80 ms 82.06 ms
6801bad 311.75 ms 359.57 ms 47.82 ms
67e6872 315.85 ms 355.78 ms 39.92 ms
46ccd28 387.92 ms 463.33 ms 75.41 ms
7b6e574 312.53 ms 366.39 ms 53.86 ms
20e3a13 354.49 ms 425.15 ms 70.66 ms

App size

Revision Plain With Sentry Diff
73551a0 0 B 0 B 0 B
cba6cf7 0 B 0 B 0 B
3fe59c8 0 B 0 B 0 B
99a62d0 0 B 0 B 0 B
3caac95 0 B 0 B 0 B
6801bad 0 B 0 B 0 B
67e6872 0 B 0 B 0 B
46ccd28 0 B 0 B 0 B
7b6e574 0 B 0 B 0 B
20e3a13 0 B 0 B 0 B

romtsn and others added 2 commits April 10, 2026 18:15
…tible

Resolve the runtime classpath at configuration time (not inside doLast)
so the task doesn't capture Gradle script object references that can't
be serialized by the configuration cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@romtsn romtsn force-pushed the fix/spring-boot2-gradle9 branch from f02577c to 8ae3c9c Compare April 10, 2026 16:22
romtsn and others added 2 commits April 10, 2026 18:54
… metadata

The from() approach with DuplicatesStrategy.INCLUDE doesn't work because
dependency JARs' spring.factories overwrites the pre-merged version.

Instead, let the shadow JAR build normally, then use a doLast action
to replace the Spring metadata files in the built JAR with the properly
merged versions using the NIO ZIP filesystem API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@romtsn romtsn force-pushed the fix/spring-boot2-gradle9 branch from 28fff8c to 05c5bac Compare April 10, 2026 17:03
romtsn and others added 3 commits April 10, 2026 23:11
The shadow JAR was missing the embedded web server auto-configuration
because AutoConfiguration.imports (used by SB 2.7+) had duplicate entries
from multiple dependency JARs, with only the last copy surviving.

Add AutoConfiguration.imports to the pre-merge file list and use doLast
JAR patching via NIO ZIP filesystem to replace metadata files after
the shadow JAR is built, avoiding the DuplicatesStrategy issue entirely.

Also suppress OldTargetApi lint for uitest-android-benchmark module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file has duplicate entries across actuator JARs and needs the
same pre-merge treatment as AutoConfiguration.imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@romtsn romtsn force-pushed the fix/spring-boot2-gradle9 branch from 5a111af to 4a277ae Compare April 10, 2026 21:19
romtsn and others added 2 commits April 10, 2026 23:50
…hing

The doLast on shadowJar doesn't run when the task is cached/up-to-date.
Move JAR patching to a separate `patchSpringMetadata` task that is
finalized by shadowJar, ensuring it always runs. Also use recursive
walkTopDown to handle nested directories (e.g. META-INF/spring/).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
romtsn and others added 2 commits April 11, 2026 09:23
…patching

The separate patchSpringMetadata task approach caused regressions — the
finalizedBy relationship didn't reliably execute the patching in CI.

Revert to doLast directly on shadowJar with outputs.upToDateWhen { false }
to ensure the patching always runs. Also use walkTopDown for recursive
directory traversal (needed for META-INF/spring/ subdirectory).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@romtsn romtsn force-pushed the fix/spring-boot2-gradle9 branch from b7ed928 to 702dfd0 Compare April 11, 2026 07:34
romtsn and others added 2 commits April 11, 2026 11:32
Replace the separate mergeSpringMetadata task with inline doLast on
shadowJar that resolves runtimeClasspath at execution time (not
configuration time). This ensures all project dependency JARs are built
before their spring.factories entries are read and merged.

Verified locally: 20/21 system tests pass. Only PersonSystemTest
'create person works' fails due to @SentrySpan AOP limitation in
shadow JARs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@romtsn romtsn force-pushed the fix/spring-boot2-gradle9 branch from b8ec4e1 to 0e33152 Compare April 13, 2026 07:15
@romtsn romtsn marked this pull request as ready for review April 13, 2026 15:06
@romtsn romtsn requested review from markushi and romtsn as code owners April 13, 2026 15:06
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Shadow 9.x upgrade breaks mergeServiceFiles in three samples
    • Added duplicatesStrategy = DuplicatesStrategy.INCLUDE to the console, JUL, and logback sample shadowJar tasks so mergeServiceFiles() receives duplicate service entries under Shadow 9.x.
  • ✅ Fixed: Redundant mergeServiceFiles() is no-op in Spring Boot samples
    • Removed no-op mergeServiceFiles() calls from the spring-boot, spring-boot-webflux, and netflix-dgs sample shadowJar tasks to match their actual metadata/service merging via MergeSpringMetadataAction.

Create PR

Or push these changes by commenting:

@cursor push 29cf9ae479
Preview (29cf9ae479)
diff --git a/sentry-samples/sentry-samples-console/build.gradle.kts b/sentry-samples/sentry-samples-console/build.gradle.kts
--- a/sentry-samples/sentry-samples-console/build.gradle.kts
+++ b/sentry-samples/sentry-samples-console/build.gradle.kts
@@ -51,6 +51,7 @@
 tasks.shadowJar {
   manifest { attributes["Main-Class"] = "io.sentry.samples.console.Main" }
   archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR
+  duplicatesStrategy = DuplicatesStrategy.INCLUDE
   mergeServiceFiles()
 }
 

diff --git a/sentry-samples/sentry-samples-jul/build.gradle.kts b/sentry-samples/sentry-samples-jul/build.gradle.kts
--- a/sentry-samples/sentry-samples-jul/build.gradle.kts
+++ b/sentry-samples/sentry-samples-jul/build.gradle.kts
@@ -44,6 +44,7 @@
 tasks.shadowJar {
   manifest { attributes["Main-Class"] = "io.sentry.samples.jul.Main" }
   archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR
+  duplicatesStrategy = DuplicatesStrategy.INCLUDE
   mergeServiceFiles()
 }
 

diff --git a/sentry-samples/sentry-samples-logback/build.gradle.kts b/sentry-samples/sentry-samples-logback/build.gradle.kts
--- a/sentry-samples/sentry-samples-logback/build.gradle.kts
+++ b/sentry-samples/sentry-samples-logback/build.gradle.kts
@@ -44,6 +44,7 @@
 tasks.shadowJar {
   manifest { attributes["Main-Class"] = "io.sentry.samples.logback.Main" }
   archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR
+  duplicatesStrategy = DuplicatesStrategy.INCLUDE
   mergeServiceFiles()
 }
 

diff --git a/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts b/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts
--- a/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts
+++ b/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts
@@ -42,7 +42,6 @@
 tasks.shadowJar {
   manifest { attributes["Main-Class"] = "io.sentry.samples.netflix.dgs.NetlixDgsApplication" }
   archiveClassifier.set("")
-  mergeServiceFiles()
 
   val springMetadataFiles =
     listOf(

diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts
--- a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts
+++ b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts
@@ -52,7 +52,6 @@
 tasks.shadowJar {
   manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
   archiveClassifier.set("")
-  mergeServiceFiles()
 
   val springMetadataFiles =
     listOf(

diff --git a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts
--- a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts
+++ b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts
@@ -79,7 +79,6 @@
 tasks.shadowJar {
   manifest { attributes["Main-Class"] = "io.sentry.samples.spring.boot.SentryDemoApplication" }
   archiveClassifier.set("")
-  mergeServiceFiles()
 
   // Shadow 9.x enforces DuplicatesStrategy before transformers run, so `append`
   // only sees one copy of each file. We merge Spring metadata from the runtime

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread gradle/libs.versions.toml
Comment thread sentry-samples/sentry-samples-spring-boot/build.gradle.kts Outdated
Comment thread sentry-samples/sentry-samples-spring-boot/build.gradle.kts Outdated
Comment thread sentry-samples/sentry-samples-spring-boot/build.gradle.kts
Drive the ANR profiling state-machine test synchronously instead of starting the background polling thread.

The previous version could read the queue-backed profile store while the polling thread was still appending stack traces, which made the release unit test flaky with NoSuchElementException in QueueFile iteration.

Co-Authored-By: Codex <noreply@openai.com>
Comment thread test/system-test-runner.py Outdated
Comment thread test/system-test-runner.py Outdated
romtsn and others added 2 commits April 13, 2026 20:13
Move the Spring metadata entry list into MergeSpringMetadataAction so
the Spring sample shadowJar tasks use one source of truth.

Drop the temporary system-test-runner unit test and keep verification
on the existing Spring Boot system test flow.

Co-Authored-By: Codex <noreply@openai.com>
Comment thread buildSrc/src/main/java/MergeSpringMetadataAction.kt
Explain that MergeSpringMetadataAction patches shadow JARs by
merging Spring metadata with file-specific semantics and by
preserving service-provider registrations from the runtime
classpath.

This keeps the intent of the build logic clear without changing
behavior.

Co-Authored-By: Codex <noreply@openai.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 327123a. Configure here.

group = "verification"
description = "Runs the System tests"

val test = project.extensions.getByType<SourceSetContainer>()["test"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing DuplicatesStrategy.INCLUDE breaks mergeServiceFiles in Shadow 9.x

Medium Severity

The Shadow plugin was upgraded from 8.3.6 to 9.4.1 in libs.versions.toml. Shadow 9.x enforces DuplicatesStrategy before transformers run, and the default is EXCLUDE. These three samples call mergeServiceFiles() without setting DuplicatesStrategy.INCLUDE, meaning duplicate service files are silently discarded before the transformer can merge them. The sibling samples (console-opentelemetry-noagent, console-otlp, log4j2) correctly add DuplicatesStrategy.INCLUDE alongside mergeServiceFiles(), but these three were missed.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 327123a. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I built the current jars for sentry-samples/sentry-samples-console/build.gradle.kts:47, sentry-samples/sentry-samples-jul/build.gradle.kts:40, and sentry-samples/sentry-samples-logback/build.gradle.kts:40 in a
temp worktree, then added duplicatesStrategy = DuplicatesStrategy.INCLUDE to those three shadowJar blocks and rebuilt.

Results:

  • jul: jar was byte-identical before/after.
  • logback: jar was byte-identical before/after.
  • console: jar changed, but only because INCLUDE preserved duplicate non-service resources like LICENSE, META-INF/LICENSE, META-INF/NOTICE, and about.html.

For the actual service files, the outputs were unchanged. In console, all three service files were identical before/after:

  • META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider
  • META-INF/services/io.sentry.profiling.JavaProfileConverterProvider
  • META-INF/services/javax.cache.spi.CachingProvider

So the general Shadow 9 point is true in principle, but it doesn’t produce a functional difference in these three sample jars. I would leave this comment unresolved as non-actionable.

Set the final agent shadowJar to fail on unexpected duplicate entries
while still allowing service descriptors to merge in the bootstrap and
inst paths. This keeps duplicate handling strict without breaking the
service file transformers Shadow still relies on.

Co-Authored-By: Claude <noreply@anthropic.com>
@romtsn romtsn merged commit 393e7d7 into deps/scripts/update-gradle.sh Apr 14, 2026
65 of 66 checks passed
@romtsn romtsn deleted the fix/spring-boot2-gradle9 branch April 14, 2026 13:03
romtsn added a commit that referenced this pull request Apr 14, 2026
* chore: update scripts/update-gradle.sh to v9.4.1

* Fix build

* fix dependsOn

* align shadow plugin version

* Fix apollo version

* Format code

* fix(build): remove Spring Boot 2 Gradle plugin for Gradle 9 compatibility (#5263)

* fix(build): remove Spring Boot 2 Gradle plugin for Gradle 9 compatibility

The Spring Boot 2.7.x Gradle plugin uses removed Gradle APIs
(LenientConfiguration.getFiles()) that are incompatible with Gradle 9.

Library modules (sentry-spring, sentry-spring-boot, sentry-spring-boot-starter):
- Replace SpringBootPlugin.BOM_COORDINATES with direct BOM reference
  via version catalog (libs.springboot2.bom)
- Remove the 'apply false' plugin declaration entirely

Sample apps (spring-boot, webflux, otel, netflix-dgs):
- Replace Spring Boot plugin with Shadow plugin for fat JAR creation
- Add application plugin for main class configuration
- Use platform(libs.springboot2.bom) for dependency version management
- Configure shadow JAR to merge Spring metadata files
- Replace BootRun task with JavaExec in otel sample

* fix: set duplicatesStrategy=INCLUDE for shadow JAR spring.factories merge

Shadow plugin 9.x defaults to DuplicatesStrategy.EXCLUDE, which drops
duplicate META-INF/spring.factories entries before transformers can
merge them. Setting INCLUDE allows the AppendingTransformer to see all
entries and properly concatenate spring.factories from all JARs.

Without this, the shadow JAR only contains spring.factories from a
single dependency, causing Spring Boot auto-configuration to fail
(e.g. missing RestTemplateBuilder, no embedded web server).

* fix: remove duplicate shadow plugin entry in version catalog

* Format code

* fix: update system test runner for shadow JAR compatibility

- Auto-detect shadowJar vs bootJar build task based on build.gradle.kts
- Add fallback HTTP readiness check for shadow JAR apps that lack
  actuator endpoints (actuator web endpoints don't work in flat JARs)
- Append spring-autoconfigure-metadata.properties in shadow JAR config

* fix(otel): use DuplicatesStrategy.INCLUDE for otel agent shadow JAR

Shadow 9.x enforces duplicatesStrategy before transformers run, so
DuplicatesStrategy.FAIL prevents mergeServiceFiles from merging
inst/META-INF/services/ files that exist in both the upstream OTel
agent JAR and the isolated distro libs. Switching to INCLUDE lets
the transformer see all duplicates and merge them correctly.

* Exclude test-support modules from api validation

* Verbose system test output and wire inputs for them properly

* align coroutines version to 1.9.0 for system tests

* fix(otel): use mergeServiceFiles path instead of include for Shadow 9.x

Shadow 9.x's ServiceFileTransformer strips the `inst/` prefix when using
`include("inst/META-INF/services/*")`, placing merged service files under
`META-INF/services/` instead of `inst/META-INF/services/`. This breaks
the OTel agent's classloader which expects isolated services under `inst/`.

Using `path = "inst/META-INF/services"` preserves the correct output path.

Also add missing `duplicatesStrategy = DuplicatesStrategy.INCLUDE` to
console-otlp, log4j2, and console-opentelemetry-noagent shadow JARs
so that mergeServiceFiles and Log4j2 transformers can see duplicates
before they are deduplicated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(otel): add default mergeServiceFiles for bootstrap service relocation

Shadow 9.x only applies package relocations to service files that are
claimed by a ServiceFileTransformer. The ContextStorageProvider service
file at META-INF/services/ was not being relocated because it wasn't
handled by any transformer — only the inst/META-INF/services/ files were.

Adding a default mergeServiceFiles() call ensures bootstrap service files
(like ContextStorageProvider) go through the transformer and get properly
relocated to their shaded paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(spring-boot2): pre-merge Spring metadata for Shadow 9.x compatibility

Shadow 9.x enforces DuplicatesStrategy before transformers run, which
breaks the `append` transformer for spring.factories and other Spring
metadata files. Only the last copy survives instead of being concatenated.

Replace `append` calls with a pre-merge task that manually concatenates
Spring metadata files (spring.factories, spring.handlers, spring.schemas,
spring-autoconfigure-metadata.properties) from the runtime classpath
before the shadow JAR is built. The merged files are included first in
the shadow JAR so they take precedence over duplicates from dependency
JARs.

This fixes the PersonSystemTest failure where @SentrySpan AOP and JDBC
instrumentation weren't working because SentryAutoConfiguration wasn't
properly registered in the merged spring.factories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Format code

* fix(lint): suppress OldTargetApi for uitest-android module

Lint 8.13.1 (set via android.experimental.lint.version) expects targetSdk 37
but we target 36. This is a test-only module so suppressing is safe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(spring-boot2): make mergeSpringMetadata configuration-cache compatible

Resolve the runtime classpath at configuration time (not inside doLast)
so the task doesn't capture Gradle script object references that can't
be serialized by the configuration cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* formatting

* fix(spring-boot2): replace from() with doLast JAR patching for spring metadata

The from() approach with DuplicatesStrategy.INCLUDE doesn't work because
dependency JARs' spring.factories overwrites the pre-merged version.

Instead, let the shadow JAR build normally, then use a doLast action
to replace the Spring metadata files in the built JAR with the properly
merged versions using the NIO ZIP filesystem API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* formatting

* fix(spring-boot2): merge AutoConfiguration.imports + doLast JAR patching

The shadow JAR was missing the embedded web server auto-configuration
because AutoConfiguration.imports (used by SB 2.7+) had duplicate entries
from multiple dependency JARs, with only the last copy surviving.

Add AutoConfiguration.imports to the pre-merge file list and use doLast
JAR patching via NIO ZIP filesystem to replace metadata files after
the shadow JAR is built, avoiding the DuplicatesStrategy issue entirely.

Also suppress OldTargetApi lint for uitest-android-benchmark module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(spring-boot2): also merge ManagementContextConfiguration.imports

This file has duplicate entries across actuator JARs and needs the
same pre-merge treatment as AutoConfiguration.imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* formatting

* fix(spring-boot2): use separate patchSpringMetadata task for JAR patching

The doLast on shadowJar doesn't run when the task is cached/up-to-date.
Move JAR patching to a separate `patchSpringMetadata` task that is
finalized by shadowJar, ensuring it always runs. Also use recursive
walkTopDown to handle nested directories (e.g. META-INF/spring/).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* formatting

* fix(spring-boot2): revert to doLast on shadowJar for Spring metadata patching

The separate patchSpringMetadata task approach caused regressions — the
finalizedBy relationship didn't reliably execute the patching in CI.

Revert to doLast directly on shadowJar with outputs.upToDateWhen { false }
to ensure the patching always runs. Also use walkTopDown for recursive
directory traversal (needed for META-INF/spring/ subdirectory).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* formatting

* fix(spring-boot2): inline Spring metadata merge into shadowJar doLast

Replace the separate mergeSpringMetadata task with inline doLast on
shadowJar that resolves runtimeClasspath at execution time (not
configuration time). This ensures all project dependency JARs are built
before their spring.factories entries are read and merged.

Verified locally: 20/21 system tests pass. Only PersonSystemTest
'create person works' fails due to @SentrySpan AOP limitation in
shadow JARs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* formatting

* fix(build): make Spring sample shadowJar patching config-cache safe

* fix(build): merge Spring metadata properties in shadow jars

* fix(build): preserve escaped spring metadata keys

* refactor(samples): drop no-op spring shadow service merging

* test(android): Avoid ANR profiling integration test race

Drive the ANR profiling state-machine test synchronously instead of starting the background polling thread.

The previous version could read the queue-backed profile store while the polling thread was still appending stack traces, which made the release unit test flaky with NoSuchElementException in QueueFile iteration.

Co-Authored-By: Codex <noreply@openai.com>

* fix(test): Require actuator health for Spring readiness

* ref(build): Share Spring metadata file list

Move the Spring metadata entry list into MergeSpringMetadataAction so
the Spring sample shadowJar tasks use one source of truth.

Drop the temporary system-test-runner unit test and keep verification
on the existing Spring Boot system test flow.

Co-Authored-By: Codex <noreply@openai.com>

* docs(build): Document Spring metadata merge action

Explain that MergeSpringMetadataAction patches shadow JARs by
merging Spring metadata with file-specific semantics and by
preserving service-provider registrations from the runtime
classpath.

This keeps the intent of the build logic clear without changing
behavior.

Co-Authored-By: Codex <noreply@openai.com>

* build(opentelemetry): Fail agent shadow duplicates by default

Set the final agent shadowJar to fail on unexpected duplicate entries
while still allowing service descriptors to merge in the bootstrap and
inst paths. This keeps duplicate handling strict without breaking the
service file transformers Shadow still relies on.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>

* Apply suggestion from @romtsn

---------

Co-authored-by: GitHub <noreply@github.com>
Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
Co-authored-by: Alexander Dinauer <adinauer@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants