From ca495ab5287aeba5d2300a997a6c24145e866bc4 Mon Sep 17 00:00:00 2001
From: James Fredley Parses BOM POM files to build a mapping of Maven property names
+ * (e.g., {@code slf4j.version}) to the artifacts they control. At
+ * dependency resolution time, checks whether the user has overridden
+ * any of these properties via {@code ext['property.name']} in
+ * {@code build.gradle} or via {@code gradle.properties}, and applies
+ * those overrides using Gradle's {@code ResolutionStrategy.eachDependency()}. Gradle's native {@code platform()} mechanism handles the base
+ * BOM import and default version management. This class only adds the
+ * one feature Gradle lacks: property-based version customization
+ * (see Gradle #9160). Usage: to override a version managed by the Grails or Spring Boot BOM, set the
+ * corresponding property in {@code gradle.properties} or {@code build.gradle}: Verifies that {@link BomManagedVersions} correctly parses BOM POM
+ * files, extracts property-to-artifact mappings, and that the Grails
+ * Gradle plugin applies {@code grails-bom} as a Gradle
+ * {@code platform()} dependency. Uses Gradle TestKit to verify that the Grails Gradle plugin correctly
+ * applies {@code grails-bom} as a Gradle {@code platform()} dependency
+ * and no longer depends on the Spring Dependency Management plugin.
+ *
+ *
+ *
+ * // gradle.properties
+ * slf4j.version=1.7.36
+ *
+ * // or build.gradle
+ * ext['slf4j.version'] = '1.7.36'
+ *
+ *
+ * @see BomManagedVersions
+ * @since 8.0
+ */
+ protected void applyGrailsBom(Project project) {
+ String grailsVersion = (project.findProperty('grailsVersion') ?: BuildSettings.grailsVersion) as String
+ String bomCoordinates = "org.apache.grails:grails-bom:${grailsVersion}" as String
+
+ project.dependencies.add('implementation', project.dependencies.platform(bomCoordinates))
- applyBomImport(dme, project)
+ project.afterEvaluate {
+ BomManagedVersions managedVersions = BomManagedVersions.resolve(project, bomCoordinates)
+ if (managedVersions.hasOverrides()) {
+ project.configurations.configureEach { Configuration conf ->
+ managedVersions.applyTo(conf)
+ }
}
}
}
@@ -377,13 +403,6 @@ ${importStatements}
}
}
- @CompileDynamic
- private void applyBomImport(DependencyManagementExtension dme, project) {
- dme.imports({
- mavenBom("org.apache.grails:grails-bom:${project.properties['grailsVersion']}")
- })
- }
-
protected String getDefaultProfile() {
'web'
}
diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy
new file mode 100644
index 00000000000..6953c1ba3eb
--- /dev/null
+++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+package org.grails.gradle.plugin.core
+
+import spock.lang.Specification
+
+/**
+ * Tests for the Grails BOM platform integration that replaced the
+ * Spring Dependency Management plugin.
+ *
+ *
This is the underlying utility used by the
+ * {@code org.apache.grails.gradle.bom-property-overrides} plugin. It is
+ * BOM-agnostic and can be used directly with any BOM that follows the
+ * Maven {@code
Exposed on the project as {@code bomPropertyOverrides}:
+ * + *
+ * apply plugin: 'org.apache.grails.gradle.bom-property-overrides'
+ *
+ * bomPropertyOverrides {
+ * // Disable scanning declared platform() dependencies (default: true)
+ * autoDetect = false
+ *
+ * // Add explicit BOM coordinates - useful when a BOM is referenced
+ * // indirectly (e.g., through a parent plugin) and not declared
+ * // directly via platform() in the consumer's build.gradle
+ * bom 'org.example:my-bom:1.0.0'
+ * bom 'org.example:other-bom:2.0.0'
+ * }
+ *
+ * // Override versions via gradle.properties or ext[]
+ * ext['slf4j.version'] = '2.0.13'
+ *
+ *
+ * By default ({@code autoDetect = true}) the plugin scans every project + * configuration for declared {@code platform()} / {@code enforcedPlatform()} + * dependencies and registers each unique BOM for property-override + * processing. Explicit entries added via {@link #bom(String)} are always + * processed in addition to auto-detected ones, regardless of the + * {@code autoDetect} flag.
+ * + * @since 8.0 + */ +@CompileStatic +class BomPropertyOverridesExtension { + + /** + * The name of the project extension exposed on every project. + */ + static final String EXTENSION_NAME = 'bomPropertyOverrides' + + /** + * Whether to auto-detect BOMs from declared {@code platform()} / + * {@code enforcedPlatform()} dependencies on the project's + * configurations. Defaults to {@code true}. + */ + final PropertyThis is the BOM-agnostic, generically reusable extraction of the
+ * property-override mechanism that historically lived inside the Spring
+ * Dependency Management plugin. Apply it to any project that consumes a
+ * BOM published with version property references in its
+ * {@code
+ * plugins {
+ * id 'org.apache.grails.gradle.bom-property-overrides'
+ * }
+ *
+ * dependencies {
+ * implementation platform('com.example:my-bom:1.0.0')
+ * }
+ *
+ * // gradle.properties or build.gradle
+ * ext['slf4j.version'] = '2.0.13'
+ *
+ *
+ * The plugin does not declare any platforms itself. + * Consumers (or other plugins like {@code grails-app}) remain responsible + * for declaring the {@code platform()} dependencies; this plugin only + * adds the property-override layer on top.
+ * + * @since 8.0 + * @see BomManagedVersions + * @see BomPropertyOverridesExtension + */ +@CompileStatic +class BomPropertyOverridesPlugin implements PluginVerifies that {@link BomManagedVersions} correctly parses BOM POM - * files, extracts property-to-artifact mappings, and that the Grails - * Gradle plugin applies {@code grails-bom} as a Gradle - * {@code platform()} dependency.
+ *Verifies that the utility correctly parses BOM POM files,
+ * extracts {@code
Uses Gradle TestKit to apply the plugin in isolation (without any + * Grails plugins) to verify that the plugin can be used generically with + * any BOM. Confirms the plugin registers its extension, auto-detects + * declared {@code platform()} / {@code enforcedPlatform()} dependencies, + * and skips non-platform dependencies.
+ * + * @since 8.0 + */ +class BomPropertyOverridesPluginFunctionalSpec extends GradleSpecification { + + def "plugin registers extension, autoDetect defaults to true, and identifies declared platforms"() { + given: + setupTestResourceProject('bom-property-overrides-basic') + + when: + def result = executeTask('inspectBomSetup') + + then: 'extension is registered with default autoDetect=true' + result.output.contains('HAS_EXTENSION=true') + result.output.contains('AUTO_DETECT_DEFAULT=true') + + and: 'auto-detect identifies both regular and enforced platforms but skips non-platform dependencies' + result.output.contains('DETECTED_BOMS=org.example:enforced-bom:2.0.0,org.example:test-bom:1.0.0') + } +} diff --git a/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy b/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy new file mode 100644 index 00000000000..c97f164c09f --- /dev/null +++ b/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy @@ -0,0 +1,146 @@ +/* + * 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. + */ +package org.grails.gradle.plugin.bom + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification + +/** + * Unit-level tests for {@link BomPropertyOverridesPlugin} using + * {@link ProjectBuilder}. + * + *Verifies plugin application creates the {@code bomPropertyOverrides} + * extension with sensible defaults, that the extension's DSL methods + * register explicit BOM coordinates, and that auto-detect identifies + * declared {@code platform()} / {@code enforcedPlatform()} dependencies.
+ * + * @since 8.0 + */ +class BomPropertyOverridesPluginSpec extends Specification { + + def "applying the plugin registers the bomPropertyOverrides extension"() { + given: + Project project = ProjectBuilder.builder().build() + + when: + project.plugins.apply(BomPropertyOverridesPlugin) + + then: + BomPropertyOverridesExtension extension = + project.extensions.findByType(BomPropertyOverridesExtension) + extension != null + extension.autoDetect.get() == true + extension.boms.get().isEmpty() + } + + def "extension bom() method registers explicit BOM coordinates"() { + given: + Project project = ProjectBuilder.builder().build() + project.plugins.apply(BomPropertyOverridesPlugin) + BomPropertyOverridesExtension extension = + project.extensions.getByType(BomPropertyOverridesExtension) + + when: + extension.bom('org.example:my-bom:1.0.0') + extension.bom('org.example:other-bom:2.0.0') + + then: + extension.boms.get() == ['org.example:my-bom:1.0.0', 'org.example:other-bom:2.0.0'] + } + + def "extension boms() vararg method registers multiple BOMs"() { + given: + Project project = ProjectBuilder.builder().build() + project.plugins.apply(BomPropertyOverridesPlugin) + BomPropertyOverridesExtension extension = + project.extensions.getByType(BomPropertyOverridesExtension) + + when: + extension.boms('org.example:a:1.0.0', 'org.example:b:2.0.0', 'org.example:c:3.0.0') + + then: + extension.boms.get() == ['org.example:a:1.0.0', 'org.example:b:2.0.0', 'org.example:c:3.0.0'] + } + + def "detectDeclaredBoms finds regular platform() dependencies"() { + given: + Project project = ProjectBuilder.builder().build() + project.plugins.apply('java') + project.dependencies.add( + 'implementation', + project.dependencies.platform('org.example:test-bom:1.0.0') + ) + + when: + SetHandles temp directory management, GradleRunner setup, test + * resource project copying, and common build assertions. Mirrors the + * helper used by the {@code grails-gradle-plugins} module.
+ * + * @since 8.0 + */ +abstract class GradleSpecification extends Specification { + + private static Path basePath + private static GradleRunner gradleRunner + + /** Project version injected by Gradle test config. */ + protected static final String PROJECT_VERSION = System.getProperty('projectVersion') + + void setupSpec() { + basePath = Files.createTempDirectory('bom-property-overrides-projects') + Path testKitDir = Files.createDirectories(basePath.resolve('.gradle')) + gradleRunner = GradleRunner.create() + .withPluginClasspath() + .withTestKitDir(testKitDir.toFile()) + } + + void cleanup() { + basePath?.toFile()?.listFiles()?.each { + if (it.name == '.gradle') { + return + } + it.deleteDir() + } + } + + void cleanupSpec() { + basePath?.toFile()?.deleteDir() + } + + /** + * Sets up a test project from resource files under + * {@code src/test/resources/test-projects/{projectName}}. + * + *Files are copied to a temp directory. Any occurrence of + * {@code __PROJECT_VERSION__} in {@code .gradle} files is replaced + * with the actual project version.
+ */ + protected GradleRunner setupTestResourceProject(String projectName) { + Path destination = basePath.resolve(projectName) + Files.createDirectories(destination) + + Path source = Path.of("src/test/resources/test-projects/${projectName}") + copyDirectory(source, destination) + + gradleRunner.withProjectDir(destination.toFile()) + } + + /** + * Executes a Gradle task and returns the build result. + */ + protected BuildResult executeTask(String taskName, ListThis replaces the Spring Dependency Management plugin with two + * orthogonal pieces:
*Usage: to override a version managed by the Grails or Spring Boot BOM, set the @@ -376,7 +385,7 @@ ${importStatements} * ext['slf4j.version'] = '1.7.36' * * - * @see BomManagedVersions + * @see BomPropertyOverridesPlugin * @since 8.0 */ protected void applyGrailsBom(Project project) { @@ -406,14 +415,13 @@ ${importStatements} } } - project.afterEvaluate { - BomManagedVersions managedVersions = BomManagedVersions.resolve(project, bomCoordinates) - if (managedVersions.hasOverrides()) { - project.configurations.configureEach { Configuration conf -> - managedVersions.applyTo(conf) - } - } - } + // Delegate property-based version overrides to the standalone plugin. + // Auto-detect picks up the platform(grails-bom) declarations we just + // added, plus any additional platform()/enforcedPlatform() the user + // declares (e.g. grails-micronaut-bom). Users can extend the override + // surface by declaring their own platforms - no extra configuration + // is required here. + project.plugins.apply(BomPropertyOverridesPlugin) } private static boolean isExcludedFromBomPlatform(String name) { diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy index 4e1a5ba3d67..efd0b2ac69e 100644 --- a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy +++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy @@ -22,16 +22,17 @@ package org.grails.gradle.plugin.core * Functional tests for the Gradle platform-based BOM integration. * *
Uses Gradle TestKit to verify that the Grails Gradle plugin correctly - * applies {@code grails-bom} as a Gradle {@code platform()} dependency - * and no longer depends on the Spring Dependency Management plugin.
+ * applies {@code grails-bom} as a Gradle {@code platform()} dependency, + * applies the standalone + * {@code org.apache.grails.gradle.bom-property-overrides} plugin, and no + * longer depends on the Spring Dependency Management plugin. * * @since 8.0 - * @see BomManagedVersions * @see GrailsGradlePlugin#applyGrailsBom */ class BomPlatformFunctionalSpec extends GradleSpecification { - def "plugin applies grails-bom as Gradle platform and does not apply Spring DM plugin"() { + def "plugin applies grails-bom as Gradle platform, applies bom-property-overrides plugin, and does not apply Spring DM plugin"() { given: setupTestResourceProject('bom-platform-basic') @@ -40,6 +41,7 @@ class BomPlatformFunctionalSpec extends GradleSpecification { then: result.output.contains('HAS_PLATFORM_BOM=true') + result.output.contains('HAS_BOM_PROPERTY_OVERRIDES=true') result.output.contains('HAS_SPRING_DM=false') } } diff --git a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle index 9b58b700622..be78e2ce8a6 100644 --- a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle +++ b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle @@ -10,6 +10,9 @@ tasks.register('inspectBomSetup') { } println "HAS_PLATFORM_BOM=${hasPlatform}" + def hasBomPropertyOverrides = project.plugins.findPlugin('org.apache.grails.gradle.bom-property-overrides') != null + println "HAS_BOM_PROPERTY_OVERRIDES=${hasBomPropertyOverrides}" + def hasSpringDm = project.plugins.findPlugin('io.spring.dependency-management') != null println "HAS_SPRING_DM=${hasSpringDm}" } diff --git a/grails-gradle/settings.gradle b/grails-gradle/settings.gradle index 406ae33b6b6..fc1a9e16dd3 100644 --- a/grails-gradle/settings.gradle +++ b/grails-gradle/settings.gradle @@ -78,3 +78,6 @@ project(':grails-gradle-model').projectDir = file('model') include 'grails-gradle-tasks' project(':grails-gradle-tasks').projectDir = file('tasks') + +include 'grails-gradle-bom-property-overrides' +project(':grails-gradle-bom-property-overrides').projectDir = file('bom-property-overrides') From bbf5e5320c72c762b9cb1ef56961007fc0300fdb Mon Sep 17 00:00:00 2001 From: James FredleyWhen multiple known BOMs are declared on the same project (for example, + * the {@code grails-app} plugin auto-injects {@code platform(grails-bom)} on + * every declarable configuration while a Micronaut project additionally + * declares {@code enforcedPlatform(grails-micronaut-bom)}), this method + * prefers an {@code enforcedPlatform} declaration over a regular + * {@code platform}. The enforced BOM is the one whose constraints actually + * win at resolution time, so it is the correct reference for the + * "expected" versions reported by the validator.
*/ static String detectBomPath(Project project) { + String regularPlatformBomPath = null + for (Configuration config : project.configurations) { for (Dependency dep : config.dependencies) { - if (BOM_PROJECT_NAMES.contains(dep.name)) { - Project bomProject = project.rootProject.findProject(":${dep.name}" as String) - if (bomProject != null) { - return bomProject.path - } + if (!BOM_PROJECT_NAMES.contains(dep.name)) { + continue + } + Project bomProject = project.rootProject.findProject(":${dep.name}" as String) + if (bomProject == null) { + continue + } + if (isEnforcedPlatformDependency(dep)) { + return bomProject.path + } + if (regularPlatformBomPath == null) { + regularPlatformBomPath = bomProject.path } } } - null + + regularPlatformBomPath + } + + private static boolean isEnforcedPlatformDependency(Dependency dep) { + if (!(dep instanceof ModuleDependency)) { + return false + } + Object categoryAttr = ((ModuleDependency) dep).attributes.getAttribute(Category.CATEGORY_ATTRIBUTE) + if (categoryAttr == null) { + return false + } + categoryAttr.toString() == Category.ENFORCED_PLATFORM } /** From 525409791329c281a657409956de95cc1ba6f634 Mon Sep 17 00:00:00 2001 From: James FredleyHandles temp directory management, GradleRunner setup, test - * resource project copying, and common build assertions. Mirrors the - * helper used by the {@code grails-gradle-plugins} module.
- * - * @since 8.0 - */ -abstract class GradleSpecification extends Specification { - - private static Path basePath - private static GradleRunner gradleRunner - - /** Project version injected by Gradle test config. */ - protected static final String PROJECT_VERSION = System.getProperty('projectVersion') - - void setupSpec() { - basePath = Files.createTempDirectory('bom-property-overrides-projects') - Path testKitDir = Files.createDirectories(basePath.resolve('.gradle')) - gradleRunner = GradleRunner.create() - .withPluginClasspath() - .withTestKitDir(testKitDir.toFile()) - } - - void cleanup() { - basePath?.toFile()?.listFiles()?.each { - if (it.name == '.gradle') { - return - } - it.deleteDir() - } - } - - void cleanupSpec() { - basePath?.toFile()?.deleteDir() - } - - /** - * Sets up a test project from resource files under - * {@code src/test/resources/test-projects/{projectName}}. - * - *Files are copied to a temp directory. Any occurrence of - * {@code __PROJECT_VERSION__} in {@code .gradle} files is replaced - * with the actual project version.
- */ - protected GradleRunner setupTestResourceProject(String projectName) { - Path destination = basePath.resolve(projectName) - Files.createDirectories(destination) - - Path source = Path.of("src/test/resources/test-projects/${projectName}") - copyDirectory(source, destination) - - gradleRunner.withProjectDir(destination.toFile()) - } - - /** - * Executes a Gradle task and returns the build result. - */ - protected BuildResult executeTask(String taskName, ListThis is the underlying utility used by the
- * {@code org.apache.grails.gradle.bom-property-overrides} plugin. It is
- * BOM-agnostic and can be used directly with any BOM that follows the
- * Maven {@code
This is the BOM-agnostic, generically reusable extraction of the
- * property-override mechanism that historically lived inside the Spring
- * Dependency Management plugin. Apply it to any project that consumes a
- * BOM published with version property references in its
- * {@code
This is the BOM-agnostic replacement for the Spring Dependency
+ * Management plugin's property-override feature. The plugin is shipped as
+ * part of {@code grails-gradle-plugins} but can be applied to any project
+ * (Grails or otherwise) that consumes a BOM published with version
+ * property references in its {@code
* plugins {
@@ -86,7 +86,7 @@ class BomPropertyOverridesPlugin implements Plugin {
@Override
void apply(Project project) {
- BomPropertyOverridesExtension extension = project.extensions.create(
+ def extension = project.extensions.create(
BomPropertyOverridesExtension.EXTENSION_NAME,
BomPropertyOverridesExtension,
project.objects
@@ -102,7 +102,7 @@ class BomPropertyOverridesPlugin implements Plugin {
* to all project configurations. Visible for testing.
*/
static void applyOverrides(Project project, BomPropertyOverridesExtension extension) {
- Set bomCoordinates = new LinkedHashSet<>()
+ def bomCoordinates = new LinkedHashSet()
if (extension.autoDetect.get()) {
bomCoordinates.addAll(detectDeclaredBoms(project))
@@ -118,7 +118,7 @@ class BomPropertyOverridesPlugin implements Plugin {
return
}
- BomManagedVersions managedVersions = BomManagedVersions.resolve(project, bomCoordinates)
+ def managedVersions = BomManagedVersions.resolve(project, bomCoordinates)
if (!managedVersions.hasOverrides()) {
return
}
@@ -134,7 +134,7 @@ class BomPropertyOverridesPlugin implements Plugin {
* Visible for testing.
*/
static Set detectDeclaredBoms(Project project) {
- Set coordinates = new LinkedHashSet<>()
+ def coordinates = new LinkedHashSet()
project.configurations.each { Configuration conf ->
for (Dependency dep : conf.dependencies) {
@@ -144,9 +144,9 @@ class BomPropertyOverridesPlugin implements Plugin {
if (!isPlatformDependency((ModuleDependency) dep)) {
continue
}
- String group = dep.group
- String name = dep.name
- String version = dep.version
+ def group = dep.group
+ def name = dep.name
+ def version = dep.version
if (group && name && version) {
coordinates.add("${group}:${name}:${version}" as String)
}
@@ -157,11 +157,11 @@ class BomPropertyOverridesPlugin implements Plugin {
}
private static boolean isPlatformDependency(ModuleDependency dep) {
- Object categoryAttr = dep.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)
+ def categoryAttr = dep.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)
if (categoryAttr == null) {
return false
}
- String category = categoryAttr.toString()
+ def category = categoryAttr.toString()
return category == Category.REGULAR_PLATFORM || category == Category.ENFORCED_PLATFORM
}
}
diff --git a/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomManagedVersionsSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomManagedVersionsSpec.groovy
similarity index 81%
rename from grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomManagedVersionsSpec.groovy
rename to grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomManagedVersionsSpec.groovy
index 9840c4b90d4..86489139c02 100644
--- a/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomManagedVersionsSpec.groovy
+++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomManagedVersionsSpec.groovy
@@ -35,9 +35,9 @@ class BomManagedVersionsSpec extends Specification {
def "parseBomFile extracts properties from BOM POM"() {
given:
- File pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
- Map bomProperties = [:]
- Map> propertyToArtifacts = [:]
+ def pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
+ def bomProperties = [:] as Map
+ def propertyToArtifacts = [:] as Map>
when:
BomManagedVersions.parseBomFile(pomFile, bomProperties, propertyToArtifacts)
@@ -50,9 +50,9 @@ class BomManagedVersionsSpec extends Specification {
def "parseBomFile maps property references to artifact coordinates"() {
given:
- File pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
- Map bomProperties = [:]
- Map> propertyToArtifacts = [:]
+ def pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
+ def bomProperties = [:] as Map
+ def propertyToArtifacts = [:] as Map>
when:
BomManagedVersions.parseBomFile(pomFile, bomProperties, propertyToArtifacts)
@@ -73,9 +73,9 @@ class BomManagedVersionsSpec extends Specification {
def "parseBomFile ignores dependencies with hardcoded versions"() {
given:
- File pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
- Map bomProperties = [:]
- Map> propertyToArtifacts = [:]
+ def pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI())
+ def bomProperties = [:] as Map
+ def propertyToArtifacts = [:] as Map>
when:
BomManagedVersions.parseBomFile(pomFile, bomProperties, propertyToArtifacts)
diff --git a/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginFunctionalSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginFunctionalSpec.groovy
similarity index 95%
rename from grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginFunctionalSpec.groovy
rename to grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginFunctionalSpec.groovy
index 7b9f1743bab..95a517b3a8d 100644
--- a/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginFunctionalSpec.groovy
+++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginFunctionalSpec.groovy
@@ -18,8 +18,10 @@
*/
package org.grails.gradle.plugin.bom
+import org.grails.gradle.plugin.core.GradleSpecification
+
/**
- * End-to-end functional test for the standalone
+ * End-to-end functional test for the
* {@code org.apache.grails.gradle.bom-property-overrides} plugin.
*
* Uses Gradle TestKit to apply the plugin in isolation (without any
diff --git a/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy
similarity index 78%
rename from grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy
rename to grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy
index c97f164c09f..2210a2e8349 100644
--- a/grails-gradle/bom-property-overrides/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy
+++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/bom/BomPropertyOverridesPluginSpec.groovy
@@ -37,14 +37,13 @@ class BomPropertyOverridesPluginSpec extends Specification {
def "applying the plugin registers the bomPropertyOverrides extension"() {
given:
- Project project = ProjectBuilder.builder().build()
+ def project = ProjectBuilder.builder().build()
when:
project.plugins.apply(BomPropertyOverridesPlugin)
then:
- BomPropertyOverridesExtension extension =
- project.extensions.findByType(BomPropertyOverridesExtension)
+ def extension = project.extensions.findByType(BomPropertyOverridesExtension)
extension != null
extension.autoDetect.get() == true
extension.boms.get().isEmpty()
@@ -52,10 +51,9 @@ class BomPropertyOverridesPluginSpec extends Specification {
def "extension bom() method registers explicit BOM coordinates"() {
given:
- Project project = ProjectBuilder.builder().build()
+ def project = ProjectBuilder.builder().build()
project.plugins.apply(BomPropertyOverridesPlugin)
- BomPropertyOverridesExtension extension =
- project.extensions.getByType(BomPropertyOverridesExtension)
+ def extension = project.extensions.getByType(BomPropertyOverridesExtension)
when:
extension.bom('org.example:my-bom:1.0.0')
@@ -67,10 +65,9 @@ class BomPropertyOverridesPluginSpec extends Specification {
def "extension boms() vararg method registers multiple BOMs"() {
given:
- Project project = ProjectBuilder.builder().build()
+ def project = ProjectBuilder.builder().build()
project.plugins.apply(BomPropertyOverridesPlugin)
- BomPropertyOverridesExtension extension =
- project.extensions.getByType(BomPropertyOverridesExtension)
+ def extension = project.extensions.getByType(BomPropertyOverridesExtension)
when:
extension.boms('org.example:a:1.0.0', 'org.example:b:2.0.0', 'org.example:c:3.0.0')
@@ -81,7 +78,7 @@ class BomPropertyOverridesPluginSpec extends Specification {
def "detectDeclaredBoms finds regular platform() dependencies"() {
given:
- Project project = ProjectBuilder.builder().build()
+ def project = ProjectBuilder.builder().build()
project.plugins.apply('java')
project.dependencies.add(
'implementation',
@@ -89,7 +86,7 @@ class BomPropertyOverridesPluginSpec extends Specification {
)
when:
- Set coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
+ def coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
then:
'org.example:test-bom:1.0.0' in coordinates
@@ -97,7 +94,7 @@ class BomPropertyOverridesPluginSpec extends Specification {
def "detectDeclaredBoms finds enforcedPlatform() dependencies"() {
given:
- Project project = ProjectBuilder.builder().build()
+ def project = ProjectBuilder.builder().build()
project.plugins.apply('java')
project.dependencies.add(
'implementation',
@@ -105,7 +102,7 @@ class BomPropertyOverridesPluginSpec extends Specification {
)
when:
- Set coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
+ def coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
then:
'org.example:enforced-bom:2.0.0' in coordinates
@@ -113,12 +110,12 @@ class BomPropertyOverridesPluginSpec extends Specification {
def "detectDeclaredBoms ignores non-platform dependencies"() {
given:
- Project project = ProjectBuilder.builder().build()
+ def project = ProjectBuilder.builder().build()
project.plugins.apply('java')
project.dependencies.add('implementation', 'org.example:regular-lib:1.0.0')
when:
- Set coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
+ def coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
then:
coordinates.isEmpty()
@@ -126,7 +123,7 @@ class BomPropertyOverridesPluginSpec extends Specification {
def "detectDeclaredBoms deduplicates the same BOM declared on multiple configurations"() {
given:
- Project project = ProjectBuilder.builder().build()
+ def project = ProjectBuilder.builder().build()
project.plugins.apply('java')
project.dependencies.add(
'implementation',
@@ -138,7 +135,7 @@ class BomPropertyOverridesPluginSpec extends Specification {
)
when:
- Set coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
+ def coordinates = BomPropertyOverridesPlugin.detectDeclaredBoms(project)
then:
coordinates == ['org.example:shared-bom:1.0.0'] as Set
diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy
index efd0b2ac69e..521610c5958 100644
--- a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy
+++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy
@@ -23,9 +23,9 @@ package org.grails.gradle.plugin.core
*
* Uses Gradle TestKit to verify that the Grails Gradle plugin correctly
* applies {@code grails-bom} as a Gradle {@code platform()} dependency,
- * applies the standalone
- * {@code org.apache.grails.gradle.bom-property-overrides} plugin, and no
- * longer depends on the Spring Dependency Management plugin.
+ * applies the {@code org.apache.grails.gradle.bom-property-overrides}
+ * plugin, and no longer depends on the Spring Dependency Management
+ * plugin.
*
* @since 8.0
* @see GrailsGradlePlugin#applyGrailsBom
diff --git a/grails-gradle/bom-property-overrides/src/test/resources/test-poms/test-bom.pom b/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom
similarity index 100%
rename from grails-gradle/bom-property-overrides/src/test/resources/test-poms/test-bom.pom
rename to grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom
diff --git a/grails-gradle/bom-property-overrides/src/test/resources/test-projects/bom-property-overrides-basic/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/bom-property-overrides-basic/build.gradle
similarity index 100%
rename from grails-gradle/bom-property-overrides/src/test/resources/test-projects/bom-property-overrides-basic/build.gradle
rename to grails-gradle/plugins/src/test/resources/test-projects/bom-property-overrides-basic/build.gradle
diff --git a/grails-gradle/bom-property-overrides/src/test/resources/test-projects/bom-property-overrides-basic/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/bom-property-overrides-basic/gradle.properties
similarity index 100%
rename from grails-gradle/bom-property-overrides/src/test/resources/test-projects/bom-property-overrides-basic/gradle.properties
rename to grails-gradle/plugins/src/test/resources/test-projects/bom-property-overrides-basic/gradle.properties
diff --git a/grails-gradle/bom-property-overrides/src/test/resources/test-projects/bom-property-overrides-basic/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/bom-property-overrides-basic/settings.gradle
similarity index 100%
rename from grails-gradle/bom-property-overrides/src/test/resources/test-projects/bom-property-overrides-basic/settings.gradle
rename to grails-gradle/plugins/src/test/resources/test-projects/bom-property-overrides-basic/settings.gradle
diff --git a/grails-gradle/settings.gradle b/grails-gradle/settings.gradle
index fc1a9e16dd3..406ae33b6b6 100644
--- a/grails-gradle/settings.gradle
+++ b/grails-gradle/settings.gradle
@@ -78,6 +78,3 @@ project(':grails-gradle-model').projectDir = file('model')
include 'grails-gradle-tasks'
project(':grails-gradle-tasks').projectDir = file('tasks')
-
-include 'grails-gradle-bom-property-overrides'
-project(':grails-gradle-bom-property-overrides').projectDir = file('bom-property-overrides')
From 931a54b54a56b94c0b7e4473dffcdd95bdd6d454 Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Thu, 21 May 2026 18:17:04 -0400
Subject: [PATCH 11/23] docs: document Spring DM removal in Grails 8 upgrade
guide
Add section 14 "Spring Dependency Management Plugin Replaced by Gradle
Platforms" to the upgrading-to-8.0.x guide, covering:
- The plugin is no longer applied; Grails Gradle Plugin uses native
platform(grails-bom) instead, and bom-property-overrides preserves
the gradle.properties / ext['...'] override workflow.
- No action required for most apps - existing override forms keep
working identically.
- The two Spring DM features with no automatic migration:
1. dependencyManagement { dependencies { dependency 'g:a:v' } }
DSL for arbitrary non-BOM pins - migrate to direct
dependencies { implementation } or resolutionStrategy.force.
2. Version-range strings inside BOM values - now
pass through to Gradle's native useVersion() rather than
Spring DM's resolver. Zero impact on the Grails / Spring
Boot BOM chain; only relevant for custom BOMs.
- Conflict-resolution behavioural difference: Spring DM forced BOM
versions unconditionally; Gradle platform() participates in
standard highest-version-wins resolution. enforcedPlatform() is
the migration path for "BOM wins always" semantics.
Sections 15-23 renumbered to make room. Two pre-existing heading-level
typos in former 21/22 (===== instead of ====, missing period after 22)
fixed in passing.
---
.../src/en/guide/upgrading/upgrading80x.adoc | 63 ++++++++++++++++---
1 file changed, 54 insertions(+), 9 deletions(-)
diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
index ef5dd4e1ede..3c8f4d03736 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@@ -465,7 +465,52 @@ Spring Security is moving toward the fluent `HttpSecurity` configuration API for
If your application references `DEFAULT_FILTER_ORDER` for custom filter positioning, replace it with the concrete value `-100` (computed as `OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100`).
Longer term, consider migrating to the fluent `HttpSecurity` API for filter chain configuration.
-==== 14. Spring Boot Starter Renames
+==== 14. Spring Dependency Management Plugin Replaced by Gradle Platforms
+
+Grails 8 standardizes on Gradle's native `platform()` dependency management and **no longer applies the `io.spring.dependency-management` plugin**.
+The Grails Gradle Plugin now adds `grails-bom` as a Gradle `platform()` on every declarable project configuration, and a new bundled plugin (`org.apache.grails.gradle.bom-property-overrides`, applied automatically by the Grails Gradle Plugin) preserves the property-based version-override workflow that Spring DM provided.
+
+**No action is required for most applications.**
+The standard `gradle.properties` and `ext['…']` override forms continue to work exactly as before:
+
+[source,groovy]
+.build.gradle
+----
+ext['slf4j.version'] = '2.0.9'
+----
+
+[source,properties]
+.gradle.properties
+----
+slf4j.version=2.0.9
+----
+
+The `grails { springDependencyManagement = … }` flag on `GrailsExtension` is now a deprecated no-op retained for source compatibility; setting it logs a deprecation warning.
+
+**Two Spring DM features have no automatic migration.**
+If your `build.gradle` uses either of the patterns below, update it as follows:
+
+[cols="2,3", options="header"]
+|===
+| Spring DM pattern (Grails 7)
+| Grails 8 replacement
+
+| `dependencyManagement { dependencies { dependency 'group:artifact:version' } }` to pin arbitrary non-BOM dependency versions
+| `dependencies { implementation 'group:artifact:version' }` for a direct dependency, or `configurations.all { resolutionStrategy.force 'group:artifact:version' }` to force a transitive version
+
+| Version-range strings (e.g. `1.+`, `[1.0,2.0)`) embedded in BOM `` values, resolved by Spring DM's own resolver
+| Now passed through to Gradle's native `useVersion()`, which uses Gradle's range semantics. Concrete versions are unaffected. Only relevant if you consume a custom BOM whose `` block contains range strings.
+|===
+
+**Behavioural difference to be aware of: conflict resolution.**
+Spring DM forced BOM-managed versions unconditionally, which could mask transitive version drift.
+Gradle's `platform()` participates in standard conflict resolution (highest-version-wins), so the BOM-pinned version is the version that wins *after* Gradle's conflict resolution.
+A `gradle.properties` override therefore pins the resolved version rather than overriding it after the fact.
+If you require Spring DM's "BOM wins always" behaviour for a given configuration, declare `enforcedPlatform("org.apache.grails:grails-bom:$grailsVersion")` instead of relying on the auto-applied non-enforced platform.
+
+NOTE: Projects that apply the `grails-micronaut` plugin already require `enforcedPlatform` for unrelated reasons (see section 7.2); those projects retain Spring DM's strict-version semantics automatically.
+
+==== 15. Spring Boot Starter Renames
Spring Boot 4 renamed several starters as part of its modularization effort.
If your `build.gradle` references any of the following, update the artifact id:
@@ -511,7 +556,7 @@ implementation 'org.springframework.boot:spring-boot-starter-aspectj'
NOTE: If you want a faster, lower-friction migration that pulls in *all* Spring Boot modules without picking starters individually, Spring Boot 4 ships `spring-boot-starter-classic` (and `spring-boot-starter-test-classic`) as a transitional fallback.
This is intended as a stopgap; new code should target the modular starters.
-==== 15. WAR Deployment Uses spring-boot-starter-tomcat-runtime
+==== 16. WAR Deployment Uses spring-boot-starter-tomcat-runtime
For WAR deployment to an external Servlet container, Spring Boot 4 introduced a dedicated `spring-boot-starter-tomcat-runtime` artifact that contributes only the integration classes needed to start under an external container, without bundling the embedded Tomcat distribution into the WAR.
@@ -532,7 +577,7 @@ providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat-runtime'
The Grails deployment guides have been updated to reflect this idiom.
Embedded Tomcat usage (the default for `grails run-app` and bootable WARs) continues to use `spring-boot-starter-tomcat` unchanged.
-==== 16. Test Infrastructure Changes
+==== 17. Test Infrastructure Changes
Spring Boot 4 made several breaking changes to its test infrastructure.
Most Grails tests use `GrailsUnitTest`, `GrailsWebUnitTest`, and `Integration` and are unaffected, but if your application uses `@SpringBootTest` directly the following apply.
@@ -588,7 +633,7 @@ If your test infrastructure registers it explicitly, remove the registration; Sp
Update the import.
A new `RestTestClient` and corresponding `@AutoConfigureRestTestClient` annotation are also available as a fluent alternative.
-==== 17. DevTools Live Reload Disabled by Default
+==== 18. DevTools Live Reload Disabled by Default
Spring Boot 4 changed the default of `spring.devtools.livereload.enabled` from `true` to `false`.
If you rely on browser live reload during development, opt in explicitly under the development environment so non-development environments still honor the new Spring Boot 4 default:
@@ -606,7 +651,7 @@ NOTE: Apps generated by the Grails Forge with the `spring-boot-devtools` reloadi
You can opt out by changing the value to `false` or by deleting the `application-development.yml` block after generation.
Existing Grails 7 applications that opt in to devtools must add this configuration manually.
-==== 18. Spring Retry No Longer Managed
+==== 19. Spring Retry No Longer Managed
Spring Boot 4 removed `spring-retry` from its managed dependencies.
Spring for Apache Kafka and Spring AMQP have moved off Spring Retry to Spring Framework's new retry support; if you used Spring Retry transitively through those projects you may not need it any more.
@@ -622,7 +667,7 @@ implementation 'org.springframework.retry:spring-retry'
NOTE: The `grails-bom` pins a known-good Spring Retry version (`2.0.x`), so you do not need to specify a version when using `enforcedPlatform("org.apache.grails:grails-bom:$grailsVersion")`.
Override the BOM-managed version in your build if you need a newer release.
-==== 19. Other Default Behavior Changes
+==== 20. Other Default Behavior Changes
Spring Boot 4 made several smaller default-behavior changes that you may notice but rarely require code changes:
@@ -651,7 +696,7 @@ If your application contributed `HttpMessageConverter` beans through this class,
Spring Framework 7 deprecated `org.springframework.lang.Nullable` and `org.springframework.lang.NonNull` in favor of the JSpecify annotations (`org.jspecify.annotations.Nullable`, `org.jspecify.annotations.NonNull`).
The Spring annotations still work, so this is non-blocking, but new code should prefer the JSpecify equivalents.
-==== 20. Known Plugin Incompatibilities
+==== 21. Known Plugin Incompatibilities
Some third-party plugins have not yet been updated for Spring Boot 4 / Spring Framework 7 compatibility.
The following are known blockers at this time:
@@ -664,7 +709,7 @@ Applications using `grails-sitemesh3` should remain on the `grails-layout` plugi
Check the https://github.com/apache/grails-core/issues[Grails issue tracker] for the latest status of plugin compatibility.
-===== 21. Custom JSON View Converters
+==== 22. Custom JSON View Converters
JSON views now use Groovy's `groovy.json.JsonGenerator` implementation instead of the previous Grails-specific
JSON generator infrastructure.
@@ -682,7 +727,7 @@ To migrate your custom converters:
`src/main/resources/META-INF/services/groovy.json.JsonGenerator$Converter`
(was previously `src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter`)
-===== 22 Rendering Enum values as JSON
+==== 23. Rendering Enum values as JSON
It is no longer possible to render a single enum value as JSON using the `render(Number.ONE as JSON)` syntax.
Previously, rendering an enum value would produce a JSON string with the type and name of the enum value like:
From 48de2b8d0d8485feb8d36795ad5bb618350986f1 Mon Sep 17 00:00:00 2001
From: James Fredley
Date: Thu, 21 May 2026 20:26:11 -0400
Subject: [PATCH 12/23] Address final 3 review threads + drop last Spring DM
coupling
Three concrete changes that together close out review feedback on
PR #15467 and remove every remaining reference to the
io.spring.gradle:dependency-management-plugin artifact from the entire
repository.
1. Item: grails { autoApplyBom = true } opt-out (jdaugherty thread AJjff)
GrailsExtension grows an autoApplyBom Property with a default
convention of true, matching the implicit Grails 7 behaviour where
Spring DM always applied the BOM. applyGrailsBom() is wrapped in
afterEvaluate so the user's build.gradle has a chance to set the flag
before the BOM gets applied. Setting `grails { autoApplyBom = false }`
now skips both the platform(grails-bom) injection AND the application
of the bom-property-overrides plugin, giving users a single clean
opt-out for the entire BOM automation.
New AutoApplyBomSpec (TestKit) verifies the opt-out path end-to-end.
2. Item: services-based BOM resolution (jdaugherty thread AJhsl on
afterEvaluate)
BomManagedVersions.resolve() and BomPropertyOverridesPlugin.applyOverrides()
are refactored to take captured Gradle services (ConfigurationContainer,
DependencyHandler, Function propertyLookup) instead of a
Project. This mirrors the captureProjectServices pattern already in use
by ExtractDependenciesTask. The afterEvaluate trigger is preserved
(it is the project's established pattern - 13 uses across
GrailsGradlePlugin alone, all addressing the same "late-binding to the
user's declarations" need), but the resolve path no longer holds a
Project reference, so the resulting BomManagedVersions instance + its
versionOverrides Map are pure data that survive
configuration-cache serialisation cleanly.
The original Project-accepting overloads on BomManagedVersions.resolve()
are retained as thin convenience wrappers for tests and ad-hoc usage.
Verified empirically with ./gradlew --configuration-cache: zero
CC warnings originate from this plugin or from BomManagedVersions.
3. Item: drop Spring DM from build-logic/docs-core (last consumer)
ExtractDependenciesTask was the only file in the entire repo still
importing io.spring.gradle.dependencymanagement.org.apache.maven.model.*
(a relic of the original Spring-DM-era BOM doc generation). The MavenXpp3Reader
+ Model usage is replaced with JDK DocumentBuilderFactory POM parsing,
following the same XXE-hardened pattern BomManagedVersions established
(setXIncludeAware(false), disallow-doctype-decl, external entities off).
A private ManagedDependency data class replaces the four shaded Maven
model fields the task actually used.
With ExtractDependenciesTask off Spring DM, the
implementation 'org.springframework.boot:spring-boot-gradle-plugin'
dependency in build-logic/docs-core/build.gradle is no longer required
(it was only there to drag Spring DM onto the build-logic classpath
via its transitive). Dropped. The build-logic Groovy source set now
has zero references to org.springframework.* outside one inert
javadoc-link URL in doc.properties.
Audit (verifying religious adherence to existing project patterns):
- afterEvaluate 13 uses elsewhere in GrailsGradlePlugin
- project.objects.property(...) 3 prior uses (BomPropertyOverridesExtension,
GrailsExtension.indy, GrailsExtension.preserveParameterNames)
- project.extensions.create() 2 prior uses
- configurations.detachedConfiguration 3 uses (BomManagedVersions,
ExtractDependenciesTask,
GrailsDependencyValidatorPlugin)
- JDK DocumentBuilderFactory POM parsing with XXE hardening
established by BomManagedVersions in
the same PR; now reused identically
in ExtractDependenciesTask
Doc: upgrading80x.adoc section 14 ("Spring Dependency Management Plugin
Replaced by Gradle Platforms") gets an "Opting out of the auto-applied
BOM" subsection with the build.gradle example.
Verification:
./gradlew :grails-gradle-plugins:build -> BUILD SUCCESSFUL,
31 tests pass
(was 30; +1 for AutoApplyBomSpec),
codenarc clean,
validatePlugins clean
./gradlew :grails-base-bom:extractConstraints
-> BUILD SUCCESSFUL, produces grails-bom-constraints.adoc
(184.9 KB, identical structure to pre-refactor output)
Spring DM imports across whole repo: ZERO
After this commit the repository has no compile-time, runtime, or
build-classpath dependency on io.spring.gradle:dependency-management-plugin
in any form.
---
.../ses_1b3c2d94dffeehtmCE00QwHQzS.json | 10 +
.../ses_1b96c228effeTZuclUToRUORl7.json | 10 +
.../ses_1ba57982dffetPNLKNXYPxd2Ep.json | 10 +
.../ses_1d408b4b1ffeywzBKStVd53F7m.json | 10 +
.../ses_1e8e4afb1ffeULcqOdvaKEZY54.json | 10 +
.../ses_1f72301eeffexh3D3I7MHLmoeG.json | 10 +
.../page-2026-05-08T21-50-43-518Z.yml | 1 +
.../page-2026-05-08T22-23-47-298Z.yml | 1 +
.../ses_1d408b4b1ffeywzBKStVd53F7m.json | 10 +
.../ses_1e8e4afb1ffeULcqOdvaKEZY54.json | 10 +
.../ses_1f72301eeffexh3D3I7MHLmoeG.json | 10 +
build-logic/docs-core/build.gradle | 1 -
.../tasks/bom/ExtractDependenciesTask.groovy | 272 +++++++++++++-----
cyclonedx-plugin-temp | 1 +
gradlew.lf | 248 ++++++++++++++++
.../src/en/guide/upgrading/upgrading80x.adoc | 15 +
grails-forge/gradlew.lf | 248 ++++++++++++++++
.../plugin/bom/BomManagedVersions.groovy | 102 +++++--
.../bom/BomPropertyOverridesPlugin.groovy | 50 +++-
.../gradle/plugin/core/GrailsExtension.groovy | 27 ++
.../plugin/core/GrailsGradlePlugin.groovy | 75 +++--
.../bom/BomPropertyOverridesPluginSpec.groovy | 8 +-
.../plugin/core/AutoApplyBomSpec.groovy | 48 ++++
.../auto-apply-bom-disabled/build.gradle | 23 ++
.../auto-apply-bom-disabled/gradle.properties | 1 +
.../grails-app/conf/application.yml | 2 +
.../auto-apply-bom-disabled/settings.gradle | 1 +
.../bom-property-overrides-basic/build.gradle | 2 +-
start-grails-after-fix.png | Bin 0 -> 34585 bytes
29 files changed, 1085 insertions(+), 131 deletions(-)
create mode 100644 .omo/run-continuation/ses_1b3c2d94dffeehtmCE00QwHQzS.json
create mode 100644 .omo/run-continuation/ses_1b96c228effeTZuclUToRUORl7.json
create mode 100644 .omo/run-continuation/ses_1ba57982dffetPNLKNXYPxd2Ep.json
create mode 100644 .omo/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json
create mode 100644 .omo/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json
create mode 100644 .omo/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json
create mode 100644 .playwright-mcp/page-2026-05-08T21-50-43-518Z.yml
create mode 100644 .playwright-mcp/page-2026-05-08T22-23-47-298Z.yml
create mode 100644 .sisyphus/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json
create mode 100644 .sisyphus/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json
create mode 100644 .sisyphus/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json
create mode 160000 cyclonedx-plugin-temp
create mode 100644 gradlew.lf
create mode 100644 grails-forge/gradlew.lf
create mode 100644 grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/AutoApplyBomSpec.groovy
create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/auto-apply-bom-disabled/build.gradle
create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/auto-apply-bom-disabled/gradle.properties
create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/auto-apply-bom-disabled/grails-app/conf/application.yml
create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/auto-apply-bom-disabled/settings.gradle
create mode 100644 start-grails-after-fix.png
diff --git a/.omo/run-continuation/ses_1b3c2d94dffeehtmCE00QwHQzS.json b/.omo/run-continuation/ses_1b3c2d94dffeehtmCE00QwHQzS.json
new file mode 100644
index 00000000000..91a8fd30453
--- /dev/null
+++ b/.omo/run-continuation/ses_1b3c2d94dffeehtmCE00QwHQzS.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1b3c2d94dffeehtmCE00QwHQzS",
+ "updatedAt": "2026-05-21T21:59:21.069Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-21T21:59:21.069Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.omo/run-continuation/ses_1b96c228effeTZuclUToRUORl7.json b/.omo/run-continuation/ses_1b96c228effeTZuclUToRUORl7.json
new file mode 100644
index 00000000000..f8464d004f5
--- /dev/null
+++ b/.omo/run-continuation/ses_1b96c228effeTZuclUToRUORl7.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1b96c228effeTZuclUToRUORl7",
+ "updatedAt": "2026-05-20T18:13:41.952Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-20T18:13:41.952Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.omo/run-continuation/ses_1ba57982dffetPNLKNXYPxd2Ep.json b/.omo/run-continuation/ses_1ba57982dffetPNLKNXYPxd2Ep.json
new file mode 100644
index 00000000000..e783e2c6ee8
--- /dev/null
+++ b/.omo/run-continuation/ses_1ba57982dffetPNLKNXYPxd2Ep.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1ba57982dffetPNLKNXYPxd2Ep",
+ "updatedAt": "2026-05-20T15:51:26.090Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-20T15:51:26.090Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.omo/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json b/.omo/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json
new file mode 100644
index 00000000000..d69f13b6422
--- /dev/null
+++ b/.omo/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1d408b4b1ffeywzBKStVd53F7m",
+ "updatedAt": "2026-05-15T15:27:15.637Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-15T15:27:15.637Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.omo/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json b/.omo/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json
new file mode 100644
index 00000000000..726500d26e6
--- /dev/null
+++ b/.omo/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1e8e4afb1ffeULcqOdvaKEZY54",
+ "updatedAt": "2026-05-11T13:01:45.805Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-11T13:01:45.805Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.omo/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json b/.omo/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json
new file mode 100644
index 00000000000..9fff71afe73
--- /dev/null
+++ b/.omo/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1f72301eeffexh3D3I7MHLmoeG",
+ "updatedAt": "2026-05-08T18:41:18.111Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-08T18:41:18.111Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-05-08T21-50-43-518Z.yml b/.playwright-mcp/page-2026-05-08T21-50-43-518Z.yml
new file mode 100644
index 00000000000..7703770e389
--- /dev/null
+++ b/.playwright-mcp/page-2026-05-08T21-50-43-518Z.yml
@@ -0,0 +1 @@
+- img [ref=e4]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-05-08T22-23-47-298Z.yml b/.playwright-mcp/page-2026-05-08T22-23-47-298Z.yml
new file mode 100644
index 00000000000..7703770e389
--- /dev/null
+++ b/.playwright-mcp/page-2026-05-08T22-23-47-298Z.yml
@@ -0,0 +1 @@
+- img [ref=e4]
\ No newline at end of file
diff --git a/.sisyphus/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json b/.sisyphus/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json
new file mode 100644
index 00000000000..d69f13b6422
--- /dev/null
+++ b/.sisyphus/run-continuation/ses_1d408b4b1ffeywzBKStVd53F7m.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1d408b4b1ffeywzBKStVd53F7m",
+ "updatedAt": "2026-05-15T15:27:15.637Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-15T15:27:15.637Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.sisyphus/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json b/.sisyphus/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json
new file mode 100644
index 00000000000..726500d26e6
--- /dev/null
+++ b/.sisyphus/run-continuation/ses_1e8e4afb1ffeULcqOdvaKEZY54.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1e8e4afb1ffeULcqOdvaKEZY54",
+ "updatedAt": "2026-05-11T13:01:45.805Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-11T13:01:45.805Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.sisyphus/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json b/.sisyphus/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json
new file mode 100644
index 00000000000..9fff71afe73
--- /dev/null
+++ b/.sisyphus/run-continuation/ses_1f72301eeffexh3D3I7MHLmoeG.json
@@ -0,0 +1,10 @@
+{
+ "sessionID": "ses_1f72301eeffexh3D3I7MHLmoeG",
+ "updatedAt": "2026-05-08T18:41:18.111Z",
+ "sources": {
+ "background-task": {
+ "state": "idle",
+ "updatedAt": "2026-05-08T18:41:18.111Z"
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/docs-core/build.gradle b/build-logic/docs-core/build.gradle
index 794cc42fc90..3b6b30c645b 100644
--- a/build-logic/docs-core/build.gradle
+++ b/build-logic/docs-core/build.gradle
@@ -48,7 +48,6 @@ dependencies {
api 'org.yaml:snakeyaml:2.4'
api "org.asciidoctor:asciidoctorj:${gradleBomDependencyVersions['asciidoctorj.version']}"
- implementation "org.springframework.boot:spring-boot-gradle-plugin:${gradleBomDependencyVersions['spring-boot.version']}"
testImplementation platform("org.spockframework:spock-bom:${gradleBomDependencyVersions['gradle-spock.version']}")
testImplementation('org.spockframework:spock-core') {
diff --git a/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy b/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy
index a7518a1c413..6f7eb354cee 100644
--- a/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy
+++ b/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy
@@ -20,9 +20,8 @@
package org.apache.grails.gradle.tasks.bom
import java.util.regex.Pattern
+import javax.xml.parsers.DocumentBuilderFactory
-import io.spring.gradle.dependencymanagement.org.apache.maven.model.Model
-import io.spring.gradle.dependencymanagement.org.apache.maven.model.io.xpp3.MavenXpp3Reader
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
@@ -48,6 +47,9 @@ import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+import org.w3c.dom.NodeList
/**
* Grails Bom files define their dependencies in a series of maps, this task takes those maps and generates an
@@ -259,91 +261,229 @@ abstract class ExtractDependenciesTask extends DefaultTask {
Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List exclusionRules, Map constraints, boolean error = true, int level = 0) {
Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom")
Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency)
+ dependencyConfiguration.transitive = false
File bomPomFile = dependencyConfiguration.singleFile
- MavenXpp3Reader reader = new MavenXpp3Reader()
- Model model = reader.read(new FileReader(bomPomFile))
-
+ Document doc = parsePom(bomPomFile)
Properties versionProperties = new Properties()
- if (model.parent) {
- // Need to populate the parent bom if it's present first
- CoordinateVersionHolder parentBom = new CoordinateVersionHolder(
- groupId: model.parent.groupId,
- artifactId: model.parent.artifactId,
- version: model.parent.version
- )
+
+ // Parent POM populated first so its properties can be overridden by the child
+ CoordinateVersionHolder parentBom = readParentCoordinates(doc)
+ if (parentBom) {
populatePlatformDependencies(parentBom, exclusionRules, constraints, false, level + 1)?.entrySet()?.each { Map.Entry