From 8d9fab19db13fb269003b94495f05409f0b8cf70 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 26 Oct 2026 16:47:18 +0100 Subject: [PATCH 1/3] test(spring): Run each Spring Boot matrix version as its own Gradle task The Spring Boot matrix workflows tested each version by rewriting the catalog with perl, so a version could only be tested locally by editing the catalog, and every job recompiled the SDK and the suites. The new io.sentry.spring-boot-matrix plugin registers a test task per suite and version, e.g. integrationTestWebfluxBoot3_2_12. It reuses the suite's compiled classes on a runtime classpath resolved for that version, which swaps every org.springframework.boot artifact, the BOM included, as the catalog edit did. The resolved classpaths match the catalog edit's exactly. So the SDK and the suites now compile once, against the catalog's version, and only the runtime varies, which is how users run our jars. Suite code may only use APIs of the oldest version of its line. allIntegrationTestsBoot runs one version's suites in a module, and allIntegrationTestsBoot every version's. Each workflow runs the latter from the root in one job per Boot line, with --continue so a failing version doesn't hide the others. The versions live only in SpringBootVersions in build-logic. Resolves JAVA-802 Co-Authored-By: Claude --- .cursor/rules/e2e_tests.mdc | 13 ++- .github/workflows/spring-boot-2-matrix.yml | 27 +---- .github/workflows/spring-boot-3-matrix.yml | 28 +---- .github/workflows/spring-boot-4-matrix.yml | 29 +---- build-logic/build.gradle.kts | 4 + .../sentry/gradle/SpringBootMatrixPlugin.kt | 102 ++++++++++++++++++ .../io/sentry/gradle/SpringBootVersions.kt | 11 ++ sentry-spring-7/build.gradle.kts | 8 ++ sentry-spring-boot-4/build.gradle.kts | 28 ++++- sentry-spring-boot-jakarta/build.gradle.kts | 27 ++++- sentry-spring-boot/build.gradle.kts | 26 ++++- sentry-spring-jakarta/build.gradle.kts | 8 ++ sentry-spring/build.gradle.kts | 8 ++ 13 files changed, 238 insertions(+), 81 deletions(-) create mode 100644 build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt create mode 100644 build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt diff --git a/.cursor/rules/e2e_tests.mdc b/.cursor/rules/e2e_tests.mdc index 1785e61b5c..fd52adb14d 100644 --- a/.cursor/rules/e2e_tests.mdc +++ b/.cursor/rules/e2e_tests.mdc @@ -64,9 +64,16 @@ it. Gradle runs the two in parallel. ## Running them -- The `spring-boot-{2,3,4}-matrix.yml` workflows run the suites against each supported Spring Boot - version, by setting the version in `gradle/libs.versions.toml`. The suites aren't part of - `check`, so a new suite only runs on CI once it's added to its matrix. +- The `springBootMatrix` block in each Spring module's build file runs its suites on every Spring + Boot version in `SpringBootVersions` (build-logic), with a task per suite and version, e.g. + `integrationTestWebfluxBoot3_2_12`. The suites and the SDK compile once, against the catalog's + version; only the runtime classpath changes. `allIntegrationTestsBoot3_2_12` runs one version's + suites, `allIntegrationTestsBoot3` every version's. +- The `spring-boot-{2,3,4}-matrix.yml` workflows run `./gradlew allIntegrationTestsBoot`, + which picks up every module of the line. The suites aren't part of `check`, so a new suite only + 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`. ## Gotchas diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml index 3ee4553cf2..e537ca96b9 100644 --- a/.github/workflows/spring-boot-2-matrix.yml +++ b/.github/workflows/spring-boot-2-matrix.yml @@ -18,12 +18,7 @@ jobs: spring-boot-2-matrix: timeout-minutes: 45 runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - springboot-version: [ '2.7.0', '2.7.18' ] - - name: Spring Boot ${{ matrix.springboot-version }} + name: Spring Boot 2.x env: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -51,27 +46,15 @@ jobs: with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - name: Update Spring Boot 2.x version - run: | - springboot_version="${{ matrix.springboot-version }}" - perl -0pi -e 'BEGIN { $v = shift } s/^springboot2[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot2 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml - echo "Updated Spring Boot 2.x version to $springboot_version" - - name: Test integration suites - run: | - ./gradlew \ - :sentry-spring:integrationTest \ - :sentry-spring-boot:integrationTest \ - :sentry-spring-boot:integrationTestWebflux \ - :sentry-spring-boot:integrationTestOtelNoAgent \ - :sentry-spring-boot:integrationTestOtelAgent \ - :sentry-spring-boot:integrationTestOtelAgentWithoutAutoInit + # Runs every version even after one fails, as separate jobs per version would. + run: ./gradlew allIntegrationTestsBoot2 --continue - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: - name: test-results-springboot-2-${{ matrix.springboot-version }} + name: test-results-springboot-2 path: | **/build/reports/* **/build/test-results/**/*.xml @@ -80,7 +63,7 @@ jobs: uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 if: always() with: - name: JUnit Spring Boot 2.x ${{ matrix.springboot-version }} + name: JUnit Spring Boot 2.x path: | **/build/test-results/**/*.xml reporter: java-junit diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml index f698683cdc..9140a0f1fc 100644 --- a/.github/workflows/spring-boot-3-matrix.yml +++ b/.github/workflows/spring-boot-3-matrix.yml @@ -18,12 +18,7 @@ jobs: spring-boot-3-matrix: timeout-minutes: 45 runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - springboot-version: [ '3.2.12', '3.3.13', '3.4.13', '3.5.13' ] - - name: Spring Boot ${{ matrix.springboot-version }} + name: Spring Boot 3.x env: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -51,28 +46,15 @@ jobs: with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - name: Update Spring Boot 3.x version - run: | - springboot_version="${{ matrix.springboot-version }}" - perl -0pi -e 'BEGIN { $v = shift } s/^springboot3[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot3 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml - echo "Updated Spring Boot 3.x version to $springboot_version" - - name: Test integration suites - run: | - ./gradlew \ - :sentry-spring-jakarta:integrationTest \ - :sentry-spring-boot-jakarta:integrationTest \ - :sentry-spring-boot-jakarta:integrationTestWebflux \ - :sentry-spring-boot-jakarta:integrationTestOtelNoAgent \ - :sentry-spring-boot-jakarta:integrationTestLog4j2 \ - :sentry-spring-boot-jakarta:integrationTestOtelAgent \ - :sentry-spring-boot-jakarta:integrationTestOtelAgentWithoutAutoInit + # Runs every version even after one fails, as separate jobs per version would. + run: ./gradlew allIntegrationTestsBoot3 --continue - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: - name: test-results-springboot-3-${{ matrix.springboot-version }} + name: test-results-springboot-3 path: | **/build/reports/* **/build/test-results/**/*.xml @@ -81,7 +63,7 @@ jobs: uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 if: always() with: - name: JUnit Spring Boot 3.x ${{ matrix.springboot-version }} + name: JUnit Spring Boot 3.x path: | **/build/test-results/**/*.xml reporter: java-junit diff --git a/.github/workflows/spring-boot-4-matrix.yml b/.github/workflows/spring-boot-4-matrix.yml index 8f91289db9..3524a66e3c 100644 --- a/.github/workflows/spring-boot-4-matrix.yml +++ b/.github/workflows/spring-boot-4-matrix.yml @@ -18,12 +18,7 @@ jobs: spring-boot-4-matrix: timeout-minutes: 45 runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - springboot-version: [ '4.0.0', '4.0.5', '4.1.0' ] - - name: Spring Boot ${{ matrix.springboot-version }} + name: Spring Boot 4.x env: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -51,29 +46,15 @@ jobs: with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - name: Update Spring Boot 4.x version - run: | - springboot_version="${{ matrix.springboot-version }}" - perl -0pi -e 'BEGIN { $v = shift } s/^springboot4[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot4 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml - echo "Updated Spring Boot 4.x version to $springboot_version" - - name: Test integration suites - run: | - ./gradlew \ - :sentry-spring-7:integrationTest \ - :sentry-spring-boot-4:integrationTest \ - :sentry-spring-boot-4:integrationTestWebflux \ - :sentry-spring-boot-4:integrationTestOtelNoAgent \ - :sentry-spring-boot-4:integrationTestLog4j2 \ - :sentry-spring-boot-4:integrationTestOtelAgent \ - :sentry-spring-boot-4:integrationTestOtelAgentWithoutAutoInit \ - :sentry-spring-boot-4:integrationTestOtlp + # Runs every version even after one fails, as separate jobs per version would. + run: ./gradlew allIntegrationTestsBoot4 --continue - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: - name: test-results-springboot-4-${{ matrix.springboot-version }} + name: test-results-springboot-4 path: | **/build/reports/* **/build/test-results/**/*.xml @@ -82,7 +63,7 @@ jobs: uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 if: always() with: - name: JUnit Spring Boot 4.x ${{ matrix.springboot-version }} + name: JUnit Spring Boot 4.x path: | **/build/test-results/**/*.xml reporter: java-junit diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 60a340c9b8..29760f38a9 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -24,5 +24,9 @@ gradlePlugin { id = "io.sentry.animalsniffer.android" implementationClass = "io.sentry.gradle.SentryAnimalSnifferAndroidPlugin" } + register("sentrySpringBootMatrix") { + id = "io.sentry.spring-boot-matrix" + implementationClass = "io.sentry.gradle.SpringBootMatrixPlugin" + } } } diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt new file mode 100644 index 0000000000..77fad36af4 --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -0,0 +1,102 @@ +package io.sentry.gradle + +import javax.inject.Inject +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.testing.Test +import org.gradle.language.base.plugins.LifecycleBasePlugin + +/** + * Runs integration suites on each Spring Boot version of a line, without recompiling anything. + * + * The suites and the SDK compile once, against the version catalog's Spring Boot version. For each + * version, a suite's test task reuses its compiled classes on a runtime classpath resolved for that + * version, which is how users run our published jars on their Spring Boot version. + */ +class SpringBootMatrixPlugin : Plugin { + override fun apply(project: Project) { + project.extensions.create("springBootMatrix", SpringBootMatrixExtension::class.java) + } +} + +abstract class SpringBootMatrixExtension @Inject constructor(private val project: Project) { + private var versions = emptyList() + + /** + * 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. + */ + fun versions(versions: List) { + this.versions = versions + val major = versions.map { it.substringBefore('.') }.distinct().single() + project.tasks.register("allIntegrationTestsBoot$major") { + description = "Runs the integration suites on Spring Boot ${versions.joinToString()}." + group = LifecycleBasePlugin.VERIFICATION_GROUP + dependsOn(versions.map { aggregateName(it) }) + } + versions.forEach { version -> + project.tasks.register(aggregateName(version)) { + description = "Runs the integration suites on Spring Boot $version." + group = LifecycleBasePlugin.VERIFICATION_GROUP + } + } + } + + /** + * Registers `Boot` for each version, running [suite]'s tests. [name] tells apart + * several tasks of one suite, and [configure] adds what the suite's own task has beyond its + * classes and classpath. + */ + fun test(suite: String, name: String = suite, configure: Test.(version: String) -> Unit = {}) { + check(versions.isNotEmpty()) { "Call versions() before test()" } + versions.forEach { version -> + // Created now rather than with the task, so `dependencies --configuration` finds it. + val runtimeClasspath = runtimeClasspath(suite, version) + val task = + project.tasks.register("$name${suffix(version)}", Test::class.java) { + description = "Runs $name on Spring Boot $version." + group = LifecycleBasePlugin.VERIFICATION_GROUP + testClassesDirs = sourceSet(suite).output.classesDirs + classpath = runtimeClasspath + useJUnit() + configure(version) + } + project.tasks.named(aggregateName(version)) { dependsOn(task) } + } + } + + /** [sourceSet]'s output and runtime dependencies, with Spring Boot at [version]. */ + fun runtimeClasspath(sourceSet: String, version: String): FileCollection { + val sources = sourceSet(sourceSet) + val name = "$sourceSet${suffix(version)}RuntimeClasspath" + 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 sources.output + dependencies + } + + private fun sourceSet(name: String) = + project.extensions.getByType(SourceSetContainer::class.java).getByName(name) + + private fun aggregateName(version: String) = "allIntegrationTests${suffix(version)}" + + private fun suffix(version: String) = "Boot" + version.replace('.', '_') +} diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt new file mode 100644 index 0000000000..588d50ce7c --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt @@ -0,0 +1,11 @@ +package io.sentry.gradle + +/** + * The Spring Boot versions the Spring integration suites run on, per major line. A WAR suite runs + * on the same versions as its Boot module, because the Boot BOM decides its Spring version. + */ +object SpringBootVersions { + val BOOT_2 = listOf("2.7.0", "2.7.18") + val BOOT_3 = listOf("3.2.12", "3.3.13", "3.4.13", "3.5.13") + val BOOT_4 = listOf("4.0.0", "4.0.5", "4.1.0") +} diff --git a/sentry-spring-7/build.gradle.kts b/sentry-spring-7/build.gradle.kts index b9d418abe4..2eadb3efb6 100644 --- a/sentry-spring-7/build.gradle.kts +++ b/sentry-spring-7/build.gradle.kts @@ -1,3 +1,4 @@ +import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -5,6 +6,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -142,3 +144,9 @@ testing { } } } + +// The suites on each Spring Boot 4 version the CI matrix tests. +springBootMatrix { + versions(SpringBootVersions.BOOT_4) + test("integrationTest") +} diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts index 7f4428551b..0077caf712 100644 --- a/sentry-spring-boot-4/build.gradle.kts +++ b/sentry-spring-boot-4/build.gradle.kts @@ -1,4 +1,5 @@ import io.sentry.gradle.ChildJvmTestArguments +import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -6,6 +7,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) alias(libs.plugins.errorprone) @@ -387,13 +389,17 @@ testing { /** * Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent * attached. [autoInit] decides whether the agent initializes Sentry or, as with - * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. + * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for + * another Spring Boot version. */ -fun Test.runAgentFixture(autoInit: Boolean) { +fun Test.runAgentFixture( + autoInit: Boolean, + appClasspath: FileCollection = agentFixture.runtimeClasspath, +) { jvmArgumentProviders.add( objects.newInstance().apply { agentJar.from(otelAgentJar) - appClasspath.from(agentFixture.runtimeClasspath) + this.appClasspath.from(appClasspath) } ) systemProperty("sentry.test.agentAutoInit", autoInit) @@ -423,3 +429,19 @@ listOf( .forEach { tasks.named(it) { compilerOptions.javaParameters = true } } + +// The suites on each Spring Boot 4 version the CI matrix tests. +springBootMatrix { + versions(SpringBootVersions.BOOT_4) + test("integrationTest") + test("integrationTestLog4j2") + test("integrationTestWebflux") + test("integrationTestOtelNoAgent") + test("integrationTestOtlp") + test("integrationTestOtelAgent") { version -> + runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version)) + } + test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version -> + runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version)) + } +} diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 9451cf9511..3b97650119 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -1,4 +1,5 @@ import io.sentry.gradle.ChildJvmTestArguments +import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -6,6 +7,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -327,13 +329,17 @@ testing { /** * Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent * attached. [autoInit] decides whether the agent initializes Sentry or, as with - * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. + * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for + * another Spring Boot version. */ -fun Test.runAgentFixture(autoInit: Boolean) { +fun Test.runAgentFixture( + autoInit: Boolean, + appClasspath: FileCollection = agentFixture.runtimeClasspath, +) { jvmArgumentProviders.add( objects.newInstance().apply { agentJar.from(otelAgentJar) - appClasspath.from(agentFixture.runtimeClasspath) + this.appClasspath.from(appClasspath) } ) systemProperty("sentry.test.agentAutoInit", autoInit) @@ -362,3 +368,18 @@ listOf( .forEach { tasks.named(it) { compilerOptions.javaParameters = true } } + +// The suites on each Spring Boot 3 version the CI matrix tests. +springBootMatrix { + versions(SpringBootVersions.BOOT_3) + test("integrationTest") + test("integrationTestLog4j2") + test("integrationTestWebflux") + test("integrationTestOtelNoAgent") + test("integrationTestOtelAgent") { version -> + runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version)) + } + test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version -> + runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version)) + } +} diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 3f24e73dc9..c20bdc71e9 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -1,4 +1,5 @@ import io.sentry.gradle.ChildJvmTestArguments +import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -6,6 +7,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -272,13 +274,17 @@ testing { /** * Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent * attached. [autoInit] decides whether the agent initializes Sentry or, as with - * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. + * `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for + * another Spring Boot version. */ -fun Test.runAgentFixture(autoInit: Boolean) { +fun Test.runAgentFixture( + autoInit: Boolean, + appClasspath: FileCollection = agentFixture.runtimeClasspath, +) { jvmArgumentProviders.add( objects.newInstance().apply { agentJar.from(otelAgentJar) - appClasspath.from(agentFixture.runtimeClasspath) + this.appClasspath.from(appClasspath) } ) systemProperty("sentry.test.agentAutoInit", autoInit) @@ -308,3 +314,17 @@ listOf( .forEach { tasks.named(it) { compilerOptions.javaParameters = true } } + +// The suites on each Spring Boot 2 version the CI matrix tests. +springBootMatrix { + versions(SpringBootVersions.BOOT_2) + test("integrationTest") + test("integrationTestWebflux") + test("integrationTestOtelNoAgent") + test("integrationTestOtelAgent") { version -> + runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version)) + } + test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version -> + runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version)) + } +} diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index 701d5a0420..a3478b383f 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -1,3 +1,4 @@ +import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -5,6 +6,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -129,3 +131,9 @@ testing { } } } + +// The suites on each Spring Boot 3 version the CI matrix tests. +springBootMatrix { + versions(SpringBootVersions.BOOT_3) + test("integrationTest") +} diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index f11087c1b0..c9e2620d56 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -1,3 +1,4 @@ +import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -5,6 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") + id("io.sentry.spring-boot-matrix") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -117,3 +119,9 @@ testing { } } } + +// The suites on each Spring Boot 2 version the CI matrix tests. +springBootMatrix { + versions(SpringBootVersions.BOOT_2) + test("integrationTest") +} From 961ffe0ae198a19025ef01034da59dd701efbeac Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 27 Oct 2026 16:04:59 +0100 Subject: [PATCH 2/3] test(spring): Run each Spring Boot version in its own CI job One job per Boot line doubled the matrix wall time, e.g. 12.5 minutes instead of about 6 for Boot 3. A single runner ran every version's suites, and the bare task name made Gradle configure every project to find it. The versions move to gradle/spring-boot-versions.json. build-logic reads it, and each workflow turns its line into one job per version, which runs that version's aggregate in both modules by project path. The version list stays in one place, and the check names match main's again. Co-Authored-By: Claude Opus 5.5 --- .cursor/rules/e2e_tests.mdc | 18 +++++----- .github/workflows/spring-boot-2-matrix.yml | 36 ++++++++++++++++--- .github/workflows/spring-boot-3-matrix.yml | 36 ++++++++++++++++--- .github/workflows/spring-boot-4-matrix.yml | 36 ++++++++++++++++--- .../sentry/gradle/SpringBootMatrixPlugin.kt | 17 +++++---- .../io/sentry/gradle/SpringBootVersions.kt | 11 ------ gradle/spring-boot-versions.json | 5 +++ sentry-spring-7/build.gradle.kts | 3 +- sentry-spring-boot-4/build.gradle.kts | 3 +- sentry-spring-boot-jakarta/build.gradle.kts | 3 +- sentry-spring-boot/build.gradle.kts | 3 +- sentry-spring-jakarta/build.gradle.kts | 3 +- sentry-spring/build.gradle.kts | 3 +- 13 files changed, 125 insertions(+), 52 deletions(-) delete mode 100644 build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt create mode 100644 gradle/spring-boot-versions.json diff --git a/.cursor/rules/e2e_tests.mdc b/.cursor/rules/e2e_tests.mdc index fd52adb14d..3603794cd5 100644 --- a/.cursor/rules/e2e_tests.mdc +++ b/.cursor/rules/e2e_tests.mdc @@ -64,14 +64,16 @@ it. Gradle runs the two in parallel. ## Running them -- The `springBootMatrix` block in each Spring module's build file runs its suites on every Spring - Boot version in `SpringBootVersions` (build-logic), with a task per suite and version, e.g. - `integrationTestWebfluxBoot3_2_12`. The suites and the SDK compile once, against the catalog's - version; only the runtime classpath changes. `allIntegrationTestsBoot3_2_12` runs one version's - suites, `allIntegrationTestsBoot3` every version's. -- The `spring-boot-{2,3,4}-matrix.yml` workflows run `./gradlew allIntegrationTestsBoot`, - which picks up every module of the line. The suites aren't part of `check`, so a new suite only - runs on CI once it's added to its module's `springBootMatrix` block. +- The `springBootMatrix` block in each Spring module's build file runs its suites on every version + of its Spring Boot line in `gradle/spring-boot-versions.json`, with a task per suite and version, + e.g. `integrationTestWebfluxBoot3_2_12`. The suites and the SDK compile once, against the + catalog's version; only the runtime classpath changes. `allIntegrationTestsBoot3_2_12` runs one + version's suites, `allIntegrationTestsBoot3` every version's. +- The `spring-boot-{2,3,4}-matrix.yml` workflows read the same file and run each version in its own + job, e.g. `./gradlew :sentry-spring-jakarta:allIntegrationTestsBoot3_2_12 + :sentry-spring-boot-jakarta:allIntegrationTestsBoot3_2_12`. The suites aren't part of `check`, so + a new suite only runs on CI once it's added to its module's `springBootMatrix` block, and a new + 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`. diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml index e537ca96b9..02b166fca4 100644 --- a/.github/workflows/spring-boot-2-matrix.yml +++ b/.github/workflows/spring-boot-2-matrix.yml @@ -15,10 +15,32 @@ concurrency: cancel-in-progress: true jobs: + versions: + runs-on: ubuntu-latest + name: Spring Boot 2.x versions + outputs: + versions: ${{ steps.versions.outputs.versions }} + + steps: + - name: Checkout Repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: gradle + + - name: Read Spring Boot 2.x versions + id: versions + run: echo "versions=$(jq -c .boot2 gradle/spring-boot-versions.json)" >> "$GITHUB_OUTPUT" + spring-boot-2-matrix: + needs: versions timeout-minutes: 45 runs-on: ubuntu-latest - name: Spring Boot 2.x + strategy: + fail-fast: false + matrix: + springboot-version: ${{ fromJSON(needs.versions.outputs.versions) }} + + name: Spring Boot ${{ matrix.springboot-version }} env: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -47,14 +69,18 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Test integration suites - # Runs every version even after one fails, as separate jobs per version would. - run: ./gradlew allIntegrationTestsBoot2 --continue + # Project paths, unlike a bare task name, keep Gradle from configuring every project. + run: | + task="allIntegrationTestsBoot${SPRINGBOOT_VERSION//./_}" + ./gradlew ":sentry-spring:$task" ":sentry-spring-boot:$task" + env: + SPRINGBOOT_VERSION: ${{ matrix.springboot-version }} - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: - name: test-results-springboot-2 + name: test-results-springboot-2-${{ matrix.springboot-version }} path: | **/build/reports/* **/build/test-results/**/*.xml @@ -63,7 +89,7 @@ jobs: uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 if: always() with: - name: JUnit Spring Boot 2.x + name: JUnit Spring Boot 2.x ${{ matrix.springboot-version }} path: | **/build/test-results/**/*.xml reporter: java-junit diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml index 9140a0f1fc..0cbe496cda 100644 --- a/.github/workflows/spring-boot-3-matrix.yml +++ b/.github/workflows/spring-boot-3-matrix.yml @@ -15,10 +15,32 @@ concurrency: cancel-in-progress: true jobs: + versions: + runs-on: ubuntu-latest + name: Spring Boot 3.x versions + outputs: + versions: ${{ steps.versions.outputs.versions }} + + steps: + - name: Checkout Repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: gradle + + - name: Read Spring Boot 3.x versions + id: versions + run: echo "versions=$(jq -c .boot3 gradle/spring-boot-versions.json)" >> "$GITHUB_OUTPUT" + spring-boot-3-matrix: + needs: versions timeout-minutes: 45 runs-on: ubuntu-latest - name: Spring Boot 3.x + strategy: + fail-fast: false + matrix: + springboot-version: ${{ fromJSON(needs.versions.outputs.versions) }} + + name: Spring Boot ${{ matrix.springboot-version }} env: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -47,14 +69,18 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Test integration suites - # Runs every version even after one fails, as separate jobs per version would. - run: ./gradlew allIntegrationTestsBoot3 --continue + # Project paths, unlike a bare task name, keep Gradle from configuring every project. + run: | + task="allIntegrationTestsBoot${SPRINGBOOT_VERSION//./_}" + ./gradlew ":sentry-spring-jakarta:$task" ":sentry-spring-boot-jakarta:$task" + env: + SPRINGBOOT_VERSION: ${{ matrix.springboot-version }} - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: - name: test-results-springboot-3 + name: test-results-springboot-3-${{ matrix.springboot-version }} path: | **/build/reports/* **/build/test-results/**/*.xml @@ -63,7 +89,7 @@ jobs: uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 if: always() with: - name: JUnit Spring Boot 3.x + name: JUnit Spring Boot 3.x ${{ matrix.springboot-version }} path: | **/build/test-results/**/*.xml reporter: java-junit diff --git a/.github/workflows/spring-boot-4-matrix.yml b/.github/workflows/spring-boot-4-matrix.yml index 3524a66e3c..75070612eb 100644 --- a/.github/workflows/spring-boot-4-matrix.yml +++ b/.github/workflows/spring-boot-4-matrix.yml @@ -15,10 +15,32 @@ concurrency: cancel-in-progress: true jobs: + versions: + runs-on: ubuntu-latest + name: Spring Boot 4.x versions + outputs: + versions: ${{ steps.versions.outputs.versions }} + + steps: + - name: Checkout Repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: gradle + + - name: Read Spring Boot 4.x versions + id: versions + run: echo "versions=$(jq -c .boot4 gradle/spring-boot-versions.json)" >> "$GITHUB_OUTPUT" + spring-boot-4-matrix: + needs: versions timeout-minutes: 45 runs-on: ubuntu-latest - name: Spring Boot 4.x + strategy: + fail-fast: false + matrix: + springboot-version: ${{ fromJSON(needs.versions.outputs.versions) }} + + name: Spring Boot ${{ matrix.springboot-version }} env: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -47,14 +69,18 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Test integration suites - # Runs every version even after one fails, as separate jobs per version would. - run: ./gradlew allIntegrationTestsBoot4 --continue + # Project paths, unlike a bare task name, keep Gradle from configuring every project. + run: | + task="allIntegrationTestsBoot${SPRINGBOOT_VERSION//./_}" + ./gradlew ":sentry-spring-7:$task" ":sentry-spring-boot-4:$task" + env: + SPRINGBOOT_VERSION: ${{ matrix.springboot-version }} - name: Upload test results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: - name: test-results-springboot-4 + name: test-results-springboot-4-${{ matrix.springboot-version }} path: | **/build/reports/* **/build/test-results/**/*.xml @@ -63,7 +89,7 @@ jobs: uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16 if: always() with: - name: JUnit Spring Boot 4.x + name: JUnit Spring Boot 4.x ${{ matrix.springboot-version }} path: | **/build/test-results/**/*.xml reporter: java-junit 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..c0eee3e7b2 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -1,5 +1,6 @@ package io.sentry.gradle +import groovy.json.JsonSlurper import javax.inject.Inject import org.gradle.api.Plugin import org.gradle.api.Project @@ -25,14 +26,18 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project private var versions = emptyList() /** - * 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. + * Registers `allIntegrationTestsBoot` for each version of Spring Boot [line] in + * `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. */ - fun versions(versions: List) { + fun versions(line: Int) { + val file = project.layout.settingsDirectory.file("gradle/spring-boot-versions.json") + val lines = + JsonSlurper().parseText(project.providers.fileContents(file).asText.get()) as Map<*, *> + val versions = (lines["boot$line"] as List<*>).map { it as String } this.versions = versions - val major = versions.map { it.substringBefore('.') }.distinct().single() - project.tasks.register("allIntegrationTestsBoot$major") { + project.tasks.register("allIntegrationTestsBoot$line") { description = "Runs the integration suites on Spring Boot ${versions.joinToString()}." group = LifecycleBasePlugin.VERIFICATION_GROUP dependsOn(versions.map { aggregateName(it) }) diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt deleted file mode 100644 index 588d50ce7c..0000000000 --- a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootVersions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.sentry.gradle - -/** - * The Spring Boot versions the Spring integration suites run on, per major line. A WAR suite runs - * on the same versions as its Boot module, because the Boot BOM decides its Spring version. - */ -object SpringBootVersions { - val BOOT_2 = listOf("2.7.0", "2.7.18") - val BOOT_3 = listOf("3.2.12", "3.3.13", "3.4.13", "3.5.13") - val BOOT_4 = listOf("4.0.0", "4.0.5", "4.1.0") -} diff --git a/gradle/spring-boot-versions.json b/gradle/spring-boot-versions.json new file mode 100644 index 0000000000..efe0fdd062 --- /dev/null +++ b/gradle/spring-boot-versions.json @@ -0,0 +1,5 @@ +{ + "boot2": ["2.7.0", "2.7.18"], + "boot3": ["3.2.12", "3.3.13", "3.4.13", "3.5.13"], + "boot4": ["4.0.0", "4.0.5", "4.1.0"] +} diff --git a/sentry-spring-7/build.gradle.kts b/sentry-spring-7/build.gradle.kts index 2eadb3efb6..5b31c03162 100644 --- a/sentry-spring-7/build.gradle.kts +++ b/sentry-spring-7/build.gradle.kts @@ -1,4 +1,3 @@ -import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -147,6 +146,6 @@ testing { // The suites on each Spring Boot 4 version the CI matrix tests. springBootMatrix { - versions(SpringBootVersions.BOOT_4) + versions(line = 4) test("integrationTest") } diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts index 0077caf712..947657f62c 100644 --- a/sentry-spring-boot-4/build.gradle.kts +++ b/sentry-spring-boot-4/build.gradle.kts @@ -1,5 +1,4 @@ import io.sentry.gradle.ChildJvmTestArguments -import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -432,7 +431,7 @@ listOf( // The suites on each Spring Boot 4 version the CI matrix tests. springBootMatrix { - versions(SpringBootVersions.BOOT_4) + versions(line = 4) test("integrationTest") test("integrationTestLog4j2") test("integrationTestWebflux") diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 3b97650119..8587e9e2a6 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -1,5 +1,4 @@ import io.sentry.gradle.ChildJvmTestArguments -import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -371,7 +370,7 @@ listOf( // The suites on each Spring Boot 3 version the CI matrix tests. springBootMatrix { - versions(SpringBootVersions.BOOT_3) + versions(line = 3) test("integrationTest") test("integrationTestLog4j2") test("integrationTestWebflux") diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index c20bdc71e9..266142dd2b 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -1,5 +1,4 @@ import io.sentry.gradle.ChildJvmTestArguments -import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -317,7 +316,7 @@ listOf( // The suites on each Spring Boot 2 version the CI matrix tests. springBootMatrix { - versions(SpringBootVersions.BOOT_2) + versions(line = 2) test("integrationTest") test("integrationTestWebflux") test("integrationTestOtelNoAgent") diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index a3478b383f..34e9b80bf6 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -1,4 +1,3 @@ -import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -134,6 +133,6 @@ testing { // The suites on each Spring Boot 3 version the CI matrix tests. springBootMatrix { - versions(SpringBootVersions.BOOT_3) + versions(line = 3) test("integrationTest") } diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index c9e2620d56..b5a8440e2b 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -1,4 +1,3 @@ -import io.sentry.gradle.SpringBootVersions import net.ltgt.gradle.errorprone.errorprone import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -122,6 +121,6 @@ testing { // The suites on each Spring Boot 2 version the CI matrix tests. springBootMatrix { - versions(SpringBootVersions.BOOT_2) + versions(line = 2) test("integrationTest") } From e48e7ab60fc9bb0742ef1c6aa715709c64e866e4 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 28 Oct 2026 11:37:37 +0100 Subject: [PATCH 3/3] test(spring): Create the Spring Boot matrix classpaths as resolvable configurations They're only ever resolved, so configurations.resolvable() fits them better than create() with isCanBeConsumed = false: nothing can consume them or declare dependencies on them. Every matrix classpath resolves the same as before. Co-Authored-By: Claude Opus 5.5 --- .../sentry/gradle/SpringBootMatrixPlugin.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 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 c0eee3e7b2..c75a316651 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -80,21 +80,23 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project 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) + ?: configurations + .resolvable(name) { + 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) + } } - } + .get() return sources.output + dependencies }