Skip to content

Inherit Spring Boot managed dependency versions in grails-bom#15730

Open
jamesfredley wants to merge 7 commits into
8.0.xfrom
deps/grails-bom-inherit-spring-managed
Open

Inherit Spring Boot managed dependency versions in grails-bom#15730
jamesfredley wants to merge 7 commits into
8.0.xfrom
deps/grails-bom-inherit-spring-managed

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

What

Stop maintaining grails-bom version pins that duplicate versions already managed by the spring-boot-dependencies BOM (Spring Boot 4.1.0). Where our pin equals Spring Boot's managed version, the pin is dead weight: it only blocks free patch/security updates whenever Spring Boot bumps the dependency. Those pins are removed so the BOM inherits the version from Spring Boot.

Fixes #15674

Note: the issue was drafted against Spring Boot 4.0.6. This PR re-audits against the 4.1.0 BOM actually on 8.0.x, so the SAME/DIVERGENT buckets differ from the issue (e.g. commons-lang3 and byte-buddy have since converged with Spring Boot and are now removable).

Removed (each equal to Spring Boot 4.1.0's managed version)

Pin Version How it's still managed
byte-buddy (+ byte-buddy-agent) 1.18.10 spring-boot-dependencies
commons-codec 1.21.0 spring-boot-dependencies
commons-lang3 3.20.0 spring-boot-dependencies
jakarta.servlet-api 6.1.0 spring-boot-dependencies
jakarta.validation-api 3.1.1 spring-boot-dependencies
junit / junit-jupiter (12 junit-* entries + junit-bom platform import) 6.0.3 Spring Boot already imports junit-bom:6.0.3
mongodb (bson, driver-core, driver-sync, bson-record-codec) 5.8.0 spring-boot-dependencies
rxjava3 3.1.12 spring-boot-dependencies

Also removed the jakarta.servlet / jakarta.validation excludes from grails-bom/base/build.gradle - they only existed to win a since-resolved selenium-vs-Spring-Boot conflict, and both versions now match Spring Boot.

Kept intentionally

  • graphql-java 25.0 - retained as a deliberate drift tripwire (rationale comment added), because graphql-java-extended-scalars is not managed by Spring Boot and must move in lockstep.
  • groovy 4.0.32 - intentional major divergence vs Spring Boot's 5.0.6 (Grails 8 baseline / Spock 2.4-groovy-4.0).
  • selenium 4.38.0 - left pinned. It is now behind Spring Boot's 4.43.0, so dropping it would be a version bump (Geb/functional impact), not a pure inheritance. Worth a separate follow-up decision.
  • spring-retry 2.0.11 - Spring Boot 4 dropped management.

Legacy CLI fix (required so this doesn't regress)

grails-shell-cli builds its own dependency management by parsing the published grails-bom POM, and previously skipped third-party imports such as spring-boot-dependencies (there was a test enforcing this). After this cleanup those versions only exist behind the Spring Boot import, so GrailsDependencyVersions now follows third-party imported BOMs too, with:

  • cycle/duplicate protection (spring-boot-dependencies is imported by both grails-bom and grails-base-bom),
  • first-writer-wins precedence so Grails' own pinned versions (e.g. Groovy 4.0.32) still win over Spring Boot's, and
  • per-POM ${...} property resolution (fixes a latent property-table clobbering bug during recursion).

The spec test that asserted third-party BOMs are never resolved was inverted; precedence, cycle-protection and nested-import coverage were added.

Verification

  • Regenerated the grails-bom and grails-base-bom POMs and diffed against baseline: only the removed constraints disappear; the spring-boot-dependencies import remains, so consumers still get the versions.
  • Resolved compile/runtime/test classpaths of representative modules (grails-data-mongodb-core, grails-codecs-core, grails-async-rxjava3, grails-converters, grails-web-core): every removed dependency resolves to Spring Boot's identical version.
  • ./gradlew validateDependencyVersions passes across all modules.
  • ./gradlew :grails-shell-cli:test passes (including the new GrailsDependencyVersionsSpec coverage).

Remove version pins from the root dependencies.gradle that duplicate
versions already managed by the spring-boot-dependencies BOM (Spring Boot
4.1.0). Where our pin equals Spring Boot's managed version it is dead
weight that only blocks free patch and security updates when Spring Boot
bumps the dependency, so it is removed and inherited from Spring Boot.

Removed (each equal to Spring Boot 4.1.0's managed version):
- byte-buddy 1.18.10 (and byte-buddy-agent)
- commons-codec 1.21.0
- commons-lang3 3.20.0
- jakarta.servlet-api 6.1.0
- jakarta.validation-api 3.1.1
- junit / junit-jupiter 6.0.3 (the 12 junit-* entries plus the redundant
  junit-bom platform import; Spring Boot already imports junit-bom:6.0.3)
- mongodb 5.8.0 (bson, mongodb-driver-core, mongodb-driver-sync,
  bson-record-codec)
- rxjava3 3.1.12

Also drop the jakarta.servlet/jakarta.validation excludes from
grails-bom/base/build.gradle. They only existed to win a since-resolved
selenium-versus-Spring-Boot conflict; both versions now match Spring
Boot, so its platform can manage them.

Kept intentionally: graphql-java 25.0 is retained as a deliberate drift
tripwire (graphql-java-extended-scalars is not managed by Spring Boot and
must move in lockstep), with a rationale comment added; groovy 4.0.32 and
selenium 4.38.0 remain intentionally divergent; spring-retry stays
because Spring Boot 4 dropped its management.

The legacy grails-shell-cli derives its own dependency management by
parsing the published grails-bom POM and previously skipped third-party
imports such as spring-boot-dependencies. Teach GrailsDependencyVersions
to also follow third-party imported BOMs, with cycle protection and
first-writer-wins precedence so Grails' own pinned versions still win, so
the CLI keeps surfacing the versions now inherited from Spring Boot.

Verified by regenerating the grails-bom and grails-base-bom POMs (only
the removed constraints disappear; the Spring Boot import remains),
confirming every affected module resolves the removed dependencies to
Spring Boot's identical versions, and running validateDependencyVersions
across all modules.

Fixes #15674

Assisted-by: claude-code:claude-opus-4-8
Copilot AI review requested due to automatic review settings June 11, 2026 15:11

Copilot AI left a comment

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.

Pull request overview

This PR reduces redundant version pinning in grails-bom by removing constraints that exactly match Spring Boot 4.1.0’s managed versions, allowing Grails to automatically inherit future patch/security updates when Spring Boot bumps those dependencies. It also updates the legacy grails-shell-cli BOM parsing logic so that versions managed only behind third-party imported BOMs (notably spring-boot-dependencies) are still surfaced to the CLI.

Changes:

  • Removed dependencies.gradle pins/constraints that duplicate Spring Boot 4.1.0’s managed versions (e.g., byte-buddy, commons-codec/lang3, jakarta servlet/validation, junit, mongodb, rxjava3).
  • Updated GrailsDependencyVersions to recurse into third-party imported BOMs with cycle/duplicate protection, per-POM property resolution, and “first-writer-wins” precedence.
  • Updated/expanded grails-shell-cli specs to cover third-party BOM recursion, deduplication, and transitive BOM imports; removed no-longer-needed exclusions in the BOM Gradle build.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
grails-shell-cli/src/test/groovy/org/grails/cli/boot/GrailsDependencyVersionsSpec.groovy Updates tests to validate third-party BOM recursion, de-duplication, and transitive import handling.
grails-shell-cli/src/main/groovy/org/grails/cli/boot/GrailsDependencyVersions.groovy Implements recursive resolution of imported BOMs (including third-party) with precedence and cycle protection.
grails-bom/base/build.gradle Removes Jakarta excludes that were previously used for an older conflict and are now redundant.
dependencies.gradle Removes redundant BOM pins so versions inherit from Spring Boot 4.1.0; adds rationale comment for intentionally retained graphql-java “tripwire”.

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

@jamesfredley

Copy link
Copy Markdown
Contributor Author

We need to figure out if selenium 4.38.0 should still be behind Spring Boot.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.19048% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.4301%. Comparing base (7da662f) to head (6797b26).

Files with missing lines Patch % Lines
...rg/grails/cli/boot/GrailsDependencyVersions.groovy 76.1905% 1 Missing and 4 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15730        +/-   ##
==================================================
- Coverage     48.4322%   48.4301%   -0.0021%     
- Complexity      15265      15267         +2     
==================================================
  Files            1875       1875                
  Lines           85852      85866        +14     
  Branches        14972      14976         +4     
==================================================
+ Hits            41580      41585         +5     
- Misses          37863      37865         +2     
- Partials         6409       6416         +7     
Files with missing lines Coverage Δ
...rg/grails/cli/boot/GrailsDependencyVersions.groovy 61.1111% <76.1905%> (+2.4904%) ⬆️

... 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.

@codeconsole

codeconsole commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Doe the spec have to hard code the spring bom version or can it resolve whatever the current one used is? 4.1.x?

The BOM resolution specs hard-coded dependency version literals in their
synthetic POM fixtures and assertions. The mock GrapeEngine matches imported
BOMs by module name, not version, so those literals were arbitrary fixtures
that only looked like real releases - inviting needless updates whenever a
managed version (e.g. Spring Boot) changes.

Replace the literals with per-feature placeholder variables injected into the
fixtures via GStrings (escaping Maven property placeholders) and asserted
against the same variables. The specs now verify the parse/import-resolution
mechanism and precedence rather than any specific release.

Assisted-by: claude-code:claude-opus-4-8
@jamesfredley

Copy link
Copy Markdown
Contributor Author

@codeconsole Good call - the spec no longer hard-codes the Spring Boot BOM version (or any other version).

These specs drive a mocked GrapeEngine that matches imported BOMs by module name (and sometimes type), never by version, so every version literal in the fixtures (4.1.0, 5.8.0, 4.0.32, etc.) was just a synthetic placeholder that happened to look like a real release - not a value resolved from the real BOM. They were never asserting against actual managed versions, which is exactly why they'd be a maintenance burden to keep "current."

I've reworked GrailsDependencyVersionsSpec so each feature declares arbitrary placeholder version variables, injects them into the synthetic POMs via GStrings, and asserts against those same variables. The specs now verify the parse/import-resolution mechanism and precedence (e.g. a Grails-pinned Groovy still wins over the Spring Boot-managed one) rather than any specific release, so they stay green no matter how the real managed versions move (4.1.x and beyond).

Pushed in 635df22.

@codeconsole codeconsole self-requested a review June 17, 2026 00:00

@codeconsole codeconsole left a comment

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.

Reworking the spec to use placeholder versions instead of hard-coded releases is the right move. Mergeable as-is; the notes below are mostly hardening.

Thanks for the spec rework in 635df22 — that resolves my earlier question about hard-coded BOM versions.

Recommendations (prioritized)

1 — Highest value: missing test for versionProperties precedence (the create-app path).
CreateAppCommand reads profileDependencyVersions.versionProperties.get('groovy.version') (and gorm.version, grails-gradle-plugins.version) to stamp a newly created app's build. This PR changes getVersionProperties() from "replaced per POM" to an accumulated putIfAbsent merge that now also recurses into spring-boot-dependencies, which declares a conflicting groovy.version (5.0.x). It works today only because the root grails-bom declares groovy.version (4.0.32) in its own <properties> and is merged before the Spring Boot recursion runs — first-writer-wins protects it. But that invariant has no test coverage; every new spec asserts on find(...), none on the versionProperties map. If it ever broke, grails create-app would silently stamp Grails 8 apps with Groovy 5. Suggest a spec where both the Grails BOM and a recursed third-party BOM declare the same *.version property, asserting the Grails value wins in versions.versionProperties.

2 — Silent-failure blast radius in resolveImportedBom.
The empty catch (Exception e) { } ("skip it") predates this PR, but its impact is now much larger: Spring Boot's versions exist only behind the recursed import, so a silently-swallowed resolution failure drops all Spring Boot-managed versions at once, with no signal. Logging isn't idiomatic in this module, so this is a judgment call — at minimum worth a comment acknowledging the wider blast radius, or a debug log if a logger is acceptable here.

3 — Document the precedence-ordering assumption.
"Grails imports before third-party imports" only separates org.apache.grails from everything else. Among third-party imports (groovy-bom, selenium-bom, spock-bom, spring-boot-dependencies), precedence is just POM declaration order. It's safe today for the conflict-prone artifacts (Groovy, Spock) only because those are direct constraints in dependencies.gradle, so first-writer-wins pre-empts every import. A one-line comment on the resolution loop noting "direct constraints above protect us; ordering among third-party imports is otherwise undefined" would stop a future maintainer from moving a Groovy pin into a BOM import and silently regressing it.

4 — Minor: find(String artifactId) single-arg now resolves first-writer for ambiguous artifactIds.
io.reactivex.rxjava2:rxjava and io.reactivex.rxjava3:rxjava share artifactId rxjava; the new first-writer guard flips single-arg find('rxjava') from last- to first-writer. The two-arg find (used everywhere internally) is unaffected, so impact is near-zero — just flagging it.

5 — Nit: the !version.startsWith('${') guard is nearly dead.
After versionLookup, version is either resolved, a literal, or null. The meaningful check is the null one (skip unresolved-property deps); startsWith('${') only triggers on a malformed ${foo with no closing brace. Harmless; the comment could just say "skip deps whose version property didn't resolve."

Verified and fine

  • Cycle/duplicate protection (resolvedBoms, keyed group:artifact:version) correctly handles spring-boot-dependencies being imported by both grails-bom and grails-base-bom — covered by the "resolved exactly once" spec.
  • The per-POM localProperties change genuinely fixes the recursion property-clobbering bug and is more Maven-accurate (properties resolve in their declaring POM).
  • Removing the jakarta.servlet / jakarta.validation excludes is consistent with removing the matching direct pins — both now inherit 6.1.0 / 3.1.1 from Spring Boot.
  • Groovy 4.0.32 precedence holds because all groovy artifacts are direct constraints.

Open item the PR already flags

selenium is left pinned at 4.38.0, now behind Spring Boot's 4.43.0. Dropping it would be a version bump with Geb/functional impact rather than pure inheritance, so deferring to a separate follow-up is reasonable.

Address review feedback on PR #15730:

- Add a GrailsDependencyVersionsSpec feature asserting the Grails BOM's
  groovy.version wins over a recursed third-party (Spring Boot) BOM in the
  versionProperties map. That map feeds CreateAppCommand when it stamps a
  newly created app's build; without first-writer-wins, create-app could
  emit the wrong Groovy version. This path previously had no coverage.
- Document the now-wider silent-failure blast radius of the catch in
  resolveImportedBom: Spring Boot's managed versions exist only behind the
  recursed import, so a swallowed failure drops them all at once.
- Document that precedence among third-party imports is just POM declaration
  order and is otherwise undefined; conflict-prone artifacts stay correct only
  because they are declared as direct constraints.
- Clarify that the null check, not the startsWith('${') guard, is the
  meaningful condition when skipping unresolved version properties.

Assisted-by: claude-code:claude-opus-4-8
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @codeconsole. Pushed 791565333e addressing the hardening notes:

1 - versionProperties precedence (the create-app path). Added a GrailsDependencyVersionsSpec feature that wires a Grails BOM declaring groovy.version and importing a Spring Boot BOM that declares a conflicting groovy.version, then asserts versionProperties['groovy.version'] resolves to the Grails value (first-writer-wins). This is exactly the invariant CreateAppCommand relies on when stamping a new app, and it now has direct coverage so a regression would fail the build instead of silently emitting the wrong Groovy.

2 - Silent-failure blast radius in resolveImportedBom. Expanded the catch comment to call out that the swallowed-failure impact is now much wider than when only Grails BOMs were followed - Spring Boot's managed versions exist only behind the recursed import, so one silent failure drops them all. Kept it as a comment since logging isn't idiomatic in this module, as you noted.

3 - Precedence-ordering assumption. Added a comment on the resolution loop noting that ordering among third-party imports is just POM declaration order and otherwise undefined, and that conflict-prone artifacts (Groovy, Spock) stay correct only because they're direct constraints that first-writer-wins pre-empts - with a warning not to move such a pin into a BOM import without restoring an equivalent direct constraint.

5 - Near-dead startsWith('${') guard. Clarified the comment to say the null check is the meaningful guard (versionLookup returns null for an unresolved ${...} reference); startsWith('${') only catches a malformed reference with no closing brace.

4 - Single-arg find(String) first-writer flip. Left as-is - as flagged, the two-arg find used internally is unaffected and the impact is near-zero, so no behavior change seemed warranted.

The change is comments plus the one new spec (no production behavior change). ./gradlew :grails-shell-cli:test --tests "org.grails.cli.boot.GrailsDependencyVersionsSpec" is green (8/8, including the new feature).

The selenium pin remains deferred as a separate follow-up, per the open item already noted on this PR.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

@jdaugherty the extra stuff in here is the fix shell-cli, which is required when inheriting some of these versions from spring-boot-dependencies.

@codeconsole

Copy link
Copy Markdown
Contributor

1 New spec: a Grails BOM declaring groovy.version + importing a Spring Boot BOM that declares a conflicting groovy.version, asserting versionProperties['groovy.version'] stays the Grails value - verified

2 Expanded the catch comment: Spring Boot's versions exist only behind the recursed import, so one swallowed failure drops them all; logging left out as non-idiomatic - verified

3 Added a comment on the resolution loop: third-party import order is undefined; Groovy/Spock stay correct only as direct constraints — with a "don't move a pin into a BOM import" warning - verified

4 two-arg find unaffected, near-zero impact - verified

5 null check is the meaningful guard - verified

@testlens-app

testlens-app Bot commented Jun 17, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 4b8bd8b
▶️ Tests: 11559 executed
⚪️ Checks: 44/44 completed


Learn more about TestLens at testlens.app.

String artifactId = dep.artifactId.text()
String version = versionLookup(dep.version.text())
String version = versionLookup(dep.version.text(), localProperties)
String scope = dep.scope.text()

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.

This is a lot of logic for resolving what should be recursive. It was claimed this was to fix the shell cli. Can you please elaborate how this was broken?

@bito-code-review

Copy link
Copy Markdown

The provided context does not contain the file GrailsDependencyVersions.groovy or any diff hunks related to the shell CLI logic you are inquiring about. As a result, I cannot elaborate on how the shell CLI was broken or why the current implementation was chosen.

addDependencyManagement(importedPom)
}
} catch (Exception e) {
// If the imported BOM cannot be resolved, skip it

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.

Does this actually occur in the build? I assume this exception should fail the build


@CompileDynamic
void addDependencyManagement(GPathResult pom) {
versionProperties = pom.properties.'*'.collectEntries { [(it.name()): it.text()] }

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.

Isn't the core issue that we are trying to manual extract the pom with xml parsing? The maven model solves all of this, no?

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants