From d2dac4ebf93b5cfd87d3086282b31e05edbd26b4 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 26 Oct 2026 16:51:02 +0100 Subject: [PATCH] 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 fd52adb14d..7cb73c8c6e 100644 --- a/.cursor/rules/e2e_tests.mdc +++ b/.cursor/rules/e2e_tests.mdc @@ -74,6 +74,8 @@ it. Gradle runs the two in parallel. runs on CI once it's added to its module's `springBootMatrix` block. - 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 77fad36af4..d5eebaab70 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -5,6 +5,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 @@ -28,6 +29,10 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project * Registers `allIntegrationTestsBoot` for each of [versions], and * `allIntegrationTestsBoot` for all of them, which CI runs from the root project for every * module of the line at once. + * + * The first of [versions] 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(versions: List) { this.versions = versions @@ -43,6 +48,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) } } /** @@ -69,28 +90,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) =