Skip to content

feat: Replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides#15467

Open
jamesfredley wants to merge 45 commits into
8.0.xfrom
feat/gradle-managed-version-overrides
Open

feat: Replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides#15467
jamesfredley wants to merge 45 commits into
8.0.xfrom
feat/gradle-managed-version-overrides

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

@jamesfredley jamesfredley commented Feb 26, 2026

Summary

Grails 8 manages dependency versions with Gradle's native platform() support plus a small bundled Gradle plugin (org.apache.grails.gradle.bom-property-overrides) that adds the one capability Gradle platforms lack: reading <properties> from BOM POMs and letting projects override those versions. The io.spring.gradle:dependency-management-plugin (Spring DM) is not applied anywhere - in the framework, in generated/example apps, or on the build classpath.

What the Grails Gradle plugin does

GrailsGradlePlugin.applyGrailsBom() (runs in afterEvaluate):

  • Applies exactly one Grails BOM, selected by the grails.bom property (default grails-bom), as a Gradle platform() - or an enforcedPlatform() for the Micronaut variants - on every declarable project configuration, giving the BOM the same global reach Spring DM had.
  • Honors a BOM the build declares by hand. If a configuration already declares a Grails BOM as a platform()/enforcedPlatform() (e.g. a Micronaut app declaring enforcedPlatform(grails-micronaut-bom), or an app generated by Grails Forge / a profile), the plugin does not layer a second BOM on top of it; it fills the remaining declarable configurations with that same single BOM. Declaring more than one distinct Grails BOM fails the build.
  • Excludes code-quality and annotation-processor classpaths from the auto-applied platform: checkstyle, codenarc, pmd, spotbugs, spotbugsPlugins, annotationProcessor, and any *AnnotationProcessor. A non-enforced platform() participates in version conflict resolution, so adding a BOM to those tool classpaths would let Gradle upgrade transitives (e.g. javaparser-core; Micronaut processors import io.micronaut.platform:micronaut-platform there) and break the tools.
  • Ensures the developmentOnly configuration always exists (configurations.maybeCreate('developmentOnly')), even without Spring Boot.
  • Applies the org.apache.grails.gradle.bom-property-overrides plugin.

Selecting / opting out of the BOM

grails {
    bom = 'grails-micronaut-bom'  // pick a curated variant (applied as enforcedPlatform)
    // bom = null                 // opt out: manage Grails versions yourself
}

grails.bom is the BOM artifact name within org.apache.grails (default grails-bom); the plugin resolves it to org.apache.grails:$bom:$grailsVersion. Setting it to null (or blank) suppresses both the automatic BOM injection and the bom-property-overrides plugin.

The deprecated grails { springDependencyManagement = false } flag is still honored for backward compatibility: setting it to false is equivalent to bom = null. New builds should use bom.

Micronaut

validateMicronautBom() (via configureMicronaut()) requires a Micronaut BOM applied as enforcedPlatform when grails-micronaut is on the classpath. It accepts grails-micronaut-bom or grails-hibernate5-micronaut-bom. Because exactly one Grails BOM is applied, the Micronaut variant is the only BOM in play - the plugin no longer layers grails-bom alongside it. The build fails at configuration time with an actionable message otherwise.

The org.apache.grails.gradle.bom-property-overrides plugin

A standalone, BOM-agnostic Gradle plugin (bundled in grails-gradle-plugins, applicable to any project by id). It restores Maven-style <properties> version overrides on top of native platform():

  • Auto-detects every platform() / enforcedPlatform() declared on the project's configurations and registers each for override processing.

  • Optional DSL for explicit control:

    bomPropertyOverrides {
        autoDetect = false                  // default: true
        bom 'com.example:my-bom:1.0.0'
        bom 'com.example:other-bom:2.0.0'
    }

A Grails web project gets this automatically through the Grails Gradle plugin; no extra setup.

Override workflow

// build.gradle
ext['slf4j.version'] = '1.7.36'
# gradle.properties (name=value only; ext[...] syntax is not valid here)
slf4j.version=1.7.36

Either form overrides the BOM-managed version.

How overrides are resolved (BomManagedVersions)

The engine has a services-based API (no Project reference, so it is configuration-cache safe) and parses BOM POMs with Maven's own org.apache.maven:maven-model library (MavenXpp3Reader into a Model), the same way the docs ExtractDependenciesTask does. Parent-POM inheritance and <scope>import</scope> resolution follow Maven semantics (e.g. grails-bom imports spring-boot-dependencies), with <properties> scoped per BOM (and its parents) rather than leaking across unrelated imported BOMs, and property interpolation depth capped at 10.

Override detection uses a two-pass version diff: it computes the effective version of every managed artifact once with the BOM's default <properties> and once with the project's overrides applied. Any artifact whose version differs between the two passes becomes an override. This model:

  • Captures every managed entry (both ${property} references and hardcoded versions), not just property-backed ones.
  • Makes overriding a property that selects an imported BOM's version (e.g. spring-boot.version, which picks the spring-boot-dependencies BOM) re-import that BOM at the overridden version and pull in its entire updated managed set.
  • Gives a BOM's own direct <dependencyManagement> entries precedence over the entries it imports (matching Maven's importing-POM-wins precedence).
  • Produces no spurious overrides: versions that are identical with and without overrides are discarded.

