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'

Reply via email to