This is an automated email from the ASF dual-hosted git repository.

jdaugherty pushed a commit to branch fix/spring-dm-example
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit cefc061d34cefa0569ed1d551701ae169e52b6df
Author: James Daugherty <[email protected]>
AuthorDate: Sat Aug 1 18:14:41 2026 -0400

    Isolate mavenBom import requirements into a separate project so the code 
being built is the code being tested
---
 .github/workflows/end-to-end.yml                   |  5 ++-
 build-logic/settings.gradle                        |  8 +++++
 end-to-end/README.md                               | 16 +++++++++
 end-to-end/gradle/test-config.gradle               | 35 ++++++++++++++++++++
 end-to-end/legacy-commands/build.gradle            |  5 +--
 end-to-end/settings.gradle                         | 17 ++++++++++
 .../spring-dependency-management/build.gradle      | 38 ++++++++++++++++------
 .../grails-app/conf/application.yml                |  0
 .../grails-app/controllers/UrlMappings.groovy      |  0
 .../controllers/springdm/HelloController.groovy    |  0
 .../grails-app/init/springdm/Application.groovy    |  0
 .../grails-app/views/error.gsp                     |  0
 .../grails-app/views/index.gsp                     |  0
 .../grails-app/views/notFound.gsp                  |  0
 .../groovy/springdm/HelloControllerSpec.groovy     |  0
 grails-forge/settings.gradle                       |  8 +++++
 grails-gradle/settings.gradle                      |  8 +++++
 settings.gradle                                    | 23 -------------
 18 files changed, 125 insertions(+), 38 deletions(-)

diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml
index ba6bb2206c..40e8f4d47e 100644
--- a/.github/workflows/end-to-end.yml
+++ b/.github/workflows/end-to-end.yml
@@ -20,7 +20,10 @@
 # substitution - that is what makes the tests end-to-end. The repository is 
the same
 # build/local-maven that grails-forge points its generated applications at, 
populated by
 # publishAllPublicationsToTestCaseMavenRepoRepository, so the suite exercises 
real poms and
-# module metadata including the CLI companion artifacts.
+# module metadata including the CLI companion artifacts. That is a hard 
requirement for some of
+# these projects rather than a nicety: spring-dependency-management imports 
grails-bom as a Maven
+# BOM through io.spring.dependency-management, which resolves it in its own 
detached configuration
+# and so can only ever consume a published pom.
 #
 # It also needs two JDKs, which is the other reason it gets its own workflow: 
the Grails 7
 # fixture must be compiled on Java 17 (the minimum for a Grails 7 app, so the 