Each detected override is applied as a strict dependency constraint on the project's declarable configurations.

Override semantics

  • An explicit override (ext['…'] / gradle.properties) is a strict constraint, so it wins over the require constraints platform(grails-bom) contributes - in both directions, including downgrades (e.g. pinning slf4j.version below the BOM default actually downgrades it). This matches Spring DM, where overrides always took effect. (A plain resolution rule would lose to Gradle's highest-version-wins conflict resolution on a downgrade.)
  • The BOM's default, non-overridden managed versions are a regular platform() and still participate in Gradle's highest-version-wins conflict resolution, so a transitive can pull a non-overridden version higher than the BOM default. Declare enforcedPlatform(grails-bom) to force the default versions to win unconditionally (Micronaut projects already do this).

Build-side (grails-bom generation and docs)

  • grails-bom (grails-bom/base/build.gradle + its pomCustomization) rewrites managed versions to ${property} references in the published POM and emits a matching <properties> block, so consumers can override them.
  • Both the runtime override engine (BomManagedVersions) and build-logic/docs-core/ExtractDependenciesTask now parse BOM POMs with Maven's maven-model library, which removes the last consumer of Spring DM's shaded Maven classes and lets spring-boot-gradle-plugin be dropped from build-logic/docs-core/build.gradle.

Migration notes (7.x -> 8.x)

End-user code consuming Grails via the standard grails-web plugin needs no changes: the BOM is still applied automatically and gradle.properties / ext['…'] overrides work as before (now via strict constraints). Section 14 of the upgrade guide covers the full migration.

Two Spring DM features have no automatic migration (not used inside grails-core, but external consumers might rely on them):

Spring DM feature Grails 8 replacement
dependencyManagement { dependencies { dependency 'g:a:v' } } for arbitrary non-BOM pins dependencies { implementation 'g:a:v' }, or configurations.all { resolutionStrategy.force 'g:a:v' } for a transitive
Version-range strings (1.+, [1.0,2.0)) inside BOM <properties> Applied as Gradle strict version constraints (Gradle range semantics); concrete versions are unaffected

Using the legacy Spring Dependency Management plugin

Spring DM is no longer applied automatically, but an application (Grails or plain Spring Boot) may still apply io.spring.dependency-management by hand and import grails-bom. Two example apps exercise this (see Testing below).

Testing

Functional tests use Gradle TestKit; BomOverrideResolutionFunctionalSpec runs a real resolution against a local Maven repo.

  • BomManagedVersionsSpec - POM parsing (maven-model), property extraction, property-to-artifact mapping, hardcoded-version handling.
  • BomPropertyOverridesPluginSpec / BomPropertyOverridesPluginFunctionalSpec - extension registration, DSL, platform()/enforcedPlatform() detection, auto-detect.
  • BomPlatformFunctionalSpec - (1) a Grails project applies grails-bom as a platform(), applies the overrides plugin, does not apply Spring DM; (2) a project that declares a Grails BOM by hand does not get a second BOM, and sibling configurations still receive the single effective BOM.
  • BomOptOutFunctionalSpec - grails { bom = null } suppresses both the platform injection and the overrides plugin.
  • GrailsExtensionSpec - bom default/selection/clear; springDependencyManagement = false maps to bom = null.
  • BomOverrideResolutionFunctionalSpec - real resolution proving a downgrade override wins over the platform constraint, and that overriding an imported-BOM selector property bumps a hardcoded transitively-managed version.
  • grails-test-examples/spring-dependency-management - a Grails application that opts out of the auto BOM (grails { bom = null }) and manages versions with the legacy io.spring.dependency-management plugin importing grails-bom; its integration test boots and serves a request.
  • grails-test-examples/gsp-spring-boot - a non-Grails Spring Boot application that renders Grails GSP and manages versions with io.spring.dependency-management importing grails-bom.

Review feedback (jdaugherty) addressed

The four blocking items from the latest review have been addressed:

  1. Only one Grails BOM is applied - GrailsExtension.autoApplyBom (boolean) is replaced by a Property<String> bom (the BOM artifact name; default grails-bom, null to opt out). The plugin applies a single BOM (enforcedPlatform for Micronaut variants), honors a hand-declared BOM instead of layering a second one, fills remaining configurations with that one BOM, and fails on more than one distinct Grails BOM. GrailsDependencyValidatorPlugin.detectBomPath now expects exactly one.
  2. Test coverage for the legacy Spring Dependency Management plugin - new grails-test-examples/spring-dependency-management Grails app.
  3. Spring Boot apps keep using the Spring plugin - the non-Grails grails-test-examples/gsp-spring-boot app is re-enabled and manages versions with Spring DM.
  4. Use maven-model for XML parsing - BomManagedVersions now parses POMs with org.apache.maven:maven-model (parent-POM recursion + per-BOM property scoping), matching ExtractDependenciesTask.

Related

…m and lightweight BOM property overrides

Replace the Spring Dependency Management Gradle plugin with Gradle's
native platform() support plus a lightweight BomManagedVersions utility
that preserves the ability to override BOM-managed dependency versions
via project properties (ext[] or gradle.properties).

This allows Grails to standardize on Gradle platforms - the modern
dependency management solution - while retaining the one feature
Gradle platforms lack: property-based version overrides from BOMs.

Changes:
- Add BomManagedVersions: parses BOM POM XML to extract property-to-
  artifact mappings, applies version overrides via eachDependency()
- Update GrailsGradlePlugin to use platform() + BomManagedVersions
  instead of Spring DM plugin
- Deprecate GrailsExtension.springDependencyManagement flag
- Remove Spring DM plugin from plugins/build.gradle dependency
- Remove Spring DM plugin from example projects
- Update documentation to reflect Gradle platform approach
- Add unit tests (BomManagedVersionsSpec) and functional test
  (BomPlatformFunctionalSpec)

Note: build-logic/docs-core/ExtractDependenciesTask still uses Spring
DM's shaded Maven model classes and should be addressed in a follow-up.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley self-assigned this Feb 26, 2026
@jamesfredley jamesfredley marked this pull request as ready for review February 26, 2026 16:30
Copilot AI review requested due to automatic review settings February 26, 2026 16:30
@jamesfredley jamesfredley added this to the grails:8.0.0-M1 milestone Feb 26, 2026
@jamesfredley jamesfredley added the relates-to:v8 Grails 8 label Feb 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the Spring Dependency Management plugin with Gradle's native platform() mechanism and introduces a lightweight utility (BomManagedVersions) to enable property-based version overrides from BOMs—the one feature Gradle platforms don't natively support.

Changes:

  • Removed Spring Dependency Management plugin dependency and usage across the codebase
  • Added BomManagedVersions utility (~350 lines) to parse BOM POMs and apply property-based version overrides
  • Updated plugin to use platform() for BOM import instead of Spring DM's mavenBom()

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy New utility class that parses BOM POMs and enables property-based version overrides via Gradle's resolution strategy
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy Replaced Spring DM plugin application with native Gradle platform support plus BomManagedVersions
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy Deprecated springDependencyManagement flag with guidance on new approach
grails-gradle/plugins/build.gradle Removed Spring DM plugin dependency
grails-test-examples/gsp-spring-boot/app/build.gradle Removed Spring DM plugin from example project
grails-data-graphql/examples/spring-boot-app/build.gradle Removed Spring DM plugin from example project
grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc Updated documentation to reflect platform-based approach and property override mechanism
grails-profiles/plugin/templates/grailsCentralPublishing.gradle Updated comment to reflect new version management approach
grails-bom/build.gradle Updated comment about version property references
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy Unit tests for BOM parsing and property extraction
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy Functional test verifying platform integration
grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/* Test project files for functional testing
grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom Test BOM POM for unit tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…h constant

Add factory.setXIncludeAware(false) for explicit XML security hardening
and extract magic number 10 to MAX_PROPERTY_INTERPOLATION_DEPTH constant.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley force-pushed the feat/gradle-managed-version-overrides branch 2 times, most recently from eb85805 to 5643538 Compare February 26, 2026 20:42
The Spring Dependency Management plugin applied version constraints
globally to every configuration via configurations.all() and
resolutionStrategy.eachDependency(). With the switch to Gradle's native
platform(), version constraints must be added explicitly.

Apply the grails-bom platform to all declarable configurations using
configureEach, matching the previous global behavior. Non-declarable
configurations (apiElements, runtimeElements, etc.) inherit constraints
through their parent configurations. Code quality tool configurations
(checkstyle, codenarc, etc.) are excluded because platform() constraints
participate in version conflict resolution and can upgrade transitive
dependencies, breaking the tools. Also ensure the developmentOnly
configuration always exists via maybeCreate.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley force-pushed the feat/gradle-managed-version-overrides branch from 5643538 to 5e89656 Compare February 26, 2026 20:59
@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

# Conflicts:
#	grails-data-graphql/examples/spring-boot-app/build.gradle
…atform

applyGrailsBom() adds a regular platform(grails-bom) to every declarable
configuration, including 'implementation'. Micronaut projects still need
to declare an enforcedPlatform(grails-bom) explicitly. With both present
on the same configuration, validateEnforcedBom() was failing on the first
grails-bom it encountered (the plugin-injected regular platform) before
it could check the user's enforcedPlatform declaration.

Scan all grails-bom declarations on 'implementation' and accept the
project as correctly configured if any one of them is an enforcedPlatform.
Only error when grails-bom is present but no enforcedPlatform exists.

Assisted-by: claude-code:claude-opus-4
…form

applyGrailsBom() adds a regular platform(grails-bom) to every declarable
configuration. For annotation-processor classpaths this conflicts with the
platform the user imports themselves (typically
io.micronaut.platform:micronaut-platform for Micronaut projects). Since
platform() constraints participate in Gradle version conflict resolution,
grails-bom's higher javaparser-core version wins over what Micronaut's
inject-java processor was compiled against, producing
NoSuchMethodError on StaticJavaParser.parseJavadoc(String) during
compileJava.

Exclude annotationProcessor and *AnnotationProcessor configurations using
the same mechanism that already excludes code-quality tool classpaths
(checkstyle, codenarc, pmd, spotbugs). Rename the helper to reflect the
broadened scope.

Assisted-by: claude-code:claude-opus-4
@jamesfredley
Copy link
Copy Markdown
Contributor Author

Final pass (commits 48de2b8 + 8829323) - ready for merge

Three concrete changes that close out the last of the review feedback and remove every remaining Spring Dependency Management coupling from the entire repo.

1. grails { autoApplyBom } opt-out (thread AJjff)

GrailsExtension gains an autoApplyBom Property<Boolean>. Default is true, which preserves the implicit Grails 7 behaviour (Spring DM always applied the BOM). Setting grails { autoApplyBom = false } in a project's build.gradle now suppresses both the automatic platform(grails-bom) injection AND the application of the bom-property-overrides plugin - one clean knob for the entire BOM automation.

applyGrailsBom() is wrapped in afterEvaluate so the flag is read after the user's build.gradle has finished evaluating. New AutoApplyBomSpec (TestKit) verifies the opt-out path end-to-end.

Documented in upgrade-guide section 14 with a build.gradle example.

2. Services-based BOM resolution (thread AJhsl)

BomManagedVersions.resolve() and BomPropertyOverridesPlugin.applyOverrides() now take captured Gradle services (ConfigurationContainer, DependencyHandler, Function<String, String> propertyLookup) instead of a Project. This matches the captureProjectServices pattern already used by ExtractDependenciesTask. The result is a BomManagedVersions instance holding only a Map<String, String> of overrides - pure data, no Project reference - which is consumed by per-configuration eachDependency closures that survive configuration-cache serialisation cleanly.

The afterEvaluate trigger is preserved because it is the project's established pattern (13 uses across GrailsGradlePlugin alone, all addressing the same late-binding-to-the-user's-declarations need). Empirical verification with ./gradlew --configuration-cache :grails-gradle-plugins:test shows zero CC warnings originating from this plugin or BomManagedVersions - the 7 CC problems reported are all in pre-existing Jar tasks and test-config.gradle, none from this PR.

Project-accepting convenience overloads on BomManagedVersions.resolve() are retained for tests and ad-hoc usage.

3. Drop Spring DM from build-logic/docs-core (the last consumer)

ExtractDependenciesTask was the only file in the entire repo still importing io.spring.gradle.dependencymanagement.org.apache.maven.model.* (Spring DM's shaded Maven model). 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.

After this commit the repository has zero compile-time, runtime, or build-classpath dependency on io.spring.gradle:dependency-management-plugin in any form.

Consistency audit (religious adherence to existing project patterns)

Verified each new pattern lines up with what the rest of the codebase already does:

Pattern Pre-existing uses Where we use it
project.afterEvaluate { ... } for late-binding 13 in GrailsGradlePlugin applyGrailsBom(), BomPropertyOverridesPlugin.apply()
project.objects.property(...).convention(...) for extension fields GrailsExtension.indy, GrailsExtension.preserveParameterNames, BomPropertyOverridesExtension.autoDetect/boms GrailsExtension.autoApplyBom
project.extensions.create(...) for plugin extensions 2 existing BomPropertyOverridesPlugin.apply()
configurations.detachedConfiguration(...) for ad-hoc POM resolution GrailsDependencyValidatorPlugin, ExtractDependenciesTask BomManagedVersions.resolvePomFile()
JDK DocumentBuilderFactory POM parsing with XXE hardening BomManagedVersions.parseXml() (this PR) ExtractDependenciesTask.parsePom() (same JDK pattern, same XXE flags)
captureProjectServices-style service capture ExtractDependenciesTask, TestPhasesGradlePlugin BomManagedVersions.resolve(ConfigurationContainer, DependencyHandler, ...)

Verification

./gradlew :grails-gradle-plugins:build
  -> BUILD SUCCESSFUL, 31 tests pass
     (was 30; +1 AutoApplyBomSpec)
     codenarc clean, validatePlugins clean

./gradlew :grails-base-bom:extractConstraints
  -> BUILD SUCCESSFUL
     grails-bom-constraints.adoc produced (184.9 KB)
     identical structure to pre-refactor output

./gradlew --configuration-cache :grails-gradle-plugins:test
  -> 0 CC warnings from BOM code
     (7 pre-existing warnings in Jar tasks + test-config.gradle, none from this PR)

git grep "io.spring.gradle.dependencymanagement"  -> 0 matches
git grep "spring-boot-gradle-plugin"             -> 0 matches outside this commit's removal

PR description rewritten to describe the final state and justify the custom code.

All 28 review threads are now resolved (15 in the original review pass + 13 in the three follow-up passes).

@jamesfredley jamesfredley changed the base branch from 8.0.x to fix/8.0.x-merge-sb4-fallout May 22, 2026 02:01
Base automatically changed from fix/8.0.x-merge-sb4-fallout to 8.0.x May 22, 2026 16:16
@jamesfredley jamesfredley requested a review from jdaugherty June 1, 2026 21:35
Resolve conflicts in GrailsGradlePlugin.groovy and upgrading80x.adoc:
- validateMicronautBom(): keep the validMicronautBoms set
  (grails-micronaut-bom, grails-hibernate5-micronaut-bom) introduced on
  8.0.x together with the PR's explanatory comment.
- Upgrade guide: renumber sections so the PR's new "Spring Dependency
  Management Plugin Replaced by Gradle Platforms" section and 8.0.x's new
  TagLib sections coexist as a sequential 1-25, fixing the duplicate
  section numbers and updating the cross-references to the renumbered
  TagLib parameter-name subsection.

Assisted-by: claude-code:claude-4.8-opus
Three correctness fixes to the new Gradle platform + BOM property-override
feature, identified during review of PR #15467:

1. Honor the deprecated `springDependencyManagement` opt-out. `applyGrailsBom()`
   only checked `autoApplyBom`, so `grails { springDependencyManagement = false }`
   (a documented Grails 7 opt-out still used by grails-extension-gradle-config.gradle
   across the build, and by external projects) had become a silent no-op that
   unexpectedly applied `platform(grails-bom)`. The deprecated setter now maps
   `false` onto `autoApplyBom = false`.

2. Apply property overrides as strict dependency constraints instead of
   `ResolutionStrategy.eachDependency().useVersion()`. A soft override loses to a
   platform's `require` constraint during conflict resolution, so a downgrade
   override was silently ignored. A strict constraint wins in both directions.

3. Honor imported-BOM property overrides. `BomManagedVersions` now computes every
   managed artifact's version twice (with BOM defaults and with project overrides,
   including imported-BOM selector versions) and records the difference as an
   override. Every managed entry is considered, not only `${property}` references,
   so overriding a property that selects an imported BOM (e.g. `spring-boot.version`)
   re-imports that BOM and picks up its updated managed set, including hardcoded
   versions. A BOM's own direct entries take precedence over imported ones.

Adds GrailsExtensionSpec and a BomOverrideResolutionFunctionalSpec backed by a
local-Maven-repo fixture that resolves real dependencies to prove the downgrade
override and the imported-BOM (hardcoded) cascade.

Assisted-by: claude-code:claude-4.8-opus
The override behaviour changed so that property overrides are applied as
strict dependency constraints (winning in both directions, including
downgrades) rather than soft resolution rules. Update the user-facing docs
that still described the old behaviour:

- gradleDependencies.adoc: overrides are strict constraints that win over the
  platform's require() constraints (including downgrades); only the BOM's
  default managed versions still participate in highest-version-wins conflict
  resolution; document imported-BOM (e.g. spring-boot.version) re-import.
- upgrading80x.adoc: correct the migration table (strict constraints, not
  useVersion()) and the conflict-resolution note to distinguish explicit
  overrides (strict, always win) from default BOM versions (conflict
  resolution; use enforcedPlatform to force).

Assisted-by: claude-code:claude-4.8-opus
@jamesfredley
Copy link
Copy Markdown
Contributor Author

Update: merged 8.0.x + three correctness fixes to the override engine

Brought the branch up to date with 8.0.x and fixed three real bugs in the property-override feature that a review surfaced. Flagging here because the behavior changes are significant. The PR description has been rewritten to describe current behavior in full.

be2c6af - merge 8.0.x

Resolved two conflicts:

  • GrailsGradlePlugin.validateMicronautBom() - kept the validMicronautBoms set (grails-micronaut-bom, grails-hibernate5-micronaut-bom) from 8.0.x alongside this PR's explanatory comment.
  • upgrading80x.adoc - renumbered the upgrade-guide sections back to a sequential 1-25 (the two branches had collided into duplicate section numbers) and fixed the § cross-references.

545b847 - fix: correct BOM property-override edge cases

1. The deprecated springDependencyManagement opt-out had become a silent no-op. applyGrailsBom() only checked autoApplyBom, so grails { springDependencyManagement = false } no longer suppressed the BOM. That flag is still set by gradle/grails-extension-gradle-config.gradle (applied across ~76 in-repo builds, with a comment warning that auto-applying the BOM "causes unexpected version mismatches in the various plugin projects") and is a documented Grails 7 opt-out external users may rely on. The deprecated setter now maps false -> autoApplyBom = false.

2. Downgrade overrides silently failed. Overrides were applied with eachDependency().useVersion(). Because platform() contributes require constraints that stay in the graph, Gradle's highest-version-wins conflict resolution ran before the override, so an override lower than the BOM default was silently ignored (you asked for slf4j 1.7.36, you got the BOM's 2.0.x, no error). Overrides are now applied as strict constraints, so they win in both directions - matching Spring DM, which always forced overrides.

3. Overriding an imported-BOM property did nothing. grails-bom imports spring-boot-dependencies via a ${spring-boot.version} property reference. The parser resolved imports using the BOM's default property values only, so overriding spring-boot.version never switched the imported Spring Boot set. BomManagedVersions was rewritten to a two-pass version-diff model: it computes every managed artifact's effective version with BOM defaults and again with project overrides applied (including to imported-BOM selector versions), and the difference becomes the override set. It now records all managed entries (literal versions included), so switching an imported BOM picks up its full managed set, including hardcoded versions; a BOM's direct entries take precedence over imported ones.

Added GrailsExtensionSpec and BomOverrideResolutionFunctionalSpec. The latter does a real resolution against a local Maven repo and asserts (a) a downgrade override wins over the platform constraint and (b) overriding an imported-BOM selector bumps a hardcoded transitively-managed version.

f1c76ac - docs: align with strict-constraint behavior

gradleDependencies.adoc and upgrading80x.adoc still described the old soft-override behavior (overrides lose to conflict resolution; use enforcedPlatform for stricter behavior). Updated them to state that explicit overrides are strict and always win (including downgrades), while only the BOM's default managed versions still participate in conflict resolution.

All affected :grails-gradle-plugins tests pass (19 BOM/extension tests, including the new real-resolution spec).

@jamesfredley jamesfredley requested a review from matrei June 2, 2026 02:16
Address @matrei's review comments (pullrequestreview-4407310454) on the
BOM property-overrides code. Style-only, no behavioral change:

- Use `def`/inferred types for local variables in new Groovy code
- Drop unnecessary terminal `return` statements in simple methods
- Use implicit `it` closure parameters where the type is statically inferred
- Simplify DocumentBuilderFactory and detached-configuration setup with `tap {}`
- Break an over-long lifecycle log statement
- Remove now-unused imports (DependencyConstraint, MutableVersionConstraint,
  Configuration)

Verified: grails-gradle-plugins and grails-docs-core compile clean.

Assisted-by: claude-code:claude-opus-4-8
@jamesfredley jamesfredley dismissed jdaugherty’s stale review June 2, 2026 14:43

Changes committed

Copy link
Copy Markdown
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly disagree with this change. We need to use the maven libraries to parse POMs and not roll our own solution. Otherwise, how do we know we have deviated from the maven implementation (especially with maven 4 around the corner)

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']}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand we're proposing to switch, don't we still need to offer this support to existing Grails apps? That means we should have a test app that uses it so we know we don't break anything.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we still have to apply the gradle plugin? Did you add a test app that uses the gradle plugin so we know historically if it will continue to work if manually applied? @jamesfredley

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 59dcb7e - grails-test-examples/spring-dependency-management, a Grails app that applies io.spring.dependency-management by hand and imports grails-bom. Its integration test boots the app and serves a request.


MavenXpp3Reader reader = new MavenXpp3Reader()
Model model = reader.read(new FileReader(bomPomFile))
def doc = parsePom(bomPomFile)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly disagree with this approach. We're now rolling our own xml parsing instead of using files that adhere to the maven standard. Libraries like plexus and others exist to parse this. We shouldn't be rolling our own. Especially because this is the dependencies task which is meant to resolve properties like maven would. There is no guarantee this resolution will mirror what upstream Maven does by not adopting the maven specific libraries.

Copy link
Copy Markdown
Contributor

@jdaugherty jdaugherty Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stopped reviewing after I saw this - we can't use custom xml parsing. We should be using upstream libraries to do this (it doesn't have to be Spring's). Maven actually publishes their maven-model library for this exact purpose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maven itself publishes maven-model for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 20f3304.

ExtractDependenciesTask now parses BOM POMs with Maven's own org.apache.maven:maven-model (MavenXpp3Reader -> Model) instead of the hand-rolled DocumentBuilderFactory parsing, so parent/properties/dependencyManagement and <scope>import</scope> resolution mirror upstream Maven rather than a bespoke approximation.

On versioning: Spring Boot's BOM does not manage org.apache.maven:maven-model (the spring-boot-dependencies POM only manages org.apache.maven.plugins:*), so there is no Spring-Boot-managed version to inherit. The version is pinned in dependencies.gradle's gradleBomDependencyVersions map alongside the other build-tooling versions, on the Maven 3.9.x line (3.9.16), which still ships org.apache.maven.model.io.xpp3.MavenXpp3Reader. It's deliberately kept out of gradleBomDependencies so it is not published as a managed constraint in the consumer-facing grails-bom.

Verified: :grails-docs-core:compileGroovy compiles, and :grails-bom:extractConstraints / :grails-base-bom:extractConstraints regenerate the constraint tables with property references (e.g. ${logback.version}) correctly resolved from the imported spring-boot-dependencies BOM.

jamesfredley added a commit that referenced this pull request Jun 2, 2026
The Micronaut platform ships org.ow2.asm 9.10.1, above the 9.9.1 inherited via
grails-base-bom, so five micronaut test-example modules used ext.allowedBomOverrides
to exempt the mismatch from the dependency-validator. That escape hatch defeats the
validator's purpose.

Pin org.ow2.asm:asm and asm-util at 9.10.1 in the micronaut BOM
(customBomVersions/customBomDependencies) so the resolved version equals the
BOM-managed version, and remove the per-app allowedBomOverrides from all five modules.
Also drop the redundant micronaut-specific groovy.version override; it now tracks the
main bom (still 5.0.7-SNAPSHOT via combinedVersions).

validateDependencyVersions passes for all five micronaut modules. See #15677 / #15467
for the upstream Micronaut platform / Gradle platform work that resolves the asm
conflict longer term.

Assisted-by: claude-code:claude-4.8-opus
The docs-core ExtractDependenciesTask parsed BOM POM files with a hand-rolled
JDK DocumentBuilderFactory parser. Switch to Maven's published
org.apache.maven:maven-model (MavenXpp3Reader -> Model) so property and
imported-BOM resolution mirror upstream Maven, as requested in PR review.

Spring Boot does not manage maven-model, so its version is pinned in
dependencies.gradle's gradleBomDependencyVersions map (Maven 3.9.x, which
still provides MavenXpp3Reader). It is intentionally not added to
gradleBomDependencies so it is not exposed as a managed constraint in the
consumer-facing grails-bom.

Assisted-by: claude-code:claude-4.8-opus
Copy link
Copy Markdown
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is out of date - we changed our assumption that only 1 grails-bom would be applied (the micronaut variant if using micronaut, etc).

* Scans the project's configurations to find which BOM project is in use.
*
* <p>When multiple known BOMs are declared on the same project (for example,
* the {@code grails-app} plugin auto-injects {@code platform(grails-bom)} on
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this is possible now with the current bom design - we should only be using one bom.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6ef4ec2. detectBomPath no longer prefers an enforcedPlatform among several - it expects exactly one Grails BOM and fails if it finds more than one distinct BOM.

Comment thread dependencies.gradle Outdated
* @param propertyToArtifacts output map to receive property name to artifact coordinate mappings
*/
static void parseBomFile(File pomFile, Map<String, String> bomProperties, Map<String, List<String>> propertyToArtifacts) {
def doc = parseXml(pomFile)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the maven model in this file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bc933fa. BomManagedVersions now parses POMs with org.apache.maven:maven-model (MavenXpp3Reader into a Model), matching ExtractDependenciesTask.

*
* <pre>
* grails {
* autoApplyBom = false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the boms are split betwen hibernate 5 & 7, I don't think we should do this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6ef4ec2. The hardcoded grails-bom assumption is gone - grails { bom = ... } selects the variant (e.g. grails-hibernate5-bom, grails-micronaut-bom), defaulting to grails-bom.


applyBomImport(dme, project)
def grailsVersion = (project.findProperty('grailsVersion') ?: BuildSettings.grailsVersion) as String
def bomCoordinates = "org.apache.grails:grails-bom:${grailsVersion}" as String
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't assume this. If we're really going to set the bom, we should change the boolean to the bom name and then default it. If it's null, we simply don't apply.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6ef4ec2 - replaced the autoApplyBom boolean with a Property<String> bom (the BOM artifact name, default grails-bom, null to opt out), exactly as suggested. The plugin resolves it to org.apache.grails:$bom:$grailsVersion.

// The Grails Gradle Plugin injects a regular platform(grails-bom) into each
// declarable configuration via applyGrailsBom(), excluding code-quality and
// annotation-processor classpaths (see isExcludedFromBomPlatform). For Micronaut
// projects the user must additionally declare an enforcedPlatform on a Micronaut BOM
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original design was to only have one bom applied, I believe this PR is out of date.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6ef4ec2 (hardened in 8cba523). Exactly one Grails BOM is applied now: the plugin applies a single platform()/enforcedPlatform(), skips injection on any configuration that already declares a Grails BOM by hand, and fails the build if more than one distinct Grails BOM is declared.

@jdaugherty
Copy link
Copy Markdown
Contributor

I'm a -1 on this until the following is addressed:

  1. we reworked the boms so we don't apply both of them. We only ever expect 1 grails bom to be applied. This PR still assumes multiple are applied.
  2. we need to have test coverage for the legacy spring gradle plugin
  3. we need to ensure the spring boot apps continue to use the spring plugin or we duplicate the apps and test both. We cannot assume that end user spring applications are going to adopt Grails specific gradle plugins
  4. we need to adopt the maven-model parsing everywhere we were parsing xml, not just our version extractor for documentation

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

❌ Patch coverage is 22.77992% with 200 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.9431%. Comparing base (4836003) to head (f63f028).

Files with missing lines Patch % Lines
...grails/gradle/plugin/bom/BomManagedVersions.groovy 14.8649% 112 Missing and 14 partials ⚠️
...rails/gradle/plugin/core/GrailsGradlePlugin.groovy 0.0000% 49 Missing ⚠️
...radle/plugin/bom/BomPropertyOverridesPlugin.groovy 42.8571% 20 Missing and 4 partials ⚠️
...le/plugin/bom/BomPropertyOverridesExtension.groovy 88.8889% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15467        +/-   ##
==================================================
- Coverage     49.0042%   48.9431%   -0.0611%     
- Complexity      16759      16785        +26     
==================================================
  Files            2014       2017         +3     
  Lines           94747      94994       +247     
  Branches        16547      16617        +70     
==================================================
+ Hits            46430      46493        +63     
- Misses          41019      41180       +161     
- Partials         7298       7321        +23     
Files with missing lines Coverage Δ
...g/grails/gradle/plugin/core/GrailsExtension.groovy 51.4286% <100.0000%> (+51.4286%) ⬆️
...le/plugin/bom/BomPropertyOverridesExtension.groovy 88.8889% <88.8889%> (ø)
...radle/plugin/bom/BomPropertyOverridesPlugin.groovy 42.8571% <42.8571%> (ø)
...rails/gradle/plugin/core/GrailsGradlePlugin.groovy 0.0000% <0.0000%> (ø)
...grails/gradle/plugin/bom/BomManagedVersions.groovy 14.8649% <14.8649%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Replace the hand-rolled JDK DocumentBuilderFactory parsing in
BomManagedVersions with Maven's own maven-model library (MavenXpp3Reader
into a Model), matching the docs ExtractDependenciesTask. Parent-POM
inheritance and imported-BOM resolution now follow Maven semantics, with
per-BOM property scoping, while preserving the two-pass default/effective
diff that detects property-based version overrides.

Add org.apache.maven:maven-model to the grails-gradle-plugins runtime
classpath (its only transitive is plexus-utils); the version is pinned in
dependencies.gradle and declared inline so it is not published as a managed
constraint in the consumer-facing grails-bom.

Assisted-by: claude-code:claude-4.8-opus
Replace the GrailsExtension.autoApplyBom boolean with a Property<String>
bom that names the Grails BOM artifact to apply (default grails-bom,
null or blank to opt out). The Grails Gradle plugin applies a single BOM as
a platform (or an enforcedPlatform for the Micronaut variants), honors a
BOM the build declares by hand instead of layering a second one, fills the
remaining declarable configurations with that same BOM, and fails fast when
more than one distinct Grails BOM is declared. The deprecated
springDependencyManagement = false flag maps onto bom = null.

GrailsDependencyValidatorPlugin.detectBomPath now expects exactly one
Grails BOM. Update the upgrade guide and the affected tests accordingly.

Assisted-by: claude-code:claude-4.8-opus
Grails 8 no longer applies io.spring.dependency-management automatically.
Add regression coverage that it still works when applied by hand:

- grails-test-examples/spring-dependency-management: a Grails application
  that opts out of the automatic BOM (grails { bom = null }) and manages
  versions with io.spring.dependency-management importing grails-bom.
- grails-test-examples/gsp-spring-boot: re-enable the non-Grails Spring
  Boot + GSP application and have it manage versions with the Spring
  Dependency Management plugin importing grails-bom.

Assisted-by: claude-code:claude-4.8-opus
Only treat a Grails BOM as hand-declared when it carries platform semantics
(a platform() / enforcedPlatform() declaration with the Category attribute),
not a plain dependency that imports no constraints. Also assert that a sibling
configuration which does not declare a BOM still receives the single effective
BOM via per-configuration injection.

Assisted-by: claude-code:claude-4.8-opus
@jamesfredley
Copy link
Copy Markdown
Contributor Author

@jdaugherty I've pushed changes addressing all four blocking items from your review. Commits:

Commit Item
bc933fa 4 - parse BOM POMs with maven-model
6ef4ec2 1 - apply exactly one Grails BOM (+ docs)
59dcb7e 2 & 3 - example apps for the Spring DM plugin
8cba523 follow-up hardening (platform-only BOM detection + sibling-config test)

1. Only one Grails BOM is ever applied. GrailsExtension.autoApplyBom (boolean) is replaced by Property<String> bom - the BOM artifact name within org.apache.grails, default grails-bom, null to opt out (per your suggestion to "change the boolean to the bom name and default it; if null, don't apply"). applyGrailsBom now:

  • applies a single BOM - a platform(), or an enforcedPlatform() for the Micronaut variants (grails-micronaut-bom, grails-hibernate5-micronaut-bom);
  • if the build already declares a Grails BOM by hand (a Micronaut app, or a Forge/profile-generated app), it does not layer a second one - it fills the remaining declarable configurations with that same BOM;
  • fails fast if more than one distinct Grails BOM is declared.

GrailsDependencyValidatorPlugin.detectBomPath now expects exactly one Grails BOM (no more "prefer enforced among multiple"). Deprecated springDependencyManagement = false maps to bom = null. This also covers the split hibernate5/7 and Micronaut BOMs - the variant is selected via grails { bom = '...' } rather than assuming grails-bom.

2. Legacy Spring Dependency Management plugin coverage (Grails app). New grails-test-examples/spring-dependency-management: a Grails app that opts out of the native platform (grails { bom = null }) and manages versions with io.spring.dependency-management importing grails-bom. Its @Integration test boots the app and serves a request.

3. Spring Boot apps keep using the Spring plugin. Re-enabled grails-test-examples/gsp-spring-boot - a non-Grails Spring Boot app that renders Grails GSP and manages versions with io.spring.dependency-management importing grails-bom. (Its runtime test is @Disabled for a pre-existing GSP-on-Spring-Boot-4 auto-config bean cycle, unrelated to dependency management - the same reason the example was previously disabled; the build-level interop is exercised by compiling it.)

4. maven-model everywhere. BomManagedVersions now parses POMs with org.apache.maven:maven-model (MavenXpp3Reader -> Model), with parent-POM inheritance and per-BOM property scoping, matching ExtractDependenciesTask. This also resolves your earlier concern that properties defined on parent BOMs would not be picked up.

Verified locally: :grails-gradle-plugins BOM/extension specs, validateDependencyVersions on app1 + micronaut, and codenarcMain all pass.

One small remaining nit from your review I did not touch: dependencies.gradle:41 ("Remove the comment") - happy to fold that in if you'd still like it removed.

The other entries in the gradleBomDependencyVersions map are uncommented; drop
the comment for consistency (review feedback).

Assisted-by: claude-code:claude-4.8-opus
@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented Jun 4, 2026

✅ All tests passed ✅

🏷️ Commit: f63f028
▶️ Tests: 880 executed
⚪️ Checks: 40/40 completed


Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Standardize on Gradle Platforms or Spring Dependency Management Gradle Plugin for grails-bom application

4 participants