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' }
