This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch fix/spring-dm-example-in-commit-bom in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 9bc5aefbf174a9798b5182e0fd146443e79b9694 Author: James Fredley <[email protected]> AuthorDate: Thu Jul 30 20:04:52 2026 -0400 fix: resolve the Spring DM example against the in-commit BOM The Spring Dependency Management example opts out of the native platform(grails-bom) injection and imports grails-bom as a Maven BOM instead. io.spring.dependency-management resolves that import as an artifact-only @pom in its own detached configuration, which never sees the project substitution in gradle/functional-test-config.gradle, so it could only ever be satisfied from a repository. That had two consequences. The example silently validated against the last BOM published to the Apache snapshot repository rather than the one in the commit under test, so a change to dependencies.gradle went unverified there and the effective BOM changed whenever CI published. And on a version that had never been published - a new release branch, immediately after the version bump - the import produced no managed versions at all rather than failing, so every managed dependency resolved with an empty version and the build died with "Could not find <group>:<artifact>:", before CI could publish the snapshot that would have fixed it. Generate the BOM poms this build produces and serve them from a local repository instead: - Stage grails-base-bom, grails-bom, grails-hibernate5-bom and grails-hibernate7-bom into .gradle/local-boms during root configuration. Spring DM resolves its detached import while the task graph is still being computed, so no task dependency can put the poms in place in time. The location is outside build/ so a combined `gradlew clean <task>` cannot delete them between configuration and resolution. - Serve them through an exclusiveContent repository scoped to this build, so the coordinates resolve locally or fail loudly rather than falling back to the remote and quietly reintroducing the stale-BOM behaviour. - Evaluate grails-base-bom last. It classifies sibling projects by whether they already carry java-platform, so evaluating it first made it adopt the other BOMs as managed dependencies and bake that into the published pom. - Stop BomPropertyOverridesPlugin resolving project platforms as external modules. It requested grails.core.ROOT:grails-hibernate5-bom:unspecified, which cannot resolve, leaving those versions unmanaged. The temporary -PbomSnapshotNotPublished guard is no longer needed and the example is included unconditionally again, including during reproducible release builds. Assisted-by: claude-code:claude-opus-5 --- .../buildsrc/GrailsRepoSettingsPlugin.groovy | 33 ++++++++++++++++++++++ build.gradle | 21 +++++++++++++- .../plugin/bom/BomPropertyOverridesPlugin.groovy | 4 +++ .../bom/BomPropertyOverridesPluginSpec.groovy | 20 +++++++++++++ settings.gradle | 25 ++-------------- 5 files changed, 80 insertions(+), 23 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsRepoSettingsPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsRepoSettingsPlugin.groovy index e4675f3cd9..92e2dd9843 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsRepoSettingsPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsRepoSettingsPlugin.groovy @@ -69,6 +69,39 @@ class GrailsRepoSettingsPlugin implements Plugin<Settings> { target.dependencyResolutionManagement { DependencyResolutionManagement manager -> manager.repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) manager.repositories { RepositoryHandler repo -> + // The Spring Dependency Management example imports grails-bom through + // io.spring.dependency-management, which resolves BOM imports with its own detached + // configuration and so never sees the project substitution in + // gradle/functional-test-config.gradle. The root build script writes the BOM poms this + // build generates into .gradle/local-boms, and they are served from here. That location + // is deliberately outside build/, so a combined `gradlew clean <task>` invocation cannot + // delete the poms after configuration has written them but before the example resolves. + // + // This is declared as exclusiveContent deliberately: these coordinates must resolve + // from the local build and nowhere else. A plain content filter would let a missing + // local pom fall through to the remote repositories and silently manage the example + // with the last published BOM instead of the one in this commit - which is the exact + // bug this arrangement exists to prevent, and it fails silently. + // + // Scoped to the grails-core root build, which is the only build that produces these + // poms, so other consumers of this settings plugin are unaffected. + if (new File(target.rootDir, 'grails-test-examples/spring-dependency-management').isDirectory()) { + repo.exclusiveContent { + it.forRepository { + repo.maven { + name = 'grailsLocalTestRepo' + url = new File(target.rootDir, '.gradle/local-boms').toURI() + metadataSources { source -> source.mavenPom() } + } + } + it.filter { filter -> + filter.includeModule('org.apache.grails', 'grails-base-bom') + filter.includeModule('org.apache.grails', 'grails-bom') + filter.includeModule('org.apache.grails', 'grails-hibernate5-bom') + filter.includeModule('org.apache.grails', 'grails-hibernate7-bom') + } + } + } if (System.getenv('GRAILS_INCLUDE_MAVEN_LOCAL')) { repo.mavenLocal() } diff --git a/build.gradle b/build.gradle index 728315011d..a8ba12e677 100644 --- a/build.gradle +++ b/build.gradle @@ -131,6 +131,25 @@ apply { from layout.projectDirectory.file('gradle/rat-root-config.gradle') } +if (findProject(':grails-test-examples-spring-dependency-management')) { + // grails-base-bom is evaluated LAST on purpose. It builds its constraints by walking the other + // projects, and anything not yet carrying java-platform is taken for an ordinary published module - + // so evaluating it first would make it adopt the sibling BOMs as managed dependencies and bake that + // into the published base BOM. Force every platform to be configured before any pom is generated. + def bomNames = ['grails-bom', 'grails-hibernate5-bom', 'grails-hibernate7-bom', 'grails-base-bom'] + bomNames.each { String bomName -> evaluationDependsOn(project(":${bomName}").path) } + bomNames.each { String bomName -> + Project bomProject = project(":${bomName}") + def generatePom = bomProject.tasks.named('generatePomFileForMavenPublication', GenerateMavenPom).get() + generatePom.doGenerate() + copy { + from generatePom.destination + into layout.projectDirectory.dir(".gradle/local-boms/org/apache/grails/${bomName}/${projectVersion}") + rename { "${bomName}-${projectVersion}.pom" } + } + } +} + // For debugging the gradle task graph: Uncomment to show task dependencies when a task is run //gradle.taskGraph.whenReady {taskGraph -> // logger.lifecycle("Found ${taskGraph.allTasks.size()} tasks.") @@ -140,4 +159,4 @@ apply { // logger.lifecycle("\t- ${dep}") // } // } -//} \ No newline at end of file +//} diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPlugin.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPlugin.groovy index 4277b5b16c..95c68a9299 100644 --- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPlugin.groovy +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPlugin.groovy @@ -26,6 +26,7 @@ import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.ModuleDependency +import org.gradle.api.artifacts.ProjectDependency import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.attributes.Category @@ -182,6 +183,9 @@ class BomPropertyOverridesPlugin implements Plugin<Project> { if (!(dep instanceof ModuleDependency)) { continue } + if (dep instanceof ProjectDependency) { + continue + } if (!isPlatformDependency((ModuleDependency) dep)) { continue } diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy index 5ebdd1976d..4d9142c152 100644 --- a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy +++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy @@ -108,6 +108,26 @@ class BomPropertyOverridesPluginSpec extends Specification { 'org.example:enforced-bom:2.0.0' in coordinates } + def "detectDeclaredBoms ignores project platform dependencies"() { + given: + def root = ProjectBuilder.builder().withName('root').build() + def bom = ProjectBuilder.builder().withName('test-bom').withParent(root).build() + bom.group = 'org.example' + bom.version = '1.0.0' + def consumer = ProjectBuilder.builder().withName('consumer').withParent(root).build() + consumer.plugins.apply('java') + consumer.dependencies.add( + 'implementation', + consumer.dependencies.platform(consumer.dependencies.project(path: ':test-bom')) + ) + + when: + def coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(consumer.configurations) + + then: + coordinates.isEmpty() + } + def "detectDeclaredBoms ignores non-platform dependencies"() { given: def project = ProjectBuilder.builder().build() diff --git a/settings.gradle b/settings.gradle index aa32a4f3a2..52569ecade 100644 --- a/settings.gradle +++ b/settings.gradle @@ -609,28 +609,9 @@ project(':grails-test-examples-jetty').projectDir = file('grails-test-examples/j project(':grails-test-examples-undertow').projectDir = file('grails-test-examples/undertow') project(':grails-test-examples-latency').projectDir = file('grails-test-examples/latency') -// The Spring Dependency Management example imports grails-bom as a Maven BOM through -// io.spring.dependency-management, which resolves BOM imports with its own detached -// configurations. Those bypass the local-project substitution rules in -// gradle/functional-test-config.gradle, so the import only works when -// org.apache.grails:grails-bom:<projectVersion> is already published to a repository. -// During a release (reproducible build, SOURCE_DATE_EPOCH set) the version being staged -// is not published anywhere yet, so the example cannot resolve its dependencies - -// exclude it from the build graph entirely. It still builds and runs on every regular -// CI build, where the -SNAPSHOT BOM is available from the Apache snapshots repository. -// -// A newly created release branch is the same condition: the moment projectVersion is -// bumped to a version that has never been published, the BOM import silently resolves -// to nothing, every managed version comes back empty, and the whole build fails with -// "Could not find <group>:<artifact>:" - before CI can ever publish the snapshot that -// would fix it. Set -PbomSnapshotNotPublished (or add it to gradle.properties) on such -// a branch to exclude the example until its first snapshot publish succeeds, then drop -// the flag again. The flag is presence-based; its value is ignored. -def bomSnapshotNotPublished = providers.gradleProperty('bomSnapshotNotPublished').isPresent() -if (!isReproducibleBuild && !bomSnapshotNotPublished) { - include 'grails-test-examples-spring-dependency-management' - project(':grails-test-examples-spring-dependency-management').projectDir = file('grails-test-examples/spring-dependency-management') -} +// The Spring Dependency Management example imports the in-commit BOMs from the local Maven repository. +include 'grails-test-examples-spring-dependency-management' +project(':grails-test-examples-spring-dependency-management').projectDir = file('grails-test-examples/spring-dependency-management') includeBuild('./grails-gradle') { name = 'grails-gradle'
