Inherit Spring Boot managed dependency versions in grails-bom#15730
Inherit Spring Boot managed dependency versions in grails-bom#15730jamesfredley wants to merge 7 commits into
Conversation
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
There was a problem hiding this comment.
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.gradlepins/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
GrailsDependencyVersionsto recurse into third-party imported BOMs with cycle/duplicate protection, per-POM property resolution, and “first-writer-wins” precedence. - Updated/expanded
grails-shell-clispecs 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.
|
We need to figure out if selenium 4.38.0 should still be behind Spring Boot. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
|
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
|
@codeconsole Good call - the spec no longer hard-codes the Spring Boot BOM version (or any other version). These specs drive a mocked I've reworked Pushed in 635df22. |
codeconsole
left a comment
There was a problem hiding this comment.
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, keyedgroup:artifact:version) correctly handlesspring-boot-dependenciesbeing imported by bothgrails-bomandgrails-base-bom— covered by the "resolved exactly once" spec. - The per-POM
localPropertieschange genuinely fixes the recursion property-clobbering bug and is more Maven-accurate (properties resolve in their declaring POM). - Removing the
jakarta.servlet/jakarta.validationexcludes is consistent with removing the matching direct pins — both now inherit6.1.0/3.1.1from Spring Boot. - Groovy
4.0.32precedence 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
|
Thanks for the thorough review @codeconsole. Pushed 1 - 2 - Silent-failure blast radius in 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 4 - Single-arg The change is comments plus the one new spec (no production behavior change). The |
|
@jdaugherty the extra stuff in here is the fix shell-cli, which is required when inheriting some of these versions from spring-boot-dependencies. |
|
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 |
✅ All tests passed ✅🏷️ Commit: 4b8bd8b 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() |
There was a problem hiding this comment.
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?
|
The provided context does not contain the file |
| addDependencyManagement(importedPom) | ||
| } | ||
| } catch (Exception e) { | ||
| // If the imported BOM cannot be resolved, skip it |
There was a problem hiding this comment.
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()] } |
There was a problem hiding this comment.
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?
What
Stop maintaining
grails-bomversion pins that duplicate versions already managed by thespring-boot-dependenciesBOM (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
Removed (each equal to Spring Boot 4.1.0's managed version)
byte-buddy(+byte-buddy-agent)commons-codeccommons-lang3jakarta.servlet-apijakarta.validation-apijunit/junit-jupiter(12junit-*entries +junit-bomplatform import)junit-bom:6.0.3mongodb(bson, driver-core, driver-sync, bson-record-codec)rxjava3Also removed the
jakarta.servlet/jakarta.validationexcludes fromgrails-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-java25.0 - retained as a deliberate drift tripwire (rationale comment added), becausegraphql-java-extended-scalarsis not managed by Spring Boot and must move in lockstep.groovy4.0.32 - intentional major divergence vs Spring Boot's 5.0.6 (Grails 8 baseline / Spock 2.4-groovy-4.0).selenium4.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-retry2.0.11 - Spring Boot 4 dropped management.Legacy CLI fix (required so this doesn't regress)
grails-shell-clibuilds its own dependency management by parsing the publishedgrails-bomPOM, and previously skipped third-party imports such asspring-boot-dependencies(there was a test enforcing this). After this cleanup those versions only exist behind the Spring Boot import, soGrailsDependencyVersionsnow follows third-party imported BOMs too, with:spring-boot-dependenciesis imported by bothgrails-bomandgrails-base-bom),${...}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
grails-bomandgrails-base-bomPOMs and diffed against baseline: only the removed constraints disappear; thespring-boot-dependenciesimport remains, so consumers still get the versions.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 validateDependencyVersionspasses across all modules../gradlew :grails-shell-cli:testpasses (including the newGrailsDependencyVersionsSpeccoverage).