From 8eb9829357f076d148fc2c0138e8d5d36dabfe23 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 26 Oct 2026 16:51:02 +0100 Subject: [PATCH 1/2] test(spring): Compile the Spring modules against their oldest Spring Boot version Since the matrix stopped rewriting the catalog, the SDK only compiles against the catalog's Spring Boot version, so nothing checked any more that it compiles against the oldest version of each line. An unguarded call to a newer API would only fail if a suite ran that code path. The Spring Boot matrix plugin now registers compileJavaBoot in each of the six modules, compiling the main sources against a compile classpath resolved for the line's oldest version. The oldest version's aggregate depends on it, so the matrix workflows run it. Co-Authored-By: Claude --- .cursor/rules/e2e_tests.mdc | 2 + .../sentry/gradle/SpringBootMatrixPlugin.kt | 60 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.cursor/rules/e2e_tests.mdc b/.cursor/rules/e2e_tests.mdc index 3603794cd5..2fbb17c196 100644 --- a/.cursor/rules/e2e_tests.mdc +++ b/.cursor/rules/e2e_tests.mdc @@ -76,6 +76,8 @@ it. Gradle runs the two in parallel. module once it's added to its workflow. - Suite code may only use APIs that exist in the oldest version of its line. Compiled against a newer one, a missing API fails at runtime with `NoSuchMethodError` or `NoClassDefFoundError`. +- The oldest version's aggregate also compiles the module's main code against that version, e.g. + `compileJavaBoot3_2_12`, since nothing else checks that the SDK compiles against it. ## Gotchas diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt index c0eee3e7b2..d199a0dfd7 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -6,6 +6,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.FileCollection import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.testing.Test import org.gradle.language.base.plugins.LifecycleBasePlugin @@ -30,6 +31,10 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project * `gradle/spring-boot-versions.json`, and `allIntegrationTestsBoot` for all of them. The CI * workflows read the same file and run each version in its own job. A WAR module uses its Boot * module's line, because the Boot BOM decides its Spring version. + * + * The line's first version must be the oldest. The module's main code compiles against it in + * `compileJavaBoot`, part of that version's aggregate, because the main compilation only + * proves the code compiles against the catalog's version. */ fun versions(line: Int) { val file = project.layout.settingsDirectory.file("gradle/spring-boot-versions.json") @@ -48,6 +53,22 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project group = LifecycleBasePlugin.VERIFICATION_GROUP } } + + val oldest = versions.first() + val compileClasspath = classpath("main", oldest, compile = true) + val compileOldest = + project.tasks.register("compileJava${suffix(oldest)}", JavaCompile::class.java) { + description = "Compiles the main Java sources against Spring Boot $oldest." + group = LifecycleBasePlugin.VERIFICATION_GROUP + val compileJava = project.tasks.named("compileJava", JavaCompile::class.java).get() + source = sourceSet("main").java + classpath = compileClasspath + destinationDirectory.set(project.layout.buildDirectory.dir("classes/java/main${suffix(oldest)}")) + javaCompiler.set(compileJava.javaCompiler) + options.release.set(compileJava.options.release) + options.annotationProcessorPath = compileJava.options.annotationProcessorPath + } + project.tasks.named(aggregateName(oldest)) { dependsOn(compileOldest) } } /** @@ -74,28 +95,29 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project } /** [sourceSet]'s output and runtime dependencies, with Spring Boot at [version]. */ - fun runtimeClasspath(sourceSet: String, version: String): FileCollection { + fun runtimeClasspath(sourceSet: String, version: String): FileCollection = + sourceSet(sourceSet).output + classpath(sourceSet, version, compile = false) + + /** [sourceSet]'s compile or runtime classpath dependencies, with Spring Boot at [version]. */ + private fun classpath(sourceSet: String, version: String, compile: Boolean): FileCollection { val sources = sourceSet(sourceSet) - val name = "$sourceSet${suffix(version)}RuntimeClasspath" + val base = + if (compile) sources.compileClasspathConfigurationName + else sources.runtimeClasspathConfigurationName + val name = "$sourceSet${suffix(version)}${if (compile) "Compile" else "Runtime"}Classpath" val configurations = project.configurations - val dependencies = - configurations.findByName(name) - ?: configurations.create(name) { - isCanBeConsumed = false - extendsFrom( - configurations.getByName(sources.implementationConfigurationName), - configurations.getByName(sources.runtimeOnlyConfigurationName), - ) - attributes.addAllLater( - configurations.getByName(sources.runtimeClasspathConfigurationName).attributes - ) - // What setting the version in the catalog does: every Spring Boot artifact, the BOM - // included, is in this group. Libraries pinned to their own version still win conflicts. - resolutionStrategy.eachDependency { - if (requested.group == "org.springframework.boot") useVersion(version) - } + return configurations.findByName(name) + ?: configurations.create(name) { + isCanBeConsumed = false + val baseConfiguration = configurations.getByName(base) + extendsFrom(*baseConfiguration.extendsFrom.toTypedArray()) + attributes.addAllLater(baseConfiguration.attributes) + // What setting the version in the catalog does: every Spring Boot artifact, the BOM + // included, is in this group. Libraries pinned to their own version still win conflicts. + resolutionStrategy.eachDependency { + if (requested.group == "org.springframework.boot") useVersion(version) } - return sources.output + dependencies + } } private fun sourceSet(name: String) = From f41478c35c292a080d1bb7fa2130bb2deffe4661 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 28 Oct 2026 11:08:09 +0100 Subject: [PATCH 2/2] test(spring): Clean up the Spring Boot matrix plugin's Gradle configuration - compileJavaBoot no longer realizes compileJava while it's configured. It links compileJava's release lazily, takes the annotation processors from the source set, and leaves the toolchain to the Java plugin's default for every JavaCompile. - The matrix classpaths extend implementation and compileOnly or runtimeOnly by name. Copying the base classpath's parents depended on what had been added to it by the time the block ran. - They're created with configurations.resolvable(), so they can't be consumed or declared on. Every matrix configuration resolves the same as before. Co-Authored-By: Claude Opus 5.5 --- .../sentry/gradle/SpringBootMatrixPlugin.kt | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt index d199a0dfd7..e10c5f67d1 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -55,18 +55,20 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project } val oldest = versions.first() + val main = sourceSet("main") val compileClasspath = classpath("main", oldest, compile = true) + val compileJava = project.tasks.named("compileJava", JavaCompile::class.java) val compileOldest = project.tasks.register("compileJava${suffix(oldest)}", JavaCompile::class.java) { description = "Compiles the main Java sources against Spring Boot $oldest." group = LifecycleBasePlugin.VERIFICATION_GROUP - val compileJava = project.tasks.named("compileJava", JavaCompile::class.java).get() - source = sourceSet("main").java + source = main.java classpath = compileClasspath - destinationDirectory.set(project.layout.buildDirectory.dir("classes/java/main${suffix(oldest)}")) - javaCompiler.set(compileJava.javaCompiler) - options.release.set(compileJava.options.release) - options.annotationProcessorPath = compileJava.options.annotationProcessorPath + destinationDirectory.set( + project.layout.buildDirectory.dir("classes/java/main${suffix(oldest)}") + ) + options.release.set(compileJava.flatMap { it.options.release }) + options.annotationProcessorPath = main.annotationProcessorPath } project.tasks.named(aggregateName(oldest)) { dependsOn(compileOldest) } } @@ -104,20 +106,27 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project val base = if (compile) sources.compileClasspathConfigurationName else sources.runtimeClasspathConfigurationName + val only = + if (compile) sources.compileOnlyConfigurationName else sources.runtimeOnlyConfigurationName val name = "$sourceSet${suffix(version)}${if (compile) "Compile" else "Runtime"}Classpath" val configurations = project.configurations return configurations.findByName(name) - ?: configurations.create(name) { - isCanBeConsumed = false - val baseConfiguration = configurations.getByName(base) - extendsFrom(*baseConfiguration.extendsFrom.toTypedArray()) - attributes.addAllLater(baseConfiguration.attributes) - // What setting the version in the catalog does: every Spring Boot artifact, the BOM - // included, is in this group. Libraries pinned to their own version still win conflicts. - resolutionStrategy.eachDependency { - if (requested.group == "org.springframework.boot") useVersion(version) + ?: configurations + .resolvable(name) { + // The base classpath's parents by name, since copying its extendsFrom would depend on + // what has been added to it by the time this runs. + extendsFrom( + configurations.getByName(sources.implementationConfigurationName), + configurations.getByName(only), + ) + attributes.addAllLater(configurations.getByName(base).attributes) + // What setting the version in the catalog does: every Spring Boot artifact, the BOM + // included, is in this group. Libraries pinned to their own version still win conflicts. + resolutionStrategy.eachDependency { + if (requested.group == "org.springframework.boot") useVersion(version) + } } - } + .get() } private fun sourceSet(name: String) =