You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/08/01 22:14:56 UTC
(grails-core) 01/01: Isolate mavenBom import requirements into a separate project so the code being built is the code being tested
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 <jd...@jdresources.net>
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'
}