binary matches
diff --git a/build-logic/settings.gradle b/build-logic/settings.gradle
index d50ff06fec..eb8d699e3f 100644
--- a/build-logic/settings.gradle
+++ b/build-logic/settings.gradle
@@ -16,6 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+import org.apache.tools.ant.DirectoryScanner
 
 pluginManagement {
     apply from: file('../gradle/plugin-repositories.gradle')
@@ -91,3 +92,10 @@ project(':build-logic').projectDir = file('plugins')
 
 include 'grails-docs-core'
 project(':grails-docs-core').projectDir = file('docs-core')
+
+// Due to https://github.com/gradle/gradle/issues/2986 , we can't change the 
global exclude for profiles, it must be done everywhere
+for (String pattern in DirectoryScanner.defaultExcludes) {
+    if (pattern.contains('gitignore') || pattern.contains('gitattributes')) {
+        DirectoryScanner.removeDefaultExclude(pattern)
+    }
+}
diff --git a/end-to-end/README.md b/end-to-end/README.md
index ff0f0c77b9..228030dc32 100644
--- a/end-to-end/README.md
+++ b/end-to-end/README.md
@@ -35,12 +35,21 @@ applications at via `GRAILS_REPO_URL`.
 | `legacy-g7-command-plugin` | A **standalone build**, not part of this one. 
Compiles against published Grails 7 / Groovy 4 to produce a genuine precompiled 
`grails.dev.commands.ApplicationCommand` binary. |
 | `legacy-commands-plugin` | A Grails 8 plugin whose legacy commands are 
recompiled under Groovy 5. |
 | `legacy-commands` | A Grails 8 application that consumes both and runs their 
commands through the registry. |
+| `spring-dependency-management` | A Grails 8 application that manages its 
versions with the legacy `io.spring.dependency-management` plugin instead of 
the Grails Gradle plugin's native `platform(grails-bom)`, as an upgraded Grails 
7 application does. |
 
 `legacy-g7-command-plugin` is deliberately excluded from `settings.gradle`. An 
included build would
 substitute `org.apache.grails:grails-core` for this repository's Groovy 5 
project, which is exactly
 the substitution the fixture exists to avoid — it must be compiled by a real 
Grails 7 toolchain for
 its trait-woven bytecode to prove anything.
 
+`spring-dependency-management` is here because Spring DM imports a BOM as a 
**Maven** BOM, resolving
+it in its own detached configuration. That bypasses any project substitution, 
so the import can only
+ever be satisfied by a published 
`org.apache.grails:grails-bom:<projectVersion>` — which is precisely
+what this build already provides. In the core build it had to be excluded 
whenever nothing had been
+published yet (a reproducible release build, or a fresh release branch whose 
version has never been
+published), and otherwise silently fell back to whatever the Apache snapshot 
repository happened to
+hold rather than the working tree.
+
 ## JDKs
 
 The Grails 7 half declares the JDK it needs in a `.sdkmanrc`, rather than a 
Gradle toolchain, so that
@@ -81,6 +90,13 @@ cd end-to-end
 
 Re-run the publish whenever you change something in the core build that these 
tests exercise;
 nothing here can detect that for you, because the whole point is that the 
build boundary is real.
+For the same reason a re-run against freshly published artifacts often comes 
back `UP-TO-DATE` —
+nothing Gradle can see about these projects changed. `DO_NOT_CACHE_TESTS` 
(honoured here exactly as
+in the core build, see `DEVELOPMENT.md`) forces the test tasks to run anyway:
+
+```shell
+DO_NOT_CACHE_TESTS=1 ./gradlew check
+```
 
 CI does the same three steps, reading both JDK majors out of the `.sdkmanrc` 
files. Only the major
 is honoured there: `sdk env` gives a developer the exact Liberica patch, but 
these are test builds
diff --git a/end-to-end/gradle/test-config.gradle 
b/end-to-end/gradle/test-config.gradle
new file mode 100644
index 0000000000..90d2e2f2b1
--- /dev/null
+++ b/end-to-end/gradle/test-config.gradle
@@ -0,0 +1,35 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+// Set DO_NOT_CACHE_TESTS=1 to force test tasks to re-run on every invocation 
without needing
+// --rerun-tasks. Useful for repeatedly running the same test command while 
chasing flaky tests
+// across runs. The core build, grails-gradle and grails-forge all honour the 
same variable.
+def doNotCacheTests = System.getenv('DO_NOT_CACHE_TESTS')?.toBoolean()
+
+tasks.withType(Test).configureEach {
+    useJUnitPlatform()
+
+    // Honor DO_NOT_CACHE_TESTS=1 so developers can repeatedly invoke the same 
test command
+    // without --rerun-tasks (and without recompiling everything else). It 
matters more in this
+    // build than in the core one: republishing grails-core does not change 
these projects'
+    // inputs in any way Gradle can see, so re-running against freshly 
published artifacts is
+    // exactly the case that otherwise comes back UP-TO-DATE or FROM-CACHE.
+    outputs.cacheIf { !doNotCacheTests }
+    outputs.upToDateWhen { !doNotCacheTests }
+}
diff --git a/end-to-end/legacy-commands/build.gradle 
b/end-to-end/legacy-commands/build.gradle
index 7ff244dba0..6c42ca1fb6 100644
--- a/end-to-end/legacy-commands/build.gradle
+++ b/end-to-end/legacy-commands/build.gradle
@@ -89,10 +89,7 @@ dependencies {
 // by its own workflow.
 apply {
     from 
rootProject.layout.projectDirectory.file('../gradle/grails-extension-gradle-config.gradle')
-}
-
-tasks.withType(Test).configureEach {
-    useJUnitPlatform()
+    from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
 }
 
 // Opt in to the Grails 7 application-command bridge (off by default on Grails 
8).
diff --git a/end-to-end/settings.gradle b/end-to-end/settings.gradle
index baa921cd5c..28629497b9 100644
--- a/end-to-end/settings.gradle
+++ b/end-to-end/settings.gradle
@@ -16,6 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+import org.apache.tools.ant.DirectoryScanner
 
 pluginManagement {
     includeBuild('../grails-gradle') {
@@ -103,6 +104,15 @@ rootProject.name = 'grails-end-to-end'
 include(
         'legacy-commands',
         'legacy-commands-plugin',
+        // Belongs here rather than in grails-test-examples: it imports 
grails-bom as a Maven BOM
+        // through io.spring.dependency-management, which resolves imports in 
its own detached
+        // configuration. That bypasses any project substitution, so the 
import can only ever be
+        // satisfied by a published 
org.apache.grails:grails-bom:<projectVersion> - which is what
+        // the repository above is. In the core build it had to be excluded 
whenever nothing had
+        // been published yet (a reproducible release build, or a fresh 
release branch whose
+        // version has never been published), and otherwise resolved against 
whatever the Apache
+        // snapshot repository held rather than the working tree.
+        'spring-dependency-management',
 )
 
 // legacy-g7-command-plugin is deliberately NOT included here. It compiles 
against published
@@ -110,3 +120,10 @@ include(
 // org.apache.grails:grails-core for this repo's Groovy 5 project - which is 
exactly the
 // substitution the fixture exists to avoid. It stays a standalone build, run 
under its own JDK
 // (legacy-g7-command-plugin/.sdkmanrc), and legacy-commands consumes the jar 
it produces.
+
+// Due to https://github.com/gradle/gradle/issues/2986 , we can't change the 
global exclude for profiles, it must be done everywhere
+for (String pattern in DirectoryScanner.defaultExcludes) {
+    if (pattern.contains('gitignore') || pattern.contains('gitattributes')) {
+        DirectoryScanner.removeDefaultExclude(pattern)
+    }
+}
diff --git a/grails-test-examples/spring-dependency-management/build.gradle 
b/end-to-end/spring-dependency-management/build.gradle
similarity index 63%
rename from grails-test-examples/spring-dependency-management/build.gradle
rename to end-to-end/spring-dependency-management/build.gradle
index be0ec5acbb..ac4f7e9edb 100644
--- a/grails-test-examples/spring-dependency-management/build.gradle
+++ b/end-to-end/spring-dependency-management/build.gradle
@@ -30,22 +30,26 @@
 //   * io.spring.dependency-management is applied directly and imports 
grails-bom as
 //     a Maven BOM, exactly as a migrated Grails 7 build would, so that the 
Spring DM
 //     plugin is the source of truth for managed versions here.
+//
+// It is an end-to-end test because a Maven BOM import is only satisfiable by 
a real
+// published pom - see the note on the include in settings.gradle.
 plugins {
+    id 'groovy'
     id 'org.apache.grails.buildsrc.properties'
     id 'org.apache.grails.buildsrc.compile'
     id 'org.apache.grails.buildsrc.vulnerability-scan'
+    id 'org.apache.grails.gradle.grails-web'
+    id 'org.apache.grails.gradle.grails-gsp'
 }
 
 version = '0.1'
-group = 'functionaltests'
-
-apply plugin: 'org.apache.grails.gradle.grails-web'
-apply plugin: 'org.apache.grails.gradle.grails-gsp'
+group = 'springdm'
 
-// io.spring.dependency-management is already available on the build 
classpath, so it is
-// applied here without a version (an explicit version in the plugins {} block 
fails when a
-// plugin is already on the classpath with an unknown version). A real 
end-user Grails build
-// would instead declare it in its own plugins {} block with a version.
+// io.spring.dependency-management arrives on the build classpath 
transitively, through the Grails
+// Gradle plugin's dependency on spring-boot-gradle-plugin, so it is applied 
here without a version
+// (an explicit version in the plugins {} block fails when a plugin is already 
on the classpath with
+// an unknown version). A real end-user Grails build would instead declare it 
in its own plugins {}
+// block with a version.
 apply plugin: 'io.spring.dependency-management'
 
 grails {
@@ -55,6 +59,8 @@ grails {
 
 dependencyManagement {
     imports {
+        // Resolved by Spring DM's own detached configuration, so it is a 
genuine Maven BOM import
+        // of the pom the core build published into build/local-maven - not a 
substituted project.
         mavenBom "org.apache.grails:grails-bom:${projectVersion}"
     }
 }
@@ -63,7 +69,7 @@ dependencyManagement {
 // the grails-bom import, so the security-patched logback isn't applied 
automatically. A migrated Grails 7
 // app hitting the same CVE would override the Spring-managed version property 
directly; reproduce that here,
 // sourcing the number from dependencies.gradle so it stays the single source 
of truth.
-apply from: rootProject.layout.projectDirectory.file('dependencies.gradle')
+apply from: rootProject.layout.projectDirectory.file('../dependencies.gradle')
 ext['logback.version'] = bomDependencyVersions['logback.version']
 // Same situation for the Jackson 3 security override (CVE-2026-59889) - see 
jackson3.version in dependencies.gradle.
 ext['jackson-bom.version'] = bomDependencyVersions['jackson3.version']
@@ -81,6 +87,18 @@ dependencies {
     integrationTestImplementation 
'org.apache.grails:grails-testing-support-http-client'
 }
 
+// The core build's gradle/functional-test-config.gradle is deliberately not 
applied here, for the
+// same reasons as in legacy-commands: its dependency substitution enumerates
+// rootProject.subprojects, which only makes sense inside the core build - and 
substituting
+// projects is the very thing this project must not do, since the point is to 
import a published
+// grails-bom pom. Its remaining job, the per-suite skip flags keyed off 
grails-test-examples-*
+// project names, has no meaning in this build, which is driven by its own 
workflow.
 apply {
-    from 
rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle')
+    from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+}
+
+tasks.withType(Test).configureEach {
+    // Allow extra headroom for slow start-up under CI load (the client 
default is 60s), matching
+    // what gradle/functional-test-config.gradle gave this application in the 
core build.
+    systemProperty('grails.http.client.timeout', '120')
 }
diff --git 
a/grails-test-examples/spring-dependency-management/grails-app/conf/application.yml
 b/end-to-end/spring-dependency-management/grails-app/conf/application.yml
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/grails-app/conf/application.yml
rename to 
end-to-end/spring-dependency-management/grails-app/conf/application.yml
diff --git 
a/grails-test-examples/spring-dependency-management/grails-app/controllers/UrlMappings.groovy
 
b/end-to-end/spring-dependency-management/grails-app/controllers/UrlMappings.groovy
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/grails-app/controllers/UrlMappings.groovy
rename to 
end-to-end/spring-dependency-management/grails-app/controllers/UrlMappings.groovy
diff --git 
a/grails-test-examples/spring-dependency-management/grails-app/controllers/springdm/HelloController.groovy
 
b/end-to-end/spring-dependency-management/grails-app/controllers/springdm/HelloController.groovy
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/grails-app/controllers/springdm/HelloController.groovy
rename to 
end-to-end/spring-dependency-management/grails-app/controllers/springdm/HelloController.groovy
diff --git 
a/grails-test-examples/spring-dependency-management/grails-app/init/springdm/Application.groovy
 
b/end-to-end/spring-dependency-management/grails-app/init/springdm/Application.groovy
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/grails-app/init/springdm/Application.groovy
rename to 
end-to-end/spring-dependency-management/grails-app/init/springdm/Application.groovy
diff --git 
a/grails-test-examples/spring-dependency-management/grails-app/views/error.gsp 
b/end-to-end/spring-dependency-management/grails-app/views/error.gsp
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/grails-app/views/error.gsp
rename to end-to-end/spring-dependency-management/grails-app/views/error.gsp
diff --git 
a/grails-test-examples/spring-dependency-management/grails-app/views/index.gsp 
b/end-to-end/spring-dependency-management/grails-app/views/index.gsp
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/grails-app/views/index.gsp
rename to end-to-end/spring-dependency-management/grails-app/views/index.gsp
diff --git 
a/grails-test-examples/spring-dependency-management/grails-app/views/notFound.gsp
 b/end-to-end/spring-dependency-management/grails-app/views/notFound.gsp
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/grails-app/views/notFound.gsp
rename to end-to-end/spring-dependency-management/grails-app/views/notFound.gsp
diff --git 
a/grails-test-examples/spring-dependency-management/src/integration-test/groovy/springdm/HelloControllerSpec.groovy
 
b/end-to-end/spring-dependency-management/src/integration-test/groovy/springdm/HelloControllerSpec.groovy
similarity index 100%
rename from 
grails-test-examples/spring-dependency-management/src/integration-test/groovy/springdm/HelloControllerSpec.groovy
rename to 
end-to-end/spring-dependency-management/src/integration-test/groovy/springdm/HelloControllerSpec.groovy
diff --git a/grails-forge/settings.gradle b/grails-forge/settings.gradle
index 3c3a65805d..545764f7f4 100644
--- a/grails-forge/settings.gradle
+++ b/grails-forge/settings.gradle
@@ -16,6 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+import org.apache.tools.ant.DirectoryScanner
 import org.gradle.api.initialization.resolve.RepositoriesMode
 
 pluginManagement {
@@ -78,3 +79,10 @@ includeBuild('..') {
 includeBuild('../grails-gradle') {
     name = 'grails-gradle'
 }
+
+// Due to https://github.com/gradle/gradle/issues/2986 , we can't change the 
global exclude for profiles, it must be done everywhere
+for (String pattern in DirectoryScanner.defaultExcludes) {
+    if (pattern.contains('gitignore') || pattern.contains('gitattributes')) {
+        DirectoryScanner.removeDefaultExclude(pattern)
+    }
+}
diff --git a/grails-gradle/settings.gradle b/grails-gradle/settings.gradle
index 406ae33b6b..7b52247893 100644
--- a/grails-gradle/settings.gradle
+++ b/grails-gradle/settings.gradle
@@ -16,6 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+import org.apache.tools.ant.DirectoryScanner
 import org.gradle.api.initialization.resolve.RepositoriesMode
 
 pluginManagement {
@@ -78,3 +79,10 @@ project(':grails-gradle-model').projectDir = file('model')
 
 include 'grails-gradle-tasks'
 project(':grails-gradle-tasks').projectDir = file('tasks')
+
+// Due to https://github.com/gradle/gradle/issues/2986 , we can't change the 
global exclude for profiles, it must be done everywhere
+for (String pattern in DirectoryScanner.defaultExcludes) {
+    if (pattern.contains('gitignore') || pattern.contains('gitattributes')) {
+        DirectoryScanner.removeDefaultExclude(pattern)
+    }
+}
diff --git a/settings.gradle b/settings.gradle
index aa32a4f3a2..f601e6c61b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -609,29 +609,6 @@ 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')
-}
-
 includeBuild('./grails-gradle') {
     name = 'grails-gradle'
 }

Reply via email to