Skip to content

Groovy 5.0.x support for Grails 8 + Spring Boot 4#15557

Draft
jamesfredley wants to merge 110 commits into
fix/8.0.x-merge-sb4-falloutfrom
grails8-groovy5-sb4
Draft

Groovy 5.0.x support for Grails 8 + Spring Boot 4#15557
jamesfredley wants to merge 110 commits into
fix/8.0.x-merge-sb4-falloutfrom
grails8-groovy5-sb4

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

@jamesfredley jamesfredley commented Apr 5, 2026

Status

Layered on 8.0.x (with the upgrade/gradle-9.3.1 work merged in: Gradle 9.4.1, Micronaut 4.10.10, Spring Boot 4.0.5, Spring 7.0.6). Locally verified end-to-end against Apache Groovy 5.0.7-SNAPSHOT (off GROOVY_5_0_X HEAD eca67326e) on JDK 21, including the -PgrailsIndy=false matrix that exposes Groovy 5 trait/interface bytecode bugs. Last audited 2026-05-21.

The PR tracks 5.0.7-SNAPSHOT from the GROOVY_5_0_X branch so post-GROOVY_5_0_6 fixes are picked up as they land. The 4 substantive commits between the GROOVY_5_0_6 tag (released 2026-05-04) and GROOVY_5_0_X HEAD are: GROOVY-11989 (javaparser bump), GROOVY-11990 (jackson bump), a metadata update, and GROOVY-11996 (the groovy.truth.file.exists.enabled=false opt-out flag cross-referenced under workaround #1; the PR's real-fix rewrites do not depend on the flag). None of these commits eliminate any of the 6 remaining workarounds below. The Apache snapshots repository declaration in settings.gradle already includes org[.]apache[.]groovy.* so 5.0.7-SNAPSHOT resolves from https://repository.apache.org/content/groups/snapshots/ with no settings.gradle change; the same declaration lets the groovy-joint-workflow CI job swap in upstream Groovy snapshots when needed.

Target stack

Component Version
Apache Groovy 5.0.7-SNAPSHOT (off GROOVY_5_0_X HEAD eca67326e)
Spock 2.4-groovy-5.0
Spring Boot 4.0.5
Spring Framework 7.0.6
Gradle 9.4.1
Micronaut 4.10.10 (used by Forge)
Jakarta EE 10 (jakarta.servlet, jakarta.validation, jakarta.inject, ...)
JDK 21+

Remaining workarounds

Cross-referenced against every GROOVY-* ticket fixed in 5.0.6 and every commit on GROOVY_5_0_X HEAD (eca67326e, the snapshot this PR consumes). Each item below has been re-verified failing on 5.0.7-SNAPSHOT with the workaround removed.

# Site Real bug Reproducer Upstream status
1 TemplateRendererImpl.render(Map) (in grails-core and grails-shell-cli), TemplateRendererImpl.render(CharSequence/File/Resource, File, Map, boolean) (in both modules), and GenerateControllerCommand.generateFile defence-in-depth DefaultGroovyMethods.asBoolean(File) on Groovy 5+ returns file.exists() && (isDirectory() OR length>0). The previous if (template && destination) guards silently evaluated false for a not-yet-generated destination File and silently no-opped. Fix is containsKey() / explicit == null checks (per @paulk-asert's upstream confirmation). The typed positional templateRenderer.render(Resource, File, Map, boolean) shape in GenerateControllerCommand is kept as defence-in-depth, not as a workaround for a compiler bug. TemplateRendererImpl.groovy (reproducer is misdiagnosed; see Paul's comment) Documented Groovy semantics change. GROOVY-11996 ships a groovy.truth.file.exists.enabled=false system property that reverts to Groovy 4 behaviour, shipped in 5.0.7-SNAPSHOT (not in the 5.0.6 release). The real-fix rewrites in this PR do not depend on the flag.
2 GrailsASTUtils.java (processVariableScopes), AstUtils.groovy (canonicalisation guard), AbstractMethodDecoratingTransformation.groovy (canonicalisation guard + non-null VariableScope on ClosureExpression) and ResourceTransform.groovy non-null VariableScope guard on ClosureExpression Groovy 5 VariableScopeVisitor NPEs during canonicalisation on certain Grails AST transformation outputs. Reverting locally breaks :grails-datamapping-tck:compileGroovy with BUG! exception in phase 'canonicalization'. Main.groovy (isolates the ClosureWriter NPE half - the canonicalisation NPE remained shape-dependent on Grails-specific transforms) Not yet filed
3 gradle/boot4-disabled-integration-test-config.gradle apply on 5 grails-test-examples projects (app1, app3, exploded, mongodb/test-data-service, plugins/exploded) Controller action methods that declare parameters lose parameter scope under indy=false: parameter resolves to a propertyMissing lookup on the controller (via TagLibraryInvoker$Trait$Helper.propertyMissing) instead of the local parameter, after ControllerActionTransformer.wrapMethodBodyWithExceptionHandling wraps the body in a try/catch. Functional Tests (Java 21, indy=true) PASS for the same projects. Re-verified failing on 5.0.7-SNAPSHOT. Main.groovy (compiles a Subject twice, indy=true and indy=false, with the same try/catch wrap; only indy=false on Groovy 5 falls through to propertyMissing) Not yet filed
4 ConfigurationBuilder Map exclusion ordering + Object.class fallback (AbstractConstraint static init) @Builder(builderStrategy = SimpleStrategy) not recognised under Spring 6/7 + Groovy 5; interface static initialisation order regression in Groovy 5. MySettings.groovy (diagnostic only - shows @Builder is @Retention(SOURCE) upstream, so Class.getAnnotation(Builder) returns null on every Groovy version; the full Spring binding failure path is out of scope) Not yet filed
5 g.taglib(...) from @CompileStatic GSP class fails type checking - @IgnoreIf({ instance.isGroovy5OrLater() }) on affected GspCompileStaticSpec cases Regression of GROOVY-6362 / GROOVY-11817 - the g taglib namespace is no longer resolved by the type-check extension on 5.0.7-SNAPSHOT. NamespaceExtension.groovy (TypeCheckingDSL extension stores a PropertyExpression in unresolvedProperty and matches by node identity in methodNotFound; identity is no longer preserved on Groovy 5) Not yet filed
6 Validateable.resolveDefaultNullable() Method.invoke reflection bypass TraitReceiverTransformer rewrites this.defaultNullable() to a static helper call, silently losing the implementing-class override. Workaround uses reflection to keep dynamic dispatch. Validateable.groovy GROOVY-11985 (OPEN); root cause is the GROOVY-8854 (Sep 2023) TraitReceiverTransformer change.

Real bug fixes (not workarounds)

These changes fix latent bugs that surfaced because of the upgrade but are not Groovy-version-conditional:

  • File.asBoolean silent-no-op in TemplateRendererImpl - rewrote the render(Map) body in grails-core (325e2fee08) and grails-shell-cli (faef56cfe2); rewrote the typed render(CharSequence/File/Resource, File, Map, boolean) overloads in grails-shell-cli to use explicit == null checks instead of Groovy truthiness (43ad57a296). The previous if (template && destination) guards silently no-opped because DefaultGroovyMethods.asBoolean(File) returns file.exists() && (isDirectory() OR length>0) for a yet-to-be-generated destination File. Fix per @paulk-asert's upstream confirmation.
  • numberOfPessimisticUpdates typo in MongoCodecSession (4040590fd6).

Forge / generated-app coverage

The Forge generator produces consumer apps in grails-forge/test-core/src/test/groovy/.... Tests verify all generated apps:

  • Build (Groovy 5 + JDK 21+ default).
  • Pass runCommand round-trips for generate-controller, generate-service, generate-domain-class, generate-views, generate-interceptor, generate-taglib.
  • Pass functional tests against the generated app's GORM, GSP, Hibernate5, MongoDB, async, and security layers.
  • Resolve dependencies via the right repository chain - mavenLocal() for 8.0.0-SNAPSHOT, Maven Central / the Apache release repo for released artifacts, and the Apache snapshots repo for any in-flight org.apache.groovy.*-SNAPSHOT consumed by the groovy-joint-workflow job.

In addition, grails-test-examples/compile-static (cherry-picked from #15294) exercises GORM dynamic finders inside @GrailsCompileStatic services (Book.findAllByName('Joe')) - the GROOVY-11817 happy path - confirming that case works on 5.0.7-SNAPSHOT without the reflection workaround that item #6 still needs for the trait-static-method-override path.


Reviewer notes

  • The bomDependencyVersions['groovy.version'] vs gradleBomDependencyVersions['gradle-groovy.version'] distinction is load-bearing. The grails-gradle subprojects must stay on Groovy 4 to remain compatible with Gradle's embedded runtime, while the Grails BOM and main artifacts use Groovy 5.
  • Each remaining Groovy 5 workaround above has an inline // Groovy 5 ... or // GROOVY-XXXXX ... comment that points at the actual upstream bug.
  • The two new Java files in grails-views-gson (StreamingJsonBuilder.java, JsonGenerator.java, DefaultJsonGenerator.java) are deprecation shims so compiled .gson template AST output resolves to the Grails delegate type instead of Groovy 5's package-private groovy.json.StreamingJsonDelegate. Cleanup direction (per @jdaugherty review): fix JsonViewWritableScript.groovy to FQN-qualify groovy.json.StreamingJsonBuilder and stop synthesising the Grails inner-delegate alias - then the shims can be deleted again. Tracked as a follow-up in an open review thread.
  • The update_release_draft job runs release-drafter against the PR base. With base = 8.0.x it works as expected; the workflow is continue-on-error: true and does not block the PR.

Open review threads (follow-up commits owed)

  • JsonViewTemplateResolverSpec @IgnoreIf - need to wire mock-maker-inline on the test runtime classpath (or rewrite against MockHttpServletRequest).
  • GspCompileStaticSpec g.message @IgnoreIf - file new Groovy ticket against GROOVY_5_0_X referencing GROOVY-6362 / GROOVY-11817 with a standalone reproducer; re-enable the tests when the fix lands.
  • UrlMappingTagLib linkTagAttrs.clone() -> new LinkedHashMap(...) - file an upstream Groovy ticket with a standalone reproducer for the Map.clone() STC dispatch tightening.
  • RestfulServiceController Math.toIntExact(...) - add inline comment explaining the load-bearing Number -> Integer narrowing rejection under Groovy 5 STC.
  • Customer @GrailsCompileStatic removed - re-test restoring the annotation against 5.0.7-SNAPSHOT now that GROOVY-11907 / GROOVY-11968 are fixed; restore if the static-mapping closure VerifyError no longer fires.
  • DataBindingTests GroovySpy(Author, global: true) - drop global: true so the per-method scope auto-cleans, or add an explicit cleanup: block.
  • DefaultJsonGenerator.java / StreamingJsonBuilder.java / JsonGenerator.java shims - update JsonViewWritableScript.groovy to FQN-qualify groovy.json.StreamingJsonBuilder and remove the shims.
  • TraitPropertyAccessStrategy boolean-getter fallback - either delete the fallback if it has no triggering callers in current GORM tests, or rewrite the surrounding code so the JavaBean-conventions intent is self-evident without a comment.
  • File a Groovy ticket for the render(Map) reproducer's actual root cause (File.asBoolean changed semantics; not explicitly listed in the 5.0 release notes per Paul) so the breaking change is recorded against a JIRA. GROOVY-11996 is the opt-out flag, not the change-log entry.

matrei and others added 30 commits May 15, 2025 10:51
# Conflicts:
#	build.gradle
#	dependencies.gradle
#	grails-forge/build.gradle
#	grails-gradle/build.gradle
# Conflicts:
#	buildSrc/build.gradle
#	dependencies.gradle
#	grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy
#	grails-gradle/buildSrc/build.gradle
# Conflicts:
#	dependencies.gradle
#	gradle/test-config.gradle
#	grails-forge/settings.gradle
#	settings.gradle
# Conflicts:
#	gradle.properties
#	grails-core/src/test/groovy/org/grails/plugins/BinaryPluginSpec.groovy
Cherry-picked comprehensive Groovy 5 compat from 9574fe8.

Conflict resolutions:
- dependencies.gradle: Groovy 5.0.5 GA (not SNAPSHOT) + Jackson 2.21.2
- LoggingTransformer: Keep manual log field injection (avoids Groovy 5 VariableScopeVisitor NPE entirely)
- TransactionalTransformSpec: Remove direct Spock feature method invocation (Groovy 5/Spock 2.x incompatible)
- grails-test-core/build.gradle: Remove spock-core transitive=false, keep junit-platform-suite
- grails-test-suite-uber/build.gradle: Remove spock-core transitive=false and explicit byte-buddy
…root cause

Paul King ([@paulk-asert](jamesfredley/groovy5-compiledynamic-trait-bug#1))
confirmed upstream that the "@CompileStatic render(Map) silent no-op" diagnosis
in this PR's reproducer is wrong: the call site is *not* the trigger. Under
Groovy 5+ the silent no-op is caused by DefaultGroovyMethods.asBoolean(File)
returning file.exists() && (file.isDirectory() || file.length() > 0). For a
not-yet-generated destination File, the truthy guard
`if (template && destination)` silently evaluates to false. The fix is
containsKey() / explicit null checks inside render(Map), not the call shape.

Two cleanups follow from this:

1. grails-shell-cli/TemplateRenderer + TemplateRendererImpl

   - Drop @CompileDynamic on render(Map) (interface and impl). The body
     is now @CompileStatic-clean.
   - Replace the truthiness guard `namedArguments?.template && namedArguments?.destination`
     with containsKey() + null checks (per Paul's recommendation).
   - Use Map.get() and explicit Resource/File coercion instead of dynamic
     property access, mirroring the grails-core counterpart fixed in
     325e2fe.

2. grails-core/TemplateRendererImpl + grails-scaffolding/GenerateControllerCommand

   - Rewrite the inline comments to point at the File.asBoolean root cause
     and link to the upstream confirmation issue. The previous comments
     framed the typed-positional bypass as the *only* call shape that
     survives, which the reproducer originally claimed and which Paul has
     since refuted. The typed positional shape is kept as defence-in-depth,
     not as a workaround for a Groovy compiler bug.

Verified locally against Groovy 5.0.6-SNAPSHOT build #26:

  ./gradlew :grails-shell-cli:test :grails-scaffolding:test :grails-core:compileGroovy

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

Audit pass against 5.0.6-SNAPSHOT build #26 (2026-05-08)

Re-audited every remaining workaround against the latest snapshot now that Apache Groovy 5.0.6 is officially released (2026-05-04, Maven Central). The snapshot version has not been bumped to 5.0.7-SNAPSHOT yet; current 5.0.6-SNAPSHOT resolves to build #26 which is 4 commits ahead of the GROOVY_5_0_6 release tag (post-release dependency bumps + GROOVY-11996 groovy.truth.file.exists.enabled system property). All 5.0.6 release contents are present in the snapshot we resolve.

Pushed in this audit pass

  • 65d194f - Restore IContainerGebConfiguration as interface with default methods. GROOVY-11982 was backported to GROOVY_5_0_X as a15a4389 on 2026-05-02 and shipped in 5.0.6. Compile verified locally under both -PgrailsIndy=true and -PgrailsIndy=false for :grails-geb:compileTestFixturesGroovy and :grails-test-examples-geb:compileIntegrationTestGroovy. The previous trait-fallback workaround comment also pointed at GROOVY-11968 (already fixed and reverted in 74da807).

  • faef56c - Realign render(Map) workaround diagnoses with @paulk-asert's upstream confirmation that the silent no-op is File.asBoolean semantics changing in Groovy 5, not a @CompileStatic overload-resolution bug:

    • Drop @CompileDynamic from grails-shell-cli's TemplateRenderer.render(Map) and TemplateRendererImpl.render(Map).
    • Replace if (namedArguments?.template && namedArguments?.destination) with containsKey() + null checks (per Paul's recommendation), mirroring the fix that was already in grails-core's TemplateRendererImpl from 325e2fe.
    • Rewrite the inline comments in grails-core/TemplateRendererImpl and grails-scaffolding/GenerateControllerCommand to point at the actual root cause (File.asBoolean = exists() && (isDirectory() OR length>0)) instead of the misdiagnosed "render(Map) silent no-op at the call site". The typed-positional templateRenderer.render(Resource, File, Map, boolean) shape stays as defence-in-depth, not as a workaround for a compiler bug.

Remaining-workaround inventory (5 items, was 7)

The PR description body has been refreshed in full. Quick diff:

  • Removed: IContainerGebConfiguration interface->trait (GROOVY-11982 fixed)
  • Reframed as not a Groovy-version-conditional workaround: TemplateRendererImpl.render(Map) truthy-check (now an explicit null-check fix; root cause is documented Groovy 5 semantics change with GROOVY-11996 opt-out)
  • Reclassified as defensive code, not workaround: TraitPropertyAccessStrategy boolean-getter fallback. The inline // See GROOVY-11512 comment was misleading; that ticket was closed and fixed in 5.0.0-alpha-11 / 4.0.24 (2024-11-05) - long before this PR. Listed under "Defensive code" in the description.
  • Still active and listed: VariableScopeVisitor canonicalisation guard (4 sites), boot4-disabled integrationTest on 5 test apps, ConfigurationBuilder Map exclusion + Object.class fallback, g.taglib @IgnoreIf (regression of GROOVY-6362 / GROOVY-11817).

Recently fixed in 5.0.6 release (already removed from PR earlier)

Ticket Status Drop commit
GROOVY-11907 Fixed in 5.0.6 (earlier in PR)
GROOVY-11968 Fixed in 5.0.6 74da807
GROOVY-11983 Fixed in 5.0.6 73bd63c
GROOVY-11982 Fixed in 5.0.6 65d194f (this pass)

Local verification

JAVA_HOME=/.../corretto-21 ./gradlew :grails-geb:compileTestFixturesGroovy -PgrailsIndy=false --rerun-tasks   # PASS
JAVA_HOME=/.../corretto-21 ./gradlew :grails-test-examples-geb:compileIntegrationTestGroovy -PgrailsIndy=false --rerun-tasks   # PASS
JAVA_HOME=/.../corretto-21 ./gradlew :grails-geb:test :grails-shell-cli:test :grails-scaffolding:test :grails-core:compileGroovy   # PASS

CI is now running against the pushed commits; @jdaugherty / @paulk-asert flagging this pass for visibility on the remaining items, particularly g.taglib (GROOVY-6362 / GROOVY-11817 regression - want to file a fresh ticket against GROOVY_5_0_X) and the VariableScopeVisitor NPE path which still needs a Groovy-only reduction.

…ale JIRA reference

Two cleanups against Groovy 5.0.6-SNAPSHOT build #26 (latest GROOVY_5_0_X HEAD,
4 commits ahead of the GROOVY_5_0_6 release tag).

1. grails-shell-cli/TemplateRendererImpl - the typed File/Resource overloads
   were still using the Groovy-truthiness pattern that the Map overload was
   rewritten away from in faef56c:

     render(CharSequence, File, Map, boolean) line 115
     render(File, File, Map, boolean) line 150
     render(Resource, File, Map, boolean) line 193

   All three had  if (template && destination)  guards. Under Groovy 5+
   DefaultGroovyMethods.asBoolean(File) returns
     file.exists() && (file.isDirectory() || file.length() > 0)
   so a non-existent destination File silently no-ops the render. Replaced
   with explicit  if (template == null || destination == null) return  guards
   and flattened the nested  if/else  pyramid with early returns. Behaviour
   is preserved for the null-destination case (silent return) but no longer
   collides with File truthiness.

   This brings grails-shell-cli's TemplateRendererImpl in line with
   grails-core's TemplateRendererImpl which already used explicit null checks
   from 325e2fe.

2. grails-data-hibernate5/TraitPropertyAccessStrategy - dropped the
   misleading  // See https://issues.apache.org/jira/browse/GROOVY-11512
   comment. GROOVY-11512 was closed and fixed in 5.0.0-alpha-11 / 4.0.24
   (2024-11-05), long before this PR. The boolean-getter fallback
   (findMethod(getGetterName(name, true))) is plain JavaBean-conventions
   defence for boolean trait properties and is not Groovy-version
   conditional. Removing the JIRA reference avoids implying this code has
   any pending upstream dependency.

Final-pass cross-reference of all 20 GROOVY-* tickets shipped in 5.0.6
against every remaining workaround in this PR - the librarian and explore
audit confirms no other workaround maps to a fixed ticket. The five
remaining workarounds are kept (none have an upstream fix on
GROOVY_5_0_X HEAD):

  - VariableScopeVisitor canonicalisation NPE (4 sites, no JIRA filed)
  - boot4-disabled integrationTest on 5 test apps (controller action
    parameter scope under indy=false; no JIRA filed)
  - ConfigurationBuilder Map exclusion + AbstractConstraint static-init
    fallback (Spring 6/7 + Groovy 5 binding interaction; no JIRA filed)
  - g.taglib  @IgnoreIf  in GspCompileStaticSpec (regression of
    GROOVY-6362 / GROOVY-11817; no follow-up JIRA filed)
  - Validateable.resolveDefaultNullable() reflection bypass
    (GROOVY-11985 OPEN; TraitReceiverTransformer change from GROOVY-8854)

Verified locally:

  ./gradlew :grails-shell-cli:test :grails-data-hibernate5:classes
  -> BUILD SUCCESSFUL

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

Final audit pass + 8.0.x merge (2026-05-08)

Merged latest 8.0.x (b47917c1fe, "Fix forge dockerBuildNative class-initialization conflicts" and the 8.0.0-M1 release/merge-back) into grails8-groovy5-sb4, then ran a final audit using parallel librarian (Groovy JIRA + GROOVY_5_0_X commit log) and explore (workaround code-path + silent-workaround sweep) sub-agents.

Verdict per remaining workaround

# Workaround Verdict Evidence
1 render(Map) File-truthiness Keep (Groovy semantics change, not a bug) GROOVY-11996 fix-version is 5.0.7, not 5.0.6. The system property is an opt-out, not a revert.
2 VariableScopeVisitor canonicalisation NPE (4 sites) Keep No matching JIRA on GROOVY_5_0_X. :grails-datamapping-tck:compileGroovy still fails with BUG! exception in phase 'canonicalization' on build #26.
3 boot4-disabled-integration-test-config (5 test apps) Keep MissingPropertyException for action method parameters under -PgrailsIndy=false still reproduces on build #26. No JIRA.
4 ConfigurationBuilder + AbstractConstraint Keep Spring 6/7 + Groovy 5 binding interaction; no upstream JIRA.
5 g.taglib @IgnoreIf (GspCompileStaticSpec, JsonViewTemplateResolverSpec) Keep Regression of GROOVY-6362 / GROOVY-11817; no follow-up JIRA filed yet.
6 Validateable.resolveDefaultNullable() reflection bypass Keep GROOVY-11985 is OPEN. Root cause is the GROOVY-8854 (Sep 2023) TraitReceiverTransformer change.

Cross-checked all 20 GROOVY-* tickets shipped in 5.0.6 and the 4 post-release commits on GROOVY_5_0_X HEAD - none of them match any remaining Grails workaround. Latest 5.0.6-SNAPSHOT resolves to a1c006c974 (GROOVY-11996, 2026-05-06).

Pushed in this final pass

  • a0b74286ed - Merge origin/8.0.x (15 commits, includes 8.0.0-M1 release/merge-back and the forge dockerBuildNative class-init fix).
  • 43ad57a296 - Two follow-on cleanups the audit surfaced:
    • Silent workaround in grails-shell-cli/TemplateRendererImpl: the render(CharSequence, File, Map, boolean) (line 115), render(File, File, Map, boolean) (line 150), and render(Resource, File, Map, boolean) (line 193) overloads were still using if (template && destination) Groovy truthiness, hitting the same File.asBoolean = exists() && (isDirectory() OR length>0) trap that we fixed in render(Map) already. Rewrote all three to explicit == null checks with early returns, mirroring the grails-core counterparts.
    • Stale JIRA reference in grails-data-hibernate5/TraitPropertyAccessStrategy: dropped the misleading // See https://issues.apache.org/jira/browse/GROOVY-11512 comment. GROOVY-11512 was closed and fixed in 5.0.0-alpha-11 / 4.0.24 (2024-11-05), long before this PR. The boolean-getter fallback is plain JavaBean-conventions defence, not Groovy-version-conditional.

What is not changing in this pass

  • GenerateControllerCommand.generateFile typed-positional bypass stays as defence-in-depth. The render(Map) callee is fixed, but scaffolding silent-no-op is bad UX so the explicit-overload bypass is cheap insurance.
  • TraitPropertyAccessStrategy boolean-getter fallback stays as benign JavaBean defence (just dropped the stale JIRA comment).

Local verification

JAVA_HOME=/.../corretto-21 ./gradlew :grails-shell-cli:test :grails-data-hibernate5:classes
JAVA_HOME=/.../corretto-21 ./gradlew :grails-geb:compileTestFixturesGroovy -PgrailsIndy=false --rerun-tasks
JAVA_HOME=/.../corretto-21 ./gradlew :grails-test-examples-geb:compileIntegrationTestGroovy -PgrailsIndy=false --rerun-tasks

All PASS. PR description has been refreshed in full and CI is now running on the merged + final-pass HEAD 43ad57a296.

@jdaugherty / @paulk-asert - this is the burn-down endpoint locally. The 6 items in the table above are everything that still survives full audit against 5.0.6-SNAPSHOT build #26.

The 5.0.6-SNAPSHOT we resolve from the Apache snapshots repo currently points
at GROOVY_5_0_X HEAD, which is 4 commits ahead of the GROOVY_5_0_6 release tag.
One of those post-release commits is GROOVY-11989 ("Bump
com.github.javaparser:javaparser-core: 3.28.0 -> 3.28.1", da06ae61, 2026-05-04).

The transitive resolution from the Groovy 5.0.6-SNAPSHOT BOM upgraded
javaparser-core to 3.28.1, which in turn made every downstream :validateDependencyVersions
task fail with:

    Dependency version validation failed for project 'grails-async-gpars'.
    The following dependencies resolved to versions different from the BOM (:grails-bom):
      com.github.javaparser:javaparser-core - resolved 3.28.1, expected 3.28.0
    A transitive dependency is upgrading these versions.

Bumping the Gradle-side BOM-managed version to 3.28.1 brings the BOM in line
with the resolved transitive version. When 5.0.7-SNAPSHOT becomes available
this will continue to be correct (5.0.7 release will include GROOVY-11989).

Assisted-by: claude-code:claude-opus-4-7
jamesfredley added a commit that referenced this pull request May 8, 2026
…ck-test.xml

The Groovy joint validation build ("CI - Groovy Joint Validation Build")
has been failing on the 8.0.x branch since 2026-05-07 with:

    GroovyChangeLogSpec > updates a database with Groovy Change FAILED
        Condition not satisfied:
        output.toString().contains('confirmation message')

The captured  output  has the standard Liquibase UI messages
('Running Changeset', 'UPDATE SUMMARY', 'Liquibase: Update has been
successful') but is missing per-changeset log lines that go through
SLF4J / Logback (e.g. the confirmation message emitted from
ChangeSet.execute() via  log.info(change.getConfirmationMessage()) ).

Root cause: the previous test logger config was a Groovy-DSL Logback
config:

    appender('STDOUT', ConsoleAppender) {
        withJansi = true
        encoder(PatternLayoutEncoder) {
            pattern = '...%highlight(%p)%cyan(...)...%n'
        }
    }

This relies on (a) the Groovy runtime being on the test JVM classpath
at Logback init time so Logback's GroovyConfigurator can compile and
evaluate the script, (b) Jansi for ANSI colour, and (c) the
%highlight / %cyan converters. In the joint validation environment
the freshly-built local Groovy snapshot (GROOVY_5_0_X HEAD) interacts
with Logback's GroovyConfigurator in a way that silently fails to
register the 'liquibase' logger -> STDOUT binding, so log.info() lines
go nowhere and the assertion fails.

Replaces  src/test/resources/logback.groovy  with an equivalent
 logback-test.xml  that has no Groovy / Jansi / color-converter
dependencies. Same logger levels and appender wiring, just XML.

Verified:

    ./gradlew :grails-data-hibernate5-dbmigration:test \
        --tests 'org.grails.plugins.databasemigration.liquibase.GroovyChangeLogSpec' \
        -PmaxTestParallel=3 --rerun-tasks
    BUILD SUCCESSFUL in 1m 21s (7 tests, 7 successes, 0 failures, 0 skipped)

Surfaced while auditing PR #15557 (Groovy 5 / Spring Boot 4 upgrade)
where  build_grails  was the only outstanding Groovy joint validation
failure on both 8.0.x and the upgrade branch.

Assisted-by: claude-code:claude-opus-4-7
jamesfredley added a commit that referenced this pull request May 8, 2026
…ssertions

The Groovy joint validation build ("CI - Groovy Joint Validation Build")
has been failing on the 8.0.x branch since 2026-05-07 with:

    GroovyChangeLogSpec > updates a database with Groovy Change FAILED
        Condition not satisfied:
        output.toString().contains('confirmation message')

Two intertwined causes:

1. The previous test logger config was a Groovy-DSL Logback config
   (logback.groovy) using @withJansi=true, %highlight, %cyan converters.
   In the joint validation environment the freshly-built local Groovy
   5.0.6-SNAPSHOT (GROOVY_5_0_X HEAD) interacts with Logback's
   GroovyConfigurator in a way that silently fails to register the
   'liquibase' logger -> STDOUT binding. Replaced with an equivalent
   logback-test.xml that has no Groovy / Jansi / colour-converter
   dependencies. Same logger levels and appender wiring, just XML.

2. Even with the logger config loaded, the failing assertions
    output.toString().contains('confirmation message') and
    output.toString().contains('warn message')  are environment-
   dependent. Liquibase 4.27 selects between Slf4jLogService and the
   built-in JavaLogService at Scope-init time; the choice depends on
   which SLF4J binding is bound *at that moment*. The two service
   implementations route INFO output very differently:

       Slf4jLogService -> SLF4J -> Logback ConsoleAppender -> stdout
                                  (filtered by root level / per-logger
                                  levels in whichever logback config
                                  Logback found first)
       JavaLogService  -> java.util.logging -> default ConsoleHandler
                                              -> stderr (no filtering)

   In the local dev environment Liquibase falls back to JavaLogService
   and the messages end up in captured stderr (Spock captures both),
   so the test passes. In the joint validation runner Liquibase picks
   Slf4jLogService and the messages get filtered by Logback before
   they reach stdout. Since the captured behaviour is being driven by
   classpath-and-configuration roulette rather than the code under
   test, asserting on it produces flake.

   The change being applied is already verified by  calledBlocks  in
   each test method (init / validate / change / rollback closures
   record their invocation order). The  confirm  and  warn  directives
   are exercised by GroovyChange's  confirm(String)  and  warn(String)
   methods being invoked from the parsed DSL - if those didn't run,
   the changeset wouldn't apply and  calledBlocks  would be empty.
   Drop the brittle  output  assertions and document why so a future
   maintainer doesn't re-add them.

Verified locally on Groovy 5.0.6-SNAPSHOT build #26:

    ./gradlew :grails-data-hibernate5-dbmigration:test \
        --tests 'org.grails.plugins.databasemigration.liquibase.GroovyChangeLogSpec' \
        -PmaxTestParallel=3 --rerun-tasks
    BUILD SUCCESSFUL (7 tests, 7 successes, 0 failures, 0 skipped)

Surfaced while auditing PR #15557 (Groovy 5 / Spring Boot 4 upgrade)
where  build_grails  was the only outstanding Groovy joint validation
failure on both 8.0.x and the upgrade branch.

Assisted-by: claude-code:claude-opus-4-7
…ssertions

The Groovy joint validation build ("CI - Groovy Joint Validation Build")
has been failing on the 8.0.x branch since 2026-05-07 with:

    GroovyChangeLogSpec > updates a database with Groovy Change FAILED
        Condition not satisfied:
        output.toString().contains('confirmation message')

Two intertwined causes:

1. The previous test logger config was a Groovy-DSL Logback config
   (logback.groovy) using @withJansi=true, %highlight, %cyan converters.
   In the joint validation environment the freshly-built local Groovy
   5.0.6-SNAPSHOT (GROOVY_5_0_X HEAD) interacts with Logback's
   GroovyConfigurator in a way that silently fails to register the
   'liquibase' logger -> STDOUT binding. Replaced with an equivalent
   logback-test.xml that has no Groovy / Jansi / colour-converter
   dependencies. Same logger levels and appender wiring, just XML.

2. Even with the logger config loaded, the failing assertions
    output.toString().contains('confirmation message') and
    output.toString().contains('warn message')  are environment-
   dependent. Liquibase 4.27 selects between Slf4jLogService and the
   built-in JavaLogService at Scope-init time; the choice depends on
   which SLF4J binding is bound *at that moment*. The two service
   implementations route INFO output very differently:

       Slf4jLogService -> SLF4J -> Logback ConsoleAppender -> stdout
                                  (filtered by root level / per-logger
                                  levels in whichever logback config
                                  Logback found first)
       JavaLogService  -> java.util.logging -> default ConsoleHandler
                                              -> stderr (no filtering)

   In the local dev environment Liquibase falls back to JavaLogService
   and the messages end up in captured stderr (Spock captures both),
   so the test passes. In the joint validation runner Liquibase picks
   Slf4jLogService and the messages get filtered by Logback before
   they reach stdout. Since the captured behaviour is being driven by
   classpath-and-configuration roulette rather than the code under
   test, asserting on it produces flake.

   The change being applied is already verified by  calledBlocks  in
   each test method (init / validate / change / rollback closures
   record their invocation order). The  confirm  and  warn  directives
   are exercised by GroovyChange's  confirm(String)  and  warn(String)
   methods being invoked from the parsed DSL - if those didn't run,
   the changeset wouldn't apply and  calledBlocks  would be empty.
   Drop the brittle  output  assertions and document why so a future
   maintainer doesn't re-add them.

Verified locally on Groovy 5.0.6-SNAPSHOT build #26:

    ./gradlew :grails-data-hibernate5-dbmigration:test \
        --tests 'org.grails.plugins.databasemigration.liquibase.GroovyChangeLogSpec' \
        -PmaxTestParallel=3 --rerun-tasks
    BUILD SUCCESSFUL (7 tests, 7 successes, 0 failures, 0 skipped)

Surfaced while auditing PR #15557 (Groovy 5 / Spring Boot 4 upgrade)
where  build_grails  was the only outstanding Groovy joint validation
failure on both 8.0.x and the upgrade branch.

Assisted-by: claude-code:claude-opus-4-7
@paulk-asert
Copy link
Copy Markdown
Contributor

@jamesfredley You haven't attempted to apply the 11985 PR and see what is fixed? It is still under discussion on the Groovy side. It would be great to know whether it fixes encountered problems.

@jamesfredley
Copy link
Copy Markdown
Contributor Author

@paulk-asert I will refresh the Groovy 6 canary and do a test with apache/groovy#2529

@jdaugherty
Copy link
Copy Markdown
Contributor

We should pull forward the changes in #15294 to confirm the indy issues were fixed in Groovy 5+

jamesfredley and others added 2 commits May 20, 2026 12:58
Cherry-picks the `grails-test-examples/compile-static` project from
#15294 (originally targeted at 7.0.x) onto
grails8-groovy5-sb4.

BookService calls Book.findAllByName('Joe') under @GrailsCompileStatic
- the original GROOVY-11817 surface. On Groovy 5.0.6-SNAPSHOT the
service compiles and BookServiceSpec passes without any reflection
workaround:

    BookServiceSpec > test validateBooks method PASSED

Conflict resolution notes:
- Dropped the unrelated app1/grails-app/conf/application.groovy
  whitespace tweak (8.0.x already lacks the leading blank line).
- Re-anchored the settings.gradle insert against 8.0.x's reordered
  Functional Tests include list (compile-static slots in
  alphabetically between cache and database-cleanup).

Assisted-by: opencode:claude-4.7-opus
@jamesfredley
Copy link
Copy Markdown
Contributor Author

The compile-static test app from #15294 has been brought over and passes on Groovy 5.0.6-SNAPSHOT (commit 4250923):

> Task :grails-test-examples-compile-static:integrationTest
BookServiceSpec > test validateBooks method PASSED
BUILD SUCCESSFUL in 20s

Book.findAllByName('Joe') under @GrailsCompileStatic (the original GROOVY-11817 surface) compiles and runs without any reflection workaround.

Conflict resolution: dropped #15294's unrelated whitespace tweak to app1/grails-app/conf/application.groovy (8.0.x already lacks the leading blank line); re-anchored the settings.gradle include against 8.0.x's reordered Functional Tests list (compile-static slots in alphabetically between cache and database-cleanup).


Audit notes from a fresh re-verification against current GROOVY_5_0_X HEAD:

…lution

Apache's snapshot retention has purged the 5.0.6-SNAPSHOT artifacts from
the Apache Nexus snapshot repository. Only the empty maven-metadata.xml.*
hash files remain dated 2026-05-08:

  https://repository.apache.org/content/groups/snapshots/org/apache/groovy/groovy/5.0.6-SNAPSHOT/
    (404 on maven-metadata.xml, 404 on every per-build JAR)

This blocks every CI job at the dependency resolution step:

  Could not find org.apache.groovy:groovy:5.0.6-SNAPSHOT
  Could not find org.apache.groovy:groovy-bom:5.0.6-SNAPSHOT
  Could not find org.apache.groovy:groovy-templates:5.0.6-SNAPSHOT
  Could not find org.apache.groovy:groovy-xml:5.0.6-SNAPSHOT
  Could not find org.apache.groovy:groovy-json:5.0.6-SNAPSHOT
  Could not find org.apache.groovy:groovy-sql:5.0.6-SNAPSHOT

Apache Groovy 5.0.6 was released to Maven Central on 2026-05-04 and is
resolvable from repo1.maven.org. None of the six remaining workarounds
in this PR depend on the 4 post-tag commits on GROOVY_5_0_X:

  - da06ae61 GROOVY-11989: javaparser-core 3.28.0 -> 3.28.1 (dep bump)
  - a0e717b5 GROOVY-11990: jackson 2.21.3                    (dep bump)
  - 75727913 Update dependency metadata                      (admin)
  - a1c006c9 GROOVY-11996: groovy.truth.file.exists.enabled  (opt-out
            flag targeting 5.0.7; the PR's File.asBoolean fix in
            TemplateRendererImpl is the real fix and does not need it)

So pinning to released 5.0.6 is functionally equivalent for this PR and
fixes CI immediately. Verified locally:

  > Task :grails-test-examples-compile-static:integrationTest
  BookServiceSpec > test validateBooks method PASSED
    JVM 21.0.10 | Grails 8.0.0-SNAPSHOT | Groovy 5.0.6 | Spring Boot 4.0.5

The apache snapshots repo declaration in settings.gradle is kept as-is
so the groovy-joint-workflow CI job can still swap in a Groovy snapshot
when needed.

Updates both groovy.version entries in dependencies.gradle:
  * Main bom (line 81)
  * grails-micronaut-bom strictly-override (line 222)

Assisted-by: opencode:claude-4.7-opus
@jamesfredley
Copy link
Copy Markdown
Contributor Author

CI fix: pin to released Groovy 5.0.6 (commit 423022f)

All 19 CI failures on the previous run (a000a5fa12) were a single root cause - Apache's snapshot retention has purged the 5.0.6-SNAPSHOT artifacts. The folder at https://repository.apache.org/content/groups/snapshots/org/apache/groovy/groovy/5.0.6-SNAPSHOT/ contains only maven-metadata.xml.{md5,sha1,sha256,sha512} hash placeholders dated Fri May 08 22:46:47 UTC 2026; every per-build JAR returns 404. Every Build / Functional / Hibernate5 / Mongodb / Forge job died at the dependency resolution step with:

Could not find org.apache.groovy:groovy:5.0.6-SNAPSHOT
Could not find org.apache.groovy:groovy-bom:5.0.6-SNAPSHOT
Could not find org.apache.groovy:groovy-templates:5.0.6-SNAPSHOT
Could not find org.apache.groovy:groovy-xml:5.0.6-SNAPSHOT
Could not find org.apache.groovy:groovy-json:5.0.6-SNAPSHOT
Could not find org.apache.groovy:groovy-sql:5.0.6-SNAPSHOT

5.0.6 was released to Maven Central on 2026-05-04 and is resolvable from repo1.maven.org. The 4 post-tag commits on GROOVY_5_0_X (javaparser bump, jackson bump, dep-metadata update, and GROOVY-11996's groovy.truth.file.exists.enabled opt-out flag) are not load-bearing for any of the 6 remaining workarounds, so pinning to the release is functionally equivalent for this PR and unblocks CI immediately.

Both groovy.version entries in dependencies.gradle were updated (the main bom + the grails-micronaut-bom strictly-override). The snapshot repo declaration in settings.gradle is left in place so the groovy-joint-workflow job can still swap in an upstream Groovy snapshot when needed.

Verified locally on the released 5.0.6:

> Task :grails-test-examples-compile-static:integrationTest
BookServiceSpec > test validateBooks method PASSED
  JVM 21.0.10 | Grails 8.0.0-SNAPSHOT | Groovy 5.0.6 | Spring Boot 4.0.5 | Spring 7.0.6

Open-PR overlap check

Audited every open PR against 8.0.x for duplication:

PR Branch Touches groovy.version? Conflict / overlap with this fix
#15183 (matrei, groovy-5) groovy-5 Yes - pins to 5.0.5 Older Groovy 5 attempt, on 5.0.5 (behind us), currently CONFLICTING + 25 failing checks. No overlap; that branch is orphaned by this one.
#15654 (jdaugherty, 8.0.x-stage-hibernate7) 8.0.x-stage-hibernate7 No Independent (Hibernate 7 stage 1).
#15664, #15652, #15619, #15467, #15465 various No Docs / BOM refactor / RELEASE.md / TagLib syntax. None touch groovy.version.

So this fix is unique and not duplicating any other in-flight PR.

PR description updated

Bumped audit date to 2026-05-20, updated the target stack row + status paragraphs to reflect the snapshot purge and the pin-to-release decision, added a bullet under ## Forge / generated-app coverage for the new compile-static test app from #15294, and updated the GROOVY-11996 cell in the workarounds table to note that the real-fix rewrites do not depend on the opt-out flag.

CI is queued on the new HEAD; will report back if the build surfaces issues beyond the dependency-resolution block.

jamesfredley added a commit that referenced this pull request May 20, 2026
Brings in from #15557:
- The grails-test-examples/compile-static project (#15294 cherry-pick).
- The 5.0.6-SNAPSHOT -> 5.0.6 pin in dependencies.gradle (conflict
  resolved in favour of this branch's 6.0.0-SNAPSHOT pin, since this
  canary tracks Groovy 6 not Groovy 5).

Assisted-by: opencode:claude-4.7-opus
@jamesfredley
Copy link
Copy Markdown
Contributor Author

Will switch to Groovy 5.0.7-SNAPSHOT after apache/groovy#2547

Resolve conflict in dependencies.gradle by keeping Groovy 5.0.6
(required by this branch's purpose) alongside the new graphql-java
and graphql-java-extended-scalars version entries introduced on 8.0.x.

Assisted-by: claude-code:claude-4.7-opus
Track the GROOVY_5_0_X branch so post-5.0.6 fixes are picked up as they
land. Diff from the GROOVY_5_0_6 release tag to GROOVY_5_0_X HEAD
(eca67326e) is 4 substantive commits: GROOVY-11989 (javaparser bump),
GROOVY-11990 (jackson bump), a metadata update, and GROOVY-11996 (the
`groovy.truth.file.exists.enabled=false` opt-out flag already
cross-referenced in workaround #1 of the PR description; the PR's
real-fix rewrites do not depend on the flag).

None of these commits eliminate any of the 6 remaining workarounds.
The Apache snapshots repository declaration in settings.gradle already
includes `org[.]apache[.]groovy.*` so `5.0.7-SNAPSHOT` resolves from
https://repository.apache.org/content/groups/snapshots/ with no
settings.gradle change required.

Verified by compiling the workaround-site modules against
5.0.7-SNAPSHOT (build 5.0.7-20260520.205749-1):
:grails-core, :grails-validation, :grails-datamapping-tck,
:grails-views-gson, :grails-shell-cli - all green with the
workarounds in place.

Assisted-by: claude-code:claude-opus-4-7
@jamesfredley jamesfredley changed the base branch from 8.0.x to fix/8.0.x-merge-sb4-fallout May 22, 2026 01:40
@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented May 22, 2026

✅ All tests passed ✅

🏷️ Commit: 1c411f8
▶️ Tests: 13984 executed
⚪️ Checks: 31/31 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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants