Skip to content

[Canary] Grails 9 on Groovy 6.0.0-SNAPSHOT - #15558

Draft
jamesfredley wants to merge 63 commits into
9.0.xfrom
grails8-groovy6-canary
Draft

[Canary] Grails 9 on Groovy 6.0.0-SNAPSHOT#15558
jamesfredley wants to merge 63 commits into
9.0.xfrom
grails8-groovy6-canary

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Purpose

Canary / DRAFT - DO NOT MERGE.

Track Apache Groovy 6.0.0-SNAPSHOT on Grails 9 while Groovy 6 is still pre-release. Early-warning only. Grails 9 is the intended Groovy 6 vehicle. Success metric: the smallest tip-vs-9.0.x delta that still builds and tests.

Groovy 5 baseline behavior from #15557 is accepted as-is and is not listed here.

Base branch: 9.0.x (refreshed 2026-08-03 with 8.0.x8.1.x9.0.x merges).

Baseline (current tip)

Component Version
Apache Groovy 6.0.0-SNAPSHOT
Spock 2.4-groovy-5.0 + temporary version-check bridge
Spring Boot / Framework 9.0.x line
Gradle 9.6.1
JDK 21+

Remaining Groovy 6 workarounds (current tip)

Prefer deleting a row when upstream removes the need for it. Do not grow this list with unrelated trunk work.

# Area What the workaround is Why it is still needed Remove when
1 Spock version-check bridge Propagate -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true through build JVM args, GroovyCompile/Test forks, Mongo/Hibernate5/functional test configs, forked GSP/GSON view compilation, and Forge-generated build.gradle. Spock still publishes only the Groovy 5 line; its global AST transform aborts under Groovy 6. Spock's Groovy 6 work is CI-only (no release artifact yet). Spock publishes a Groovy 6 artifact and the BOM can pin it.
2 Micronaut island isolation Auto-skip Micronaut projects unless the root BOM is still on Groovy 5; pin Hibernate5 Micronaut island Groovy to 5.0.8. Micronaut island is Groovy 5 / Spock 5 and cannot share a Groovy 6 root graph. Micronaut island supports Groovy 6, or the island leaves this monorepo build.
3 Jackson 2 BOM alignment Manage Jackson 2 modules at 2.22.1 (annotations independently at 2.22) so the BOM stays >= the suite pulled by groovy-yaml / Hibernate7 dbmigration. Groovy 6 dependency graph resolves Jackson 2.22.x newer than the 9.0.x pin (2.21.5). Groovy 6 no longer forces a newer Jackson 2 line, or 9.0.x already manages the same line.
4 JSON views verifier GrailsJsonViewHelper.render(...) methods are temporary default methods; TemplateRenderer has explicit render forwarders instead of relying only on @Delegate. Groovy 6 verifier mishandles abstract/inner-class return types with @Delegate / interface methods. Candidate for retest against current Groovy master (GROOVY-10687), not removed without canary proof. Upstream verifier accepts the Groovy 5 shape without these shims.
5 Form fields constructors Explicit @MapConstructor on BeanPropertyAccessorImpl. Under @CompileStatic, Groovy 6 no longer gets the named-arg constructor from @Canonical alone. Groovy restores the prior constructor generation.
6 Config write tracking @Delegate(excludes = ['put', 'putAll', 'remove']) on WriterFilteringMap. Mutating calls can dispatch to generated delegate methods and skip tracking under Groovy 6. Dispatch always hits the tracking overrides without excludes.
7 GORM static property missing GormEntity.staticPropertyMissing checks the class MetaClass before falling through to the static API. Trait/static property dispatch still differs under Groovy 6. Static GORM property reads work without the guard.
8 Core plugin bean registration Use BeanConfiguration.addProperty / setParent instead of dynamic property assignment in CoreGrailsPlugin BeanBuilder closures. Groovy 6 rejects the old dynamic property-set path used by BeanBuilder. BeanBuilder dynamic property assignment works again under Groovy 6.
9 URL mapping cleanup Qualify UrlMappingsHandlerMapping.MATCHED_REQUEST in the nested handler. Unqualified constant resolution under Groovy 6 targets the wrong receiver and breaks request cleanup. Nested-class constant resolution matches Groovy 5.
10 AST transform Map access Prefer classInjectorCache.get(key) over subscript in GlobalGrailsClassInjectorTransformation (retained through the 9.0.x transform rewrite). @CompileStatic subscript binds to a DGM method removed in Groovy 6. Static Map subscript compiles/runs without the .get rewrite.
11 SBOM JLine 4 licenses Force correct licenses for org.jline:*@4.x Maven jar PURLs. Groovy 6 pulls JLine 4; CycloneDX still mis-identifies those licenses. CycloneDX / metadata reports JLine 4 correctly.
12 Joint Groovy CI branch Map Groovy major 6 joint validation to Apache Groovy master. There is no GROOVY_6_0_X maintenance branch yet; alpha-2 is already behind master. Groovy opens a GROOVY_6_0_X line, or joint validation learns the same rule on trunk.
13 Classic call-site module Manage org.apache.groovy:groovy-callsite in the BOM and declare it api on grails-common. Groovy 6 split classic (non-indy) call-site bytecode generation into an optional module (GROOVY-11158), and the Grails Gradle plugin disables indy by default (#15293), so class generation aborts with ClassNotFoundException: org.codehaus.groovy.runtime.callsite.CallSiteArray without it. Groovy folds call-site support back into the core jar, or Grails defaults indy to enabled. Note: the dependency is Groovy 6-only - gate it by Groovy major if this ever moves to a branch that still builds on Groovy 5.
14 Lifecycle interface default method in Java Move GrailsApplicationLifeCycle from Groovy to Java so default BeanRegistrar beanRegistrar() is a plain JVM default method; keep an explicit null override on GrailsApplicationLifeCycleAdapter. Boot blocker under classic callsites. Groovy 6 classic-callsite compilation of a Groovy interface default method emits invokestatic GrailsApplicationLifeCycle$1.$getCallSiteArray but never packages GrailsApplicationLifeCycle$1, so real apps fail at boot with NoClassDefFoundError: grails/core/GrailsApplicationLifeCycle$1 when the framework is built with -PgrailsIndy=false (the Grails default). Indy compilation of the same method is a plain return null and boots fine. Confirmed by javap + grails-test-examples-latency integration tests. Related to GROOVY-11158 / interface-default + classic call-site codegen. Groovy 6 classic-callsite correctly emits and packages CallSite holders for interface default methods (or Grails permanently defaults indy on). Sibling landmines to retest under non-indy: GrailsJsonViewHelper defaults (@CompileStatic), IContainerGebConfiguration defaults (testFixtures).

Not counted as workarounds

Present in the tip delta but treated as ordinary canary baseline, fixes, or tests:

  • Switching groovy.version to 6.0.0-SNAPSHOT
  • Gradle 9.6.1 upgrade (trunk is on 9.6.0)
  • ConfigurationBuilder nested-map conversion / unknown-key rejection
  • ClassPropertyFetcher interface-entry static property hierarchy fix
  • Parentheses fix in HibernateGormInstanceApi embedded association check (if still tip-only)
  • Tests updated for Groovy 6 call/resolution behavior
  • Spring DM end-to-end example ext['groovy.version'] override so Spring DM does not pull Boot's Groovy over the canary BOM
  • Regression unit spec GrailsApplicationLifeCycleDefaultMethodSpec (guards This is a unit test for GRAILS-4200 #14)

Operating rule

If a new Groovy 6 snapshot or Spock release makes a row unnecessary, delete that workaround in this PR. Do not grow the list with unrelated trunk work. Prefer verifying removal with a focused canary build before deleting.

Tip delta size (vs 9.0.x)

Roughly ~50 files as of 2026-08-03 after the trunk refresh, the groovy-callsite fix, and the lifecycle Java default-method fix. Keep shrinking this number.

Local indy vs classic callsite notes (2026-08-03)

Framework JMH hot-path benches (separate experimental branch, technique from #16071) showed no clear indy regressions vs classic callsites on Groovy 6 for databinding/GSP/urlmappings, with interceptor matching faster under indy.

App-level validation is more important: with #14 unfixed, apps could not boot at all under non-indy. After the Java lifecycle default, grails-test-examples-latency boots and serves under both -PgrailsIndy=false and true. That makes classic-callsite A/B possible again for real apps once the harness lands.

@testlens-app

This comment has been minimized.

Comment thread build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy Outdated
@bito-code-review

This comment was marked as outdated.

jamesfredley added a commit that referenced this pull request Apr 25, 2026
Per @jdaugherty review on
#15558 (comment):

> This defeats the entire purpose of this plugin. We should not wholesale
> map these. every version has to be checked because at any time a license
> can change. We need to review these individually
>
> FYI: if these are really wrong, we should be pushing upstream on cyclone
> or the jline project itself to fix their licensing.

Both points are correct. The SBOM plugin's value is exactly that each
artifact-version is auditable, and a wholesale group rule erases that
guarantee the moment a transitive bumps onto a new major. Drop the
LICENSE_GROUP_MAPPING map and the matching group-fallback branch in
pickLicense, and go back to per-version entries with explicit
provenance.

Per-version replacements added (each carries the upstream-versioned
LICENSE.txt URL inline so future maintainers can re-verify on the next
SNAPSHOT bump):

  pkg:maven/org.jline/jansi@4.0.12             BSD-3-Clause
  pkg:maven/org.jline/jline@3.30.6             BSD-3-Clause   (direct)
  pkg:maven/org.jline/jline-builtins@4.0.12    BSD-3-Clause
  pkg:maven/org.jline/jline-console@4.0.12     BSD-3-Clause
  pkg:maven/org.jline/jline-console-ui@4.0.12  BSD-3-Clause
  pkg:maven/org.jline/jline-native@4.0.12      BSD-3-Clause
  pkg:maven/org.jline/jline-reader@4.0.12      BSD-3-Clause
  pkg:maven/org.jline/jline-shell@4.0.12       BSD-3-Clause
  pkg:maven/org.jline/jline-style@4.0.12       BSD-3-Clause
  pkg:maven/org.jline/jline-terminal@4.0.12    BSD-3-Clause
  pkg:maven/org.jline/jline-terminal-jni@4.0.12 BSD-3-Clause

Each was verified against
https://github.com/jline/jline3/blob/jline-parent-<version>/LICENSE.txt
which carries the BSD-3-Clause text. The cyclonedx-core-java#205
misclassification (BSD-4-Clause) is the same root issue we have for the
2.14.6 / antlr4 entries.

The 3.30.9 and 4.0.7 entries from the merge with grails8-groovy5-sb4
are dropped because Groovy 6.0.0-SNAPSHOT now resolves the entire
org.jline:* group to 4.0.12 transitively via groovy-groovysh; verified
with `:grails-shell-cli:dependencies --configuration runtimeClasspath`
plus the `Forcing license for ...` log lines on cyclonedxBom. If a
future SNAPSHOT bumps onto a new major (5.x), we add fresh per-version
entries with re-verified provenance, exactly as the SBOM plugin
intends.

Verified locally:
  ./gradlew :grails-shell-cli:cyclonedxBom :grails-console:cyclonedxBom \
            :grails-dependencies-starter-web:cyclonedxBom \
            -PskipCodeStyle --rerun-tasks
  -> BUILD SUCCESSFUL in 1m 56s

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

This comment was marked as outdated.

@jamesfredley

This comment was marked as outdated.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Final wrap-up: Groovy 5 / Groovy 6 workaround audit complete

This is the final state of the workaround audit after end-to-end integration testing against Apache Groovy 6.0.0-SNAPSHOT master HEAD (build 508-516; verified that builds 509-516 add only Javadoc + 1 build-infra refactor on top of build 508 - so no functional delta vs. master HEAD).

Bottom line

5 Groovy 5 workarounds removed in this audit. 7 confirmed required. 2 will be removable when 2 OPEN upstream PRs merge. 1 is Spring 7 specific. 3 untestable locally (Forge canary red for unrelated reasons).

Methodology recap

  1. Read every // Groovy 5 and // GROOVY- comment in the working tree
  2. For each, attempted removal and ran the affected module's test suite on Groovy 6.0.0-SNAPSHOT build 508
  3. Verified MongoDB integration tests with live mongo:7.0 TestContainer (478 tests passed with BsonPersistentEntityCodec.resolvePropertyType() walker removed)
  4. Verified Geb indy=false integration tests with selenium/standalone-chrome:latest TestContainer (reproduced VerifyError: get long/double overflows locals on ContainerSupport$Trait$Helper.createFileInputSource @0: dload_3 when @CompileDynamic reverted - the workaround stays)
  5. Cloned apache/groovy master, fetched OPEN PRs GRAILS-10803: Can't see chinese in log console in 2.3.x #2492 and GRAILS-6219: Under certain circumstances the DefaultGrailsDomainClass's metaClass is a MetaClassImpl, not an ExpandoMetaClass #2493, built each locally with ./gradlew publishToMavenLocal, replaced cached SNAPSHOT JARs, reverted the corresponding Grails workaround, and re-ran the affected tests to confirm both upstream PRs unblock our workarounds

Verified upstream PRs that unblock our remaining workarounds (NOT yet merged to master)

Upstream Local verification Grails workaround it unblocks
OPEN PR apache/groovy#2492 (GROOVY-11966, Paul King) - synchronises NodeMetaDataHandler.getNodeMetaData map access Built locally, replaced cache JAR (NodeMetaDataHandler monitorenter count went from 0 to 7), reverted both parallelism guards, ran 8 parallel :grails-test-examples-*:compileGroovyPages - BUILD SUCCESSFUL, 0 ListHashMap errors. Race only reliably reproduces in CI (timing-dependent) so we couldn't reliably reproduce the negative case locally, but the synchronisation is a textbook fix for the documented ArrayIndexOutOfBoundsException race grails-gsp/core/.../GroovyPageCompiler.groovy parallelism guard + grails-views-core/.../AbstractGroovyTemplateCompiler.groovy parallelism guard
OPEN PR apache/groovy#2493 (GROOVY-11967, Paul King) - adds CHECKCAST to indy-mode ListExpressionTransformer to fix the VerifyError on the synthesised lower-arity bridge constructor Built locally, replaced cache JAR (verified InvokeDynamicWriter reference now in ListExpressionTransformer$NewListExpression.class), reverted DefaultConstraintFactory + MappingContextAwareConstraintFactory back to single-constructor form with targetTypes = [Object] as List<Class> default value, ran :grails-validation:test :grails-datamapping-validation:test under both indy=false and indy=true - BUILD SUCCESSFUL grails-datamapping-validation/.../DefaultConstraintFactory.groovy two-ctor split + grails-datamapping-core/.../MappingContextAwareConstraintFactory.groovy two-ctor split

Verified upstream JIRAs already in build 508+ master HEAD

JIRA Status Fix commit Already-removed Grails workaround
GROOVY-11907 "trait static field helper generates invalid bytecode" Resolved (5.0.6) 19f38997a (2026-04-08) HibernateEntity static SQL methods (commit 8af5d1dc4c), JspTagImpl @CompileDynamic (commit 2bb0930a5d), ClassPropertyFetcherTests generic trait (commit a71c8b5ebb), GormEntityTransformation AST shim path (now unconditional, commit 8e9cdbc50f), MongoCodecSession increment, scaffolding GROOVY-11907 trait statics (commit a290b37156). Note: indy=false static-setter helper path is NOT covered - reproduced today on ContainerSupport, needs an upstream follow-up filed
GROOVY-11911 "Restore Groovy 5's MOP-aware call dispatch for Java Closure subclasses overriding call(Object) without doCall" Resolved (master) ac71deb (2026-04-26 07:19 UTC, in build 508) ControllerActionTransformer Closure dispatch workaround (REMOVED in this audit) - reverted Java for-loop back to DefaultGroovyMethods.count(Iterable, Closure) form, all 133 :grails-controllers:test :grails-rest-transforms:test tasks green
GROOVY-11512 "Inconsistent isAttribute & getAttribute behavior in Groovy 4 with traits" Resolved (4.0.28 / 5.0.0-beta-2 / 6.0.0-alpha) 88c63360 (2024-11-01) TraitPropertyAccessStrategy (inherited from base PR #15557, not retested in this canary)
GROOVY-11829 "Properties located from a set(key, value) always use the same method even when the value type is better matched by another" Resolved (6.0.0-alpha-1) 7bc29825bc (2026-01-01) This addresses set(...) not get(...) - it does NOT address our MetaClassImpl.isGenericGetMethod instance-dispatch hijack, which is why our GormEntityTransformation instance get(String) shim is still required
GROOVY-11522 "Possible Null Pointer Dereference in VariableScopeVisitor" Resolved (4.0.28 / 5.0.0-beta-2 / 3.0.26) f5666584e1 (2026-02-24) This addresses a different findClassMember NPE - it does NOT address our 4 visitConstructorOrMethod NPE catches, which still reproduce on master HEAD when removed. Our NPE class needs a separate upstream filing

Issues that NEED to be filed upstream (no matching JIRA / PR found)

Issue Caused workaround
MetaClassImpl static-trait get(String) hijacking instance dispatch (distinct from GROOVY-11829 which addresses set(key, value)) GormEntityTransformation instance get(String) shim
Our specific VariableScopeVisitor.visitConstructorOrMethod NPE class (4 catch sites + 2 null-VariableScope ClosureWriter sites; one umbrella bug family per Oracle review) GrailsASTUtils, AstUtils, AbstractMethodDecoratingTransformation, ResourceTransform, LoggingTransformer
@Delegate field on trait silently returns null on Groovy 5/6 lowering (separate from GROOVY-11512) GrailsApplicationCommand trait → abstract class
TraitReceiverTransformer static override loss when calling this.method() from a trait static method Validateable.resolveDefaultNullable reflection lookup
ConfigObject infinite recursion under Map iteration (Groovy 4 → 5 behavior change carried into 6, no JIRA filed since 2014) NavigableMap.convertConfigObjectToMap() shallow + lazy conversion
Interface $getCallSiteArray() IncompatibleClassChangeError under indy=false IContainerGebConfiguration interface → trait
GROOVY-11907 follow-up: indy=false static-setter trait helper bytecode (verified today: dload_3 overflow on 2-local frame) ContainerSupport @CompileDynamic

Forge integration tests (3 workarounds remaining there)

GrailsApplicationCommand (trait → abstract class), TemplateRendererImpl (explicit type checks), and GenerateControllerCommand (explicit 4-arg render calls) all compile cleanly when reverted. Their failure mode is silent runtime @Delegate returning null, only catchable by Forge ScaffoldingSpec.test generate-controller command integration test. Build Grails Forge is currently red on this branch's CI for an unrelated compileTestGroovy failure (was already red on the previous canary CI run before any of these fixes), so we cannot get a clean signal. These workarounds stay until Forge canary goes green.

Net delta

  • Working tree: 14 files changed, 5 source workarounds removed (-128 / +51 lines), 9 inline comment updates from "Groovy 5" → "Groovy 5/6" with reproducer details on the workarounds confirmed still required.
  • All 530 modules compile clean: ./gradlew classes -> BUILD SUCCESSFUL in 2m 10s.
  • No commit yet - waiting for review.

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

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Final wrap-up: workaround burndown audit complete

After end-to-end testing against apache/groovy master HEAD (build 508+; verified == master HEAD modulo Javadoc commits), here's the bottom line.

Burndown count

  • 6 Groovy 5 workarounds REMOVED in this audit (verified by full module tests)
  • 4 outstanding workarounds gated on 2 OPEN upstream PRs (apache/groovy#2492 and #2493 by Paul King) - locally verified that those PRs unblock the corresponding Grails workarounds
  • 13 outstanding workarounds need to be filed upstream - each has a reproducer test that fails on master HEAD when reverted
  • 1 inherited from base PR removed (TraitPropertyAccessStrategy is now restored to pre-Groovy-5 form since GROOVY-11512 is fixed in 6.0.0-alpha)

Verified upstream PRs unblock our remaining workarounds

Upstream Verification
GROOVY-11966 / OPEN PR apache/groovy#2492 synchronises NodeMetaDataHandler.getNodeMetaData map access Cloned apache/groovy, fetched PR branch, ran ./gradlew publishToMavenLocal -x test -x check -x javadoc -x groovydoc -x asciidoctor, replaced cached 6.0.0-SNAPSHOT JARs (verified monitorenter count in NodeMetaDataHandler went from 0 to 7), reverted both parallelism guards, ran 8-project parallel :grails-test-examples-*:compileGroovyPages - clean. Original race only reliably reproduces in CI (timing-dependent).
GROOVY-11967 / OPEN PR apache/groovy#2493 adds CHECKCAST to indy-mode ListExpressionTransformer Built PR #2493 locally, replaced cached snapshot (verified InvokeDynamicWriter reference present in ListExpressionTransformer$NewListExpression), reverted DefaultConstraintFactory + MappingContextAwareConstraintFactory back to single-constructor form, ran :grails-validation:test :grails-datamapping-validation:test under both indy=true and indy=false - clean.

Verified upstream JIRAs already in master and removed our workaround

  • GROOVY-11512 (trait boolean property generates isser and getter) - resolved in 6.0.0-alpha. Removed TraitPropertyAccessStrategy is-prefix fallback, verified :grails-data-hibernate5-core:test --rerun-tasks 79/79 green.
  • GROOVY-11829 (set(key, value) method selection) - resolved in 6.0.0-alpha. NOT our get(String) hijack issue (those are different code paths) so our GormEntityTransformation AST shim is still required.
  • GROOVY-11907 (trait static field bytecode) - resolved in 5.0.6. Most workarounds removed in earlier commits. ContainerSupport indy=false static-setter helper path is NOT covered by this fix - verified today with reproducible VerifyError: get long/double overflows locals at ContainerSupport$Trait$Helper.createFileInputSource @0: dload_3 under :grails-test-examples-geb:integrationTest -PgrailsIndy=false. Needs a follow-up filed.
  • GROOVY-11911 (count(Iterable, Closure) MOP doCall) - resolved in master, in build 508+. Removed ControllerActionTransformer Closure dispatch workaround, restored to original DefaultGroovyMethods.count(...) form, verified 133/133 tasks green.
  • GROOVY-11522 (VariableScopeVisitor NPE in findClassMember) - resolved in 4.0.28 / 5.0.0-beta-2. NOT our visitConstructorOrMethod NPE class (different code path, same file).

Issues to file upstream (no matching JIRA / PR found in apache/groovy master commit log or JIRA)

The 7 upstream issues that still need to be filed:

  1. MetaClassImpl static-trait get(String) hijacking instance dispatch (caused GormEntityTransformation AST shim - distinct from GROOVY-11829's set(key, value) fix)
  2. VariableScopeVisitor.visitConstructorOrMethod NPE class (4 catch sites + 2 null-VariableScope ClosureWriter sites - one umbrella bug family)
  3. @Delegate field on trait silently returns null on Groovy 5/6 lowering (caused GrailsApplicationCommand trait → abstract class)
  4. @Delegate named-arg bridge silently corrupts template name in render(Map) (verified today: Template [Controller.groovy]] not found - caused TemplateRendererImpl + GenerateControllerCommand workarounds)
  5. TraitReceiverTransformer static override loss when calling this.method() from a trait static method (caused Validateable.resolveDefaultNullable reflection)
  6. ConfigObject infinite recursion under Map iteration (caused NavigableMap.convertConfigObjectToMap shallow conversion - latest ConfigObject commits in master are 2012-2014)
  7. Interface $getCallSiteArray() IncompatibleClassChangeError under indy=false (caused IContainerGebConfiguration interface → trait)

Plus: GROOVY-11907 follow-up for the indy=false static-setter trait helper bytecode (caused ContainerSupport @CompileDynamic - reproduced today).

Net change

15 files modified, -135/+51 lines, 6 Groovy 5 workarounds removed. All 530 modules compile clean. Forge ScaffoldingSpec.test generate-controller command passes with the workarounds in place.

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

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Audit pass against Groovy 6.0.0-SNAPSHOT build #518 (2026-04-27)

Pulled the latest snapshot from Apache snapshots (build #518, timestamp 2026-04-27 14:33:02 UTC; tracks apache/groovy master HEAD at 2026-04-27 15:50 UTC modulo CI lag) and re-verified every Groovy 6 workaround on this branch. Inherited the Groovy 5 audit results from #15557 via merge.

Workarounds with confirmed upstream fix in flight

Workaround Upstream PR / JIRA
GSP compile parallelism guard (GroovyPageCompiler, AbstractGroovyTemplateCompiler) apache/groovy#2492 (GROOVY-11966)
DefaultConstraintFactory / MappingContextAwareConstraintFactory two-constructor split apache/groovy#2493 (GROOVY-11967)
ContainerSupport @CompileDynamic (trait static fields under indy=false) apache/groovy#2495 (GROOVY-11968) - newly opened by @paulk-asert today, explicit GROOVY-11907 follow-up

All three are OPEN as of build #518; bug confirmed still present. When each merges + a fresh snapshot publishes, the corresponding workaround can be reverted.

Standalone reproducers published for the four real Groovy 6 regressions still needing upstream filing

# Reproducer repo What it isolates
1 groovy6-get-as-generic-getter Groovy 6 MetaClassImpl picks up Object get(Serializable) as the genericGetMethod for instance property access, hijacking propertyMissing(String). Drives the GormEntityTransformation per-entity AST Object get(String) shim.
2 groovy-trait-static-method-override-bug Groovy 5+ TraitReceiverTransformer rewrites this.someStatic() from inside a trait body to call the trait helper directly, silently losing implementing-class overrides. Drives Validateable.resolveDefaultNullable(Class) reflection workaround.
3 groovy5-compiledynamic-trait-bug Groovy 5+ @CompileStatic render(Map<String,Object>) overload silently no-ops against multi-overload interface references. Drives the typed positional call shape in GenerateControllerCommand and TemplateRendererImpl. (Despite the repo name, also covers Groovy 6 with the same shape and outcome.)
4 groovy5-compiledynamic-trait-bug/quick-checks/InterfaceDefaultsCheck.groovy Interface with default methods compiled with $getCallSiteArray() -> IncompatibleClassChangeError under indy=false. Drives the IContainerGebConfiguration interface->trait conversion.

Each repo has a self-contained build, README pinned to Java 21 + Gradle 9.4.1, and toggles for Groovy 4/5/6 + indy=true/false. Reverting any of the corresponding Grails workarounds and re-running the related test on this branch reproduces the cited failure.

Inherited-from-#15557 workarounds re-verified on Groovy 6

  • PersistentEntityCodec smart-cast workaround (SmartCastCheck.groovy) - still needed
  • NavigableMap.resolveConfigMapValue containsKey + get fix - still needed
  • VariableScopeVisitor try/catch guards (4 sites) - still needed
  • ResourceTransform non-null VariableScope guard - still needed
  • @Slf4j LoggingTransformer was just a comment update - reverted

Removed since Groovy 5 (Groovy 6 fixed them)

  • AbstractConstraint.java getDefaultMessageFromBundle fallback
  • GroovyConfigPropertySourceLoader.toRegularMap
  • HibernateEntityTransformation instanceof InnerClassNode swap
  • ControllerActionTransformer count overload (GROOVY-11911 merged 2026-04-26)
  • BsonPersistentEntityCodec.resolvePropertyType hierarchy walker
  • TraitPropertyAccessStrategy is-prefix fallback (GROOVY-11512 in 6.0.0-alpha)

Net effect

Workaround surface area on this canary is now:

cc @paulk-asert - the four "no upstream PR yet" reproducers (#1-#4 above) are all small, deterministic, and don't pull in Grails or GORM. Each one would benefit from upstream eyes; happy to file the JIRAs and link the reproducers from there if that helps.

The PR description has the full per-site inventory.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Re-audit 2026-05-02 against Groovy 6.0.0-SNAPSHOT build #546

Pulled latest grails8-groovy5-sb4 into the canary, resolved the two merge conflicts (dependencies.gradle keeping groovy.version: 6.0.0-SNAPSHOT while picking up base's new hibernate-groovy-proxy / jakarta-servlet-api / jakarta-validation / junit / selenium 4.38.0 entries; ContainerSupport.groovy re-evaluated below), then re-checked every workaround against apache/groovy master HEAD 4654b1134b5c3f5d7a8277f150f16ae430521ad9 (2026-05-02 12:26 UTC) and the freshly-published 6.0.0-SNAPSHOT build #546 (2026-05-02 11:47 UTC).

Headline finding

The three upstream PRs that the previous audit flagged as OPEN all merged on 2026-05-01, after the previous canary baseline (build #518, 2026-04-27) was cut. Build #546 contains all three fixes:

JIRA apache/groovy PR Merged Merge SHA
GROOVY-11968 #2495 2026-05-01 03:40 UTC 84f2f37c4f93d6ea44ad8bc76570704c84499c6b
GROOVY-11967 #2493 2026-05-01 09:37 UTC 406feaf5082f1741c318f924b520c4c27bfa0754
GROOVY-11966 #2492 2026-05-01 18:58 UTC 8dde1c84134ef6fdeecf26b5cbb5183d5aab4dac

Workarounds removed in this push (5 sites, 1 commit: 2a5e983555)

  • grails-geb/.../testFixtures/grails/plugin/geb/support/ContainerSupport.groovy - reverted from @CompileDynamic to @CompileStatic (GROOVY-11968 fix). The Trait$Helper static-setter VerifyError under grailsIndy=false no longer reproduces.
  • grails-datamapping-validation/.../DefaultConstraintFactory.groovy - collapsed the two-constructor split back to a single constructor with List<Class> targetTypes = [Object] (GROOVY-11967 fix).
  • grails-datamapping-core/.../MappingContextAwareConstraintFactory.groovy - same collapse.
  • grails-gsp/core/.../GroovyPageCompiler.groovy - dropped the parallelism guard, the grails.gsp.compiler.parallelism system property, the computeGspCompilerParallelism() helper and the isGroovy6OrLater() runtime probe; restored the original Executors.newFixedThreadPool(availableProcessors() * 2) and the matching collationLevel (GROOVY-11966 fix). 60 lines deleted.
  • grails-views-core/.../AbstractGroovyTemplateCompiler.groovy - same restoration; dropped the grails.views.compiler.parallelism system property. 59 lines deleted.

Net diff: 5 files changed, 7 insertions(+), 146 deletions(-).

Local verification

Compiled all five touched modules under Java 21 / Groovy 6.0.0-SNAPSHOT build #546:

./gradlew :grails-datamapping-validation:compileGroovy   -> BUILD SUCCESSFUL
./gradlew :grails-datamapping-core:compileGroovy         -> BUILD SUCCESSFUL
./gradlew :grails-gsp-core:compileGroovy                 -> BUILD SUCCESSFUL
./gradlew :grails-views-core:compileGroovy               -> BUILD SUCCESSFUL
./gradlew :grails-geb:compileTestFixturesGroovy          -> BUILD SUCCESSFUL

Runtime validation (parallel GSP / GSON template compilation surfacing the ListHashMap.toMap race; ContainerGebSpec class-init under grailsIndy=false exercising the trait-static-field VerifyError; validator constraint construction exercising the lower-arity bridge constructor VerifyError) is deferred to the canary CI matrix on this push.

Workarounds re-evaluated and kept (no new upstream fix yet)

Each was checked against the same build #546 / master HEAD; none has an upstream merge:

  1. grails-datamapping-core/.../GormEntityTransformation.groovy - per-entity AST Object get(String) shim. MetaClassImpl genericGetMethod hijack on GORM entities. Reproducer: https://github.com/jamesfredley/groovy6-get-as-generic-getter.
  2. grails-validation/.../Validateable.groovy - resolveDefaultNullable(Class) reflection dispatch. TraitReceiverTransformer static-method override loss. Reproducer: https://github.com/jamesfredley/groovy-trait-static-method-override-bug.
  3. grails-core/.../template/TemplateRendererImpl.groovy + grails-scaffolding/.../GenerateControllerCommand.groovy - typed positional render(Resource, File, Map, boolean) instead of named-argument render(Map). @CompileStatic overload resolution silent no-op on render(Map). Reproducer: https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug.
  4. grails-geb/.../testFixtures/grails/plugin/geb/ContainerGebConfiguration.groovy - IContainerGebConfiguration as trait rather than interface. Interface $getCallSiteArray() IncompatibleClassChangeError under grailsIndy=false. Reproducer: https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug/blob/main/quick-checks/src/main/groovy/InterfaceDefaultsCheck.groovy.
  5. grails-data-mongodb/core/.../PersistentEntityCodec.groovy - two ManyToMany.isAssignableFrom(...) swaps. @CompileStatic smart-cast bug in the else branch of if (cond && !(x instanceof Y)). Reproducer: https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug/blob/main/quick-checks/src/main/groovy/SmartCastCheck.groovy.
  6. grails-bootstrap/.../NavigableMap.groovy - containsKey + get instead of [] operator. ConfigObject [] operator mutates on missing-key reads.
  7. grails-core/.../GrailsASTUtils.java, grails-datastore-core/.../AstUtils.groovy, grails-datamapping-core/.../AbstractMethodDecoratingTransformation.groovy, grails-rest-transforms/.../ResourceTransform.groovy - try/catch around VariableScopeVisitor plus non-null VariableScope guard on ClosureExpression. VariableScopeVisitor NPE during canonicalisation on certain Grails AST transformation outputs.

The four "no upstream PR yet" rows still need to be filed against apache/groovy with the standalone reproducers above.

Heads-up: unrelated snapshot drift in build #546

While running local builds I hit one failure that is not caused by these removals and that I want to flag separately:

> Task :grails-data-hibernate5-core:compileGroovy
.../HibernateConnectionSourceSettings.java:89: error: clone() in HibernateSettings cannot override clone() in HashMap
@groovy.transform.Generated() public  ...HibernateSettings clone() throws java.lang.CloneNotSupportedException { return null; }
overridden method does not throw CloneNotSupportedException

The Groovy 6 stub generator now emits throws java.lang.CloneNotSupportedException on the @Generated clone() override of a class that extends LinkedHashMap<String, String>, but LinkedHashMap.clone() doesn't declare that exception, so javac rejects the stub. Confirmed by stashing my workaround removals: the failure reproduces against the merged state without my edits, so it's a separate Groovy 6 SNAPSHOT regression that arrived between build #518 and build #546, not a side effect of dropping these workarounds. Filing this as its own upstream issue is the right next step (likely a sibling of GROOVY-11823 / the @Generated-overrides-checked-exceptions family); for now grails-data-hibernate5-core will fail until the Groovy team revisits it or we add a narrowly-scoped workaround there.

Files

@jamesfredley

Copy link
Copy Markdown
Contributor Author

CI fix - 2026-05-02 follow-up

The Core Projects job was red on the previous push (2a5e983555), but not because of the workaround removals: the failure reproduces against the unmodified merged tree (verified locally by stashing the audit edits). Root cause is a separate Groovy 6.0.0-SNAPSHOT regression that arrived between build #518 and build #546 - the same one I flagged in the previous comment.

Symptom

> Task :grails-data-hibernate5-core:compileGroovy FAILED
HibernateConnectionSourceSettings.java:89: error: clone() in HibernateSettings cannot override clone() in HashMap
@groovy.transform.Generated() public  ...HibernateSettings clone() throws java.lang.CloneNotSupportedException { return null; }
overridden method does not throw CloneNotSupportedException

Root cause

Groovy 6's Java stub generator now unconditionally emits throws java.lang.CloneNotSupportedException on the @Generated clone() stub for any @AutoClone target, even when the actual parent's clone() doesn't declare that exception. HibernateSettings extends LinkedHashMap<String, String> and LinkedHashMap.clone() drops the throws clause, so javac rejects the stub as not a valid override.

I confirmed by trying @AutoClone(style = AutoCloneStyle.COPY_CONSTRUCTOR) first - same broken stub, same javac error - so this is in the stub generator, independent of the @AutoClone style.

Fix in 4a518983a2

Defined clone() explicitly on HibernateSettings. @AutoClone short-circuits its own clone() generation when a user-supplied clone() exists, so the stub generator emits a stub matching the user-defined no-throws signature. Body mirrors @AutoClone(style = CLONE) to preserve deep-clone of the typed nested settings (OsivSettings, CacheSettings, FlushSettings, additionalProperties) - this matters for the getSettings().clone() call at HibernateDatastore.java:597 in createTenantConnectionSource, which expects multi-tenant settings instances to be properly isolated rather than sharing nested mutable state.

Local verification on Java 21 / Groovy 6.0.0-SNAPSHOT build #546

./gradlew :grails-data-hibernate5-core:compileGroovy --rerun-tasks  -> BUILD SUCCESSFUL
./gradlew :grails-data-hibernate5-core:codeStyle                    -> BUILD SUCCESSFUL
./gradlew :grails-data-hibernate5-core:test --tests \
    'org.grails.orm.hibernate.connections.HibernateConnectionSourceSettingsSpec'
                                                                    -> 1 tests, 1 successes, 0 failures

Code style status

./gradlew codeStyle was already green on the 5 modules touched by 2a5e983555 (datamapping-validation, datamapping-core, gsp-core, views-core, geb). Re-ran on grails-data-hibernate5-core after this fix - also green. The CI failure was pure compile-error, not a code-style violation.

Next steps

This stub-generator regression should be filed upstream against apache/groovy with a minimal reproducer (Groovy class with @AutoClone + extends LinkedHashMap + a Java consumer in the same compilation unit). The workaround in HibernateConnectionSourceSettings.groovy is documented inline and explicitly marked removable once upstream lands the stub-generator fix.

PR description updated to add this entry to "Real Groovy 6 regressions, no upstream PR yet (need to be filed)".

@jamesfredley

Copy link
Copy Markdown
Contributor Author

2026-05-04 audit pass against Groovy 6.0.0-SNAPSHOT build #571

Pulled apache/groovy master to commit 40499016 (HEAD as of 2026-05-03 18:03 UTC) and the published 6.0.0-SNAPSHOT snapshot at build #571 (6.0.0-20260503.181740-571). Three new master commits unlock additional workaround removals (two on this canary, two more inherited via the merge from grails8-groovy5-sb4).

New Groovy 6 fixes since the previous audit (build #546#571)

Ticket Master commit (UTC) Workaround dropped on this canary
GROOVY-11980 ced726ce (2026-05-02 21:29) HibernateConnectionSourceSettings.HibernateSettings.clone() explicit override removed - @AutoClone(style = CLONE) on a LinkedHashMap-extending class now generates the override with the correct (no-throws) signature.
GROOVY-11982 88ca738c (2026-05-02 23:16) IContainerGebConfiguration reverted from trait back to interface with default methods. The downstream IncompatibleClassChangeError: Method '...$getCallSiteArray()' must be InterfaceMethodref constant under -PgrailsIndy=false is fixed at the bytecode level.
GROOVY-11983 af95d66d (2026-05-03 01:25) Inherited from grails8-groovy5-sb4 via the merge: PersistentEntityCodec two ManyToMany.isAssignableFrom swaps + DefaultHalViewHelper ToOne-first/ToMany-second cascade reorder, both reverted to natural instanceof form.

Local compilation against build #571 (Java 21):

./gradlew :grails-data-hibernate5-core:compileGroovy --refresh-dependencies
./gradlew :grails-geb:compileTestFixturesGroovy --refresh-dependencies

Both BUILD SUCCESSFUL.

Runtime validation deferred to CI

The GROOVY-11982 fix is at the bytecode Methodref vs InterfaceMethodref constant-pool emission layer. The compile path is happy on both old and new bytecode shapes - the failure is at class-loading time when a downstream class compiled with -PgrailsIndy=false consumes the interface. The affected specs (InheritedConfigSpec, ChildPreferenceInheritedConfigSpec in grails-test-examples-geb) extend ContainerGebSpec implements IContainerGebConfiguration and exercise the exact $getCallSiteArray() dispatch the upstream fix addresses. The canary CI matrix (Functional Tests (Java 21/25, indy=false)) is the right gate for that - rather than re-running the geb integration test locally with a Selenium container, leaving it for the CI run on this push.

The GROOVY-11980 fix is a Java stub generator change. Compilation succeeds locally, so the stub for HibernateSettings no longer carries the bogus throws CloneNotSupportedException. Runtime deep-clone semantics for tenant connection-source settings (the original reason for the explicit clone() body) are preserved by @AutoClone(style = CLONE), which is the default style and produces the exact same per-field clone shape the explicit override implemented manually.

Remaining real Groovy 6 regressions (no upstream PR yet)

Re-verified failing against build #571 by reverting locally:

Each has a deterministic standalone reproducer on Groovy 6.0.0-SNAPSHOT build #571. None has been filed upstream yet - will get tickets opened against apache/groovy over the next couple of pushes.

Pre-existing canary issue (independent)

:grails-fields:compileGroovy fails on the merged tree with Target constructor for constructor call expression hasn't been set in BeanPropertyAccessorFactory.groovy:83. Reproduces on the unmodified canary - independent of the workaround removals. Will track separately and not let it block this audit pass.

Net effect

Five workarounds dropped against this round of upstream fixes: 2 directly on this canary (GROOVY-11980, GROOVY-11982), 2 inherited from #15557 via merge (GROOVY-11983 × 2), and the ContainerSupport / constraint factory / parallelism workarounds removed in the previous audit cycle (GROOVY-11968 / GROOVY-11967 / GROOVY-11966). PR description has been refreshed to reflect the current state.

cc @jdaugherty for visibility on the canary progress; @paulk-asert if any of the remaining no-upstream-PR-yet items would benefit from a JIRA ticket sooner rather than later.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Burn-down pass: 2026-05-08, against Groovy 6.0.0-SNAPSHOT build #609

Diff vs the previous push (canary bd7a30ae -> 3cbd88b1):

  1. Pulled Groovy 5.0.x support for Grails 8 + Spring Boot 4 #15557 forward into this canary (f8bb2829). Brings in the final Groovy 5 audit work that landed on grails8-groovy5-sb4 between 2026-05-04 and 2026-05-08:

    • 813b1316 Bump javaparser-core to 3.28.1 to align with Groovy 5.0.6-SNAPSHOT
    • 43ad57a2 Final Groovy 5 audit pass: clean up silent File-truthiness traps + stale JIRA reference
    • faef56cf render(Map) workarounds: align inline diagnoses with File-truthiness root cause
    • 65d194f4 Restore IContainerGebConfiguration as interface - GROOVY-11982 fixed in 5.0.6
    • b47917c1 / d48be122 forge dockerBuildNative class-initialization fixes
    • 8f711231 Merge back 8.0.0-M1 (release v8.0.0-M1, JDK 21+ minimum, JDK 26 added, testcontainers 2.x in forge generated apps)

    Merge was clean, conflicts resolved by ort strategy with no manual intervention. dependencies.gradle kept the groovy.version: '6.0.0-SNAPSHOT' pin (the base bumped its groovy.version to 5.0.6-SNAPSHOT; this canary stays on 6.0.0-SNAPSHOT and inherits everything else).

  2. Diff'd apache/groovy master 40499016..bc4caccc (audit window 2026-05-03 18:03 UTC -> 2026-05-08 19:32 UTC, 30+ commits) and mapped each commit onto the canary's open and closed workaround inventory. Three upstream fixes mapped:

    • GROOVY-11986 "genericGetMethod registration too permissive: matches any get(X) where X is a supertype of String" - apache/groovy 999f6dcd + a4caaa4b. Removable (was the open canary-only Groovy 6 workaround).
    • GROOVY-11980 and GROOVY-11982 - already removed in bd7a30ae last push.

    The rest of the upstream window is dependency bumps (jline, jackson, javaparser), test-infrastructure work (@ForkedJvm, @ExpectedToFail extensions on groovy-test-junit6), and unrelated language work (intersection types GROOVY-11998 parts 1-5, serializable method references GROOVY-11993, GROOVY-11999 ProxyGeneratorAdapter NPE on mixed classloaders, GROOVY-11994 groovy.val.enabled flag, GROOVY-11996 test-only follow-up to the 5.0.6 groovy.truth.file.exists.enabled flag, GROOVY-11988 {@inheritDoc} for external JDK classes, GROOVY-11987 groovydoc CLI fix, GROOVY-11995 groovyc ant task system properties).

  3. Dropped the GormEntityTransformation per-entity AST Object get(String) shim (3cbd88b1). Three deletions:

    • The 26-line AST instanceGetBody block in GormEntityTransformation.applyTransformation (lines 295-320 of the previous tree).
    • The 18-line stale doc comment on GormEntity.get(Serializable) describing the now-resolved Groovy 6 dispatch hijack.
    • The 'test Groovy 6 generic-getter instance-dispatch guard' regression test in GormEntityTransformSpec (lines 220-235). It only verified that the AST shim was added - so it has no meaning once the shim is gone. The actual dispatch behaviour is gated by the integration suites that originally surfaced the regression (DataServiceConnectionRoutingSpec, CrossLayerMultiDataSourceSpec in the Hibernate5 / Functional / Mongodb matrices).

    Total: 3 files, 59 deletions.

Local verification (JDK 21, against the cached 6.0.0-SNAPSHOT publication 20260508.194756 = build #609):

./gradlew :grails-datamapping-core:compileGroovy   BUILD SUCCESSFUL
./gradlew :grails-datamapping-core:test            BUILD SUCCESSFUL

Full integration validation (Hibernate5, Functional, Mongodb under both -PgrailsIndy=false and -PgrailsIndy=true) is deferred to the canary CI matrix on this push.

Standing position after this push: zero Groovy-6-only workarounds remain on this canary. The five remaining workarounds in the description are all inherited from #15557 and reproduce identically on Groovy 5.0.6-SNAPSHOT and on Groovy 6.0.0-SNAPSHOT. GROOVY-11985 (Validateable trait-static dispatch) is the only one with an open upstream ticket; the other four ("VariableScopeVisitor NPE in canonicalisation", "indy=false controller-parameter scope loss", "ConfigurationBuilder + AbstractConstraint static-init", "GROOVY-6362/GROOVY-11817 g taglib regression") need standalone reproducers extracted before they can be filed.

cc @paulk-asert - thanks for GROOVY-11986; verified clean on build #609.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

2026-05-13 audit pass against Groovy 6.0.0-SNAPSHOT build #645

Pulled apache/groovy master to commit 0a04376328 ("try to make JMX tests more resilient", 2026-05-13 13:20 UTC) and the published 6.0.0-SNAPSHOT snapshot at build #645 (6.0.0-20260513.133635-645).

Snapshot audit window (build #609 -> build #645)

Diff'd apache/groovy master bc4caccca6..0a04376328 (audit window 2026-05-08 19:42 UTC -> 2026-05-13 13:20 UTC). No upstream fixes in this window map onto a workaround on this canary. The window contents:

  • AI-readiness skills, javadoc/package-info additions
  • GROOVY-12001 jline 4.1.0 (already in last audit baseline)
  • GROOVY-12002 MarkdownSlurper support in groovysh
  • GROOVY-12003 /img command in groovysh
  • GROOVY-12004 grape command line maven/ivy shorthands
  • GROOVY-12005 Grape cache-corruption / CDN hardening
  • GROOVY-12006 Gradle 9.5.x bump (build)
  • GROOVY-12007 log4j2 2.26.0 bump (test dependency)

None overlap with the remaining workaround inventory.

Brought forward from grails8-groovy5-sb4 (merge d3384e9395)

  • 0ce8095700 Fix dbmigration GroovyChangeLogSpec: drop env-dependent log-capture assertions
  • 9b048e177a Restore micronaut-jackson-databind for grails-forge-web-netty JSON runtime
  • The 5.x audit work in bda52ad1bb (8.0.x merged back into grails8-groovy5-sb4)

Conflict-free merge (dependencies.gradle kept groovy.version: '6.0.0-SNAPSHOT' on this canary).

Standing position

Zero Groovy-6-only workarounds remain on this canary. The five remaining workarounds in the description are all inherited from #15557 and reproduce identically on Groovy 5.0.6-SNAPSHOT and on Groovy 6.0.0-SNAPSHOT build #645. GROOVY-11985 is the only one with an open upstream ticket and now has a candidate fix in apache/groovy#2529 - validated end-to-end on this canary below.


Validation of apache/groovy#2529 (Paul King, GROOVY-11985)

Tested in two independent layers:

Layer 1: standalone reproducer

The standalone reproducer at jamesfredley/groovy-trait-static-method-override-bug (the minimal extract of the Validateable site) was run against both versions side-by-side, JDK 21:

Build Test 1 (direct call) Test 2 (this.defaultNullable() from trait body) Test 3 (reflection workaround)
Apache snapshot 6.0.0-20260513.133635-645 (master HEAD, NO PR #2529) PASS FAIL - override hijacked back to trait helper default PASS
Local build of apache/groovy#2529 HEAD 9115a3de rebased on master PASS PASS - override seen by trait body PASS
$ ./gradlew run -PgroovyVersion=6.0.0-20260513.133635-645
Test 2: trait body sees `this.defaultNullable()` from inside the trait
  result: false   (expected: true if override is honoured, false if hijacked by trait helper)
  FAIL - override hijacked back to trait helper default

$ ./gradlew run -PgroovyVersion=6.0.0-SNAPSHOT  # mavenLocal, post-PR-2529 build
Test 2: trait body sees `this.defaultNullable()` from inside the trait
  result: true   (expected: true if override is honoured, false if hijacked by trait helper)
  PASS - override seen by trait body

Layer 2: real Grails Validateable workaround removed, validation tests run

Removed the resolveDefaultNullable(Class<?>) reflection shim from grails-validation/.../Validateable.groovy and reverted both call sites to direct unqualified defaultNullable() calls (the natural shape the trait was originally written in):

-            boolean isDefaultNullable = resolveDefaultNullable(this)
+            boolean isDefaultNullable = defaultNullable()
 ...
-        boolean isDefaultNullable = resolveDefaultNullable(this.class)
+        boolean isDefaultNullable = defaultNullable()
 ...
-    private static boolean resolveDefaultNullable(Class<?> clazz) {
-        ... // 18-line reflective dispatch helper removed
-    }

Then republished the local Groovy build as the unique version 6.0.0-PR2529-SNAPSHOT (so Gradle resolution couldn't fall back to the published Apache snapshot of plain 6.0.0-SNAPSHOT) and pointed dependencies.gradle at that version. Ran the full ValidateableTraitSpec suite:

./gradlew :grails-validation:compileGroovy :grails-validation:test \
    --tests grails.validation.ValidateableTraitSpec --rerun-tasks --no-daemon

Confirmed via build log that the test was executing against 6.0.0-PR2529-SNAPSHOT:

Executing Spock 2.4.0-groovy-5.0 with NOT compatible Groovy version 6.0.0-PR2529-SNAPSHOT

Result: 14 of 14 ValidateableTraitSpec tests PASS (1m 41s, BUILD SUCCESSFUL), including the two that fail without Paul's fix when the workaround is removed:

Test Without workaround, against build #645 (no fix) Without workaround, against PR #2529
Test that constraints are nullable by default if overridden and ensure nullable:true constraint is not applied when no other constraints were defined by user FAIL - constraints.size() == 3 got 4, stray name:nullable:true from override-not-seen PASS
Test that properties defined in a class with overridden defaultNullable which are not explicitly constrained are not accessed during validation FAIL - UnsupportedOperationException: getName() should not have been called during validation, override-not-seen forced unconstrained-property access PASS
Other 12 ValidateableTraitSpec cases PASS PASS

Conclusion

apache/groovy#2529 fully resolves the GROOVY-11985 workaround on this canary. Once it merges to apache/groovy master and a snapshot publishes containing the fix, the Validateable.resolveDefaultNullable(Class<?>) reflection shim can be removed and both call sites can revert to plain defaultNullable(). The local validation edit, the unique-version Groovy republish, and the dependencies.gradle pin were all reverted before this comment; the canary tree is back at d3384e9395.

Thanks @paulk-asert - this clears the only one of the five remaining inherited workarounds that had an open upstream ticket. The other four (VariableScopeVisitor canonicalisation NPE, indy=false controller-parameter scope loss, ConfigurationBuilder + AbstractConstraint static-init, GROOVY-6362 / GROOVY-11817 g taglib regression) still need standalone reproducers filed against apache/groovy.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

2026-05-20 burn-down audit against Groovy 6.0.0-SNAPSHOT build #692

Per-cycle audit on the upgraded snapshot baseline.

Snapshot signals

  • apache/groovy master HEAD: a2ce6f02 "minor refactor: remove javadoc warning" (2026-05-20 15:04 UTC). 59 commits ahead of the prior audit baseline 0a04376328. None of those commits touch the call sites of the 5 inherited workarounds (VariableScopeVisitor, ControllerActionTransformer, ConfigurationBuilder / AbstractConstraint, g-taglib STC extension, TraitReceiverTransformer). Notable adjacent work: bfa50cd0 "STC: fix derived and interface checks for union types" - inspected, addresses union-type assignability, not the unresolvedProperty / node-identity issue behind workaround 5.
  • Latest published snapshot: build Grails 3.0.1 CLI won't start too #692 timestamp 2026-05-20 15:17:43 UTC (groovy-6.0.0-20260520.151743-692.jar).
  • apache/groovy#2529 (candidate fix for GROOVY-11985): still OPEN, not merged.
  • Spock for Groovy 6: still does not exist on Maven Central or Sonatype snapshots. The -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true bridge remains required.

Workaround burn-down attempts on #692

Each workaround was removed locally (reverting to the pre-Groovy-5 shape) and the targeted test was run.

# Workaround removed Test command Outcome Verdict
1 Validateable.resolveDefaultNullable(Class) reflection bypass - 2 call sites + private helper deleted :grails-validation:test --tests "grails.validation.ValidateableTraitSpec" 2 FAILED: Test that constraints are nullable by default if overridden ..., Test that properties defined in a class with overridden defaultNullable ... Workaround still required. GROOVY-11985 / apache/groovy#2529 path.
2 All 4 VariableScopeVisitor guards (GrailsASTUtils.processVariableScopes try/catch, AstUtils.processVariableScopes try/catch, AbstractMethodDecoratingTransformation null-scope fallback + dummy-SourceUnit try/catch, ResourceTransform setVariableScope(new VariableScope())) :grails-datamapping-tck:compileGroovy FAILED: BUG! exception in phase 'canonicalization' in source unit '.../DataServiceRoutingProductDataService.groovy' unexpected NullPointerException Workaround still required. Same shape as prior audits.
3 gradle/boot4-disabled-integration-test-config.gradle apply on 5 projects Not surgically attempted in this cycle (integration tests with both indy modes are too expensive locally). No upstream Groovy 6 commit in the 59-commit delta touches ControllerActionTransformer-adjacent dispatch. n/a Workaround assumed still required until contradicted by a CI matrix run.
4 AbstractConstraint.getDefaultMessageFromBundle (the static-init order half of workaround 4); ConfigurationBuilder handleConverterNotFoundException not attempted :grails-validation:test :grails-datamapping-validation:test All tests passed locally with the AbstractConstraint fallback removed, BUT the underlying interface static-init order regression is a runtime-bootstrap issue not exercised by the unit-test suite. Re-applied the workaround pending a test that actually triggers the production bootstrap path. Workaround kept defensively; needs a dedicated reproducer before it can be safely deleted.
5 All 5 @IgnoreIf({ instance.isGroovy5OrLater() ... }) annotations in GspCompileStaticSpec :grails-gsp-core:test --tests "org.grails.gsp.GspCompileStaticSpec" 5 FAILED: 3 should support message tag invocation [gDotPrefix: true] parametric iterations + should fail compilation when using invalid property + should fail compilation when calling method on invalid property Workaround still required. GROOVY-6362 / GROOVY-11817 regression persists on 6.0.0-SNAPSHOT #692.

Net result: 0 of 5 workarounds removable on Groovy 6.0.0-SNAPSHOT #692. All five fire identically to the prior audit baseline.

Branch hygiene

The base branch (grails8-groovy5-sb4) advanced while this canary was audited (#15557 picked up the compile-static test app from #15294 and pinned Groovy to released 5.0.6). This canary now carries both:

  • The grails-test-examples/compile-static project from test - #15290 - add test for compile static dynamic checking #15294, exercising the GROOVY-11817 dynamic-finder-under-@GrailsCompileStatic happy path on the canary's Groovy 6.
  • The released-5.0.6 pin in dependencies.gradle from the base was rejected during the merge in favour of this branch's 6.0.0-SNAPSHOT pin (intentional - this PR remains a Groovy 6 canary).

Merge commit: 0ea4f26.

CI is the authoritative next signal.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Groovy 6 Verifier regression on grails-views-gson:compileGroovy - investigation summary

The merge brought the canary forward past the previously-blocking grails-data-graphql-core:compileGroovy failure (now resolved on sb4 and rolled up in 7156ed8e3a). The next failure that surfaces is a brand-new Groovy 6 Verifier regression in grails-views-gson that does not exist on Groovy 5.

Confirmation: same source, two outcomes

The file grails-views-gson/src/main/groovy/grails/plugin/json/view/api/internal/DefaultGrailsJsonViewHelper.groovy is bit-identical between grails8-groovy5-sb4 (68fe246bef, latest sb4 commit) and grails8-groovy6-canary (HEAD):

SHA256 sb4    = 83272A2B5083578D96B8E653ED4310DAEB17CDD13AA5E2E4C1EF9D49DAFE1C9B
SHA256 canary = 83272A2B5083578D96B8E653ED4310DAEB17CDD13AA5E2E4C1EF9D49DAFE1C9B

The actual error

Can't have an abstract method in a non-abstract class. The class
'grails.plugin.json.view.api.internal.DefaultGrailsJsonViewHelper'
must be declared abstract or the method
'grails.plugin.json.builder.JsonOutput$JsonWritable render(java.util.Map)'
must be implemented.

5 errors, one per overload: render(Map), render(Object, Map, Closure), render(Object, Map), render(Object), render(Object, Closure).

All 5 methods are declared explicitly on the class with matching signatures. They are also reachable via the render(Object, Map = ..., Closure = ...) default-argument form at line 348. Groovy 6 ignores both forms and reports them as unimplemented.

Fresh-cache confirmation

Tested against org.apache.groovy:groovy:6.0.0-SNAPSHOT build #700 (6.0.0-20260522.234755-700) after wiping ~/.gradle/caches/modules-2/files-2.1/org.apache.groovy and re-running with --refresh-dependencies --rerun-tasks. The freshly-downloaded jar resolved to the maven-metadata.xml-published build #700 (latest as of 2026-05-22 23:47 UTC). Bug reproduces unchanged. None of the 12 apache/groovy master commits since 2026-05-20 (a2ce6f02..3cbd88c4a5) touch the Verifier or @CompileStatic abstract-method-implementation path.

Workaround attempts (all REJECTED)

# Attempt Result
1 Add 5 explicit @Override render(...) forwarders for the default-arg form Same 5 errors
2 Fully qualify return type to grails.plugin.json.builder.JsonOutput.JsonWritable at every site Same 5 errors
3 Rename the inner class JsonOutput.JsonWritable -> JsonOutput.GrailsJsonWritable (eliminates the name shadowing with groovy.json.JsonOutput.JsonWritable) Same 5 errors with the renamed type
4 Remove @CompileStatic from DefaultGrailsJsonViewHelper Same 5 errors. Confirms the bug is at the Verifier layer, not the STC.
5 Remove @CompileStatic from GrailsJsonViewHelper interface Same 5 errors
6 Replace @InheritConstructors with an explicit DefaultGrailsJsonViewHelper(GrailsView) constructor Same 5 errors when other annotations are also present
7 Mark DefaultGrailsJsonViewHelper abstract + create concrete subclass ConcreteGrailsJsonViewHelper extends DefaultGrailsJsonViewHelper Suppresses bug on the abstract parent but fires identically on the concrete subclass - the Verifier check is per-class
8 Mark the concrete subclass @CompileDynamic Same 5 errors. Confirms the bug ignores @CompileDynamic.
9 Remove extends GrailsViewHelper from GrailsJsonViewHelper (breaks the diamond inheritance of GrailsViewHelper between the parent class chain and the interface chain) Only superficially suppresses the bug. The build then halts on 2 STC errors in DefaultHalViewHelper (viewHelper.link(Map) is no longer reachable through the interface). Once those are fixed (e.g. via ((GrailsViewHelper) viewHelper).link(...) casts), the abstract-method bug re-fires on DefaultGrailsJsonViewHelper. The diamond-removal does not actually fix anything - it just delays the bug until the build progresses past the link calls.
10 All of #9 PLUS remove the covariant getG() override from the JsonView trait (so it inherits the parent trait's GrailsViewHelper getG()) Same outcome as #9 - bug re-fires once the build progresses

Annotation-isolation matrix

With every other change reverted to baseline and one annotation at a time on the class header:

Class annotations Result
@CompileStatic @InheritConstructors @Slf4j (original) 5 errors
@InheritConstructors @Slf4j (no @CompileStatic) 5 errors
@CompileStatic @Slf4j + explicit constructor 5 errors
@CompileStatic alone + explicit constructor 2 STC errors (unrelated log undeclared - confirms abstract-method bug is gone with @Slf4j removed)
@Slf4j alone + explicit constructor 5 errors
@InheritConstructors alone 5 errors
No annotations + explicit constructor 5 errors
No annotations, no explicit constructor 1 error (missing constructor - compile aborts before the abstract-method check fires)

The matrix shows the abstract-method check fires on every class configuration that can compile far enough to reach the check. The bug is not gated by any annotation or transform - it is a fundamental Verifier defect for this inheritance shape on Groovy 6.

Minimal-reproducer status

Saved at groovy6-inner-abstract-class-stc-bug/ (will push to a public repo for upstream filing). The reproducer mirrors the structural pattern:

  • Java outer class with inner abstract class shadowing the parent's inner class (reproducer.JsonOutput.JsonWritable shadowing groovy.json.JsonOutput.JsonWritable)
  • @CompileStatic interface chain MyInterface extends ParentInterface extends LinkGenerator with 5 render(...) overloads returning the inner abstract class
  • Concrete class @CompileStatic @InheritConstructors MyImpl extends MyIntermediateBase extends MyBase implements MyInterface, ParentInterface (diamond)

The reproducer compiles cleanly on Groovy 6.0.0-SNAPSHOT build #700. Some additional element of the real grails-views-gson codebase is required to trigger the Verifier path; I have not isolated it yet. The grails-views-gson source itself is the working reproducer for now (open source, fully self-contained module).

Recommendation

Three paths forward, none ideal:

  1. File the upstream ticket with grails-views-gson itself as the reproducer; track as workaround Patch for GRAILS-6695 #6 (blocked, upstream-only) until the Groovy 6 release picks up the fix. CI stays red on this matrix entry.
  2. Pin the canary to an older Groovy 6 snapshot if there is a build prior to whichever one introduced the regression; needs a git-bisect across apache/groovy to find the offending commit.
  3. Disable :grails-views-gson:compileGroovy on the canary as a known-failing-quarantine entry. Loses regression coverage.

Happy to take direction on any of these. The four CI fixes in 7156ed8e3a (jansi@4.1.0 license, asm 9.10 BOM overrides, TemplateRenderer 5 forwarders, BeanPropertyAccessorImpl @MapConstructor) are mechanical and stand on their own; they remove 3 of the 4 distinct CI failure categories the canary was hitting before the merge.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Burn-down audit 2026-05-29 against Groovy 6.0.0-SNAPSHOT build #716

Pulled grails8-groovy5-sb4 into the canary (merge 4a63acadb8: Spring Boot 4.0.6, Gradle 9.5.1, Jackson 3 / mongodb 5.6.5 alignment; dependencies.gradle kept groovy.version: 6.0.0-SNAPSHOT) and re-audited every workaround against the latest published snapshot 6.0.0-20260527.104747-716 (master HEAD 2026-05-27, which now contains the GROOVY-12040 merge).

Removed this cycle

  • ConfigurationBuilder @Builder-detection heuristic (commit 5d3896d0f2). GROOVY-12040 (apache/groovy#2565, merged to master 2026-05-27) restores @Builder to @Retention(RUNTIME). The isLikelyBuilderType() heuristic + its three call-site disjuncts were only needed because Class.getAnnotation(Builder) returned null under the SOURCE-retention regression; detection now reverts to the pre-Groovy-5 getAnnotation(Builder) form. The Spring 7 Map-to-typed-config conversion fallbacks (handleConverterNotFoundException, handleConversionException) are independent of the Groovy version and are retained.

Already cleared via the base merge

  • g.taglib STC from @CompileStatic GSP (former workaround Some fixes #5) - resolved upstream-style by the GROOVY-12041 Grails-side change (GroovyPageTypeCheckingExtension matches the taglib namespace by name); now a real fix on the base, inherited here. The previous description's Some fixes #5 row is dropped.

Re-audited and KEPT (no upstream fix; fire identically on 5.0.7-SNAPSHOT and 6.0.0-SNAPSHOT #716)

  1. VariableScopeVisitor canonicalization NPE guards (GrailsASTUtils / AstUtils / AbstractMethodDecoratingTransformation).
  2. gradle/boot4-disabled-integration-test-config.gradle (indy=false controller-parameter scope loss + SiteMesh3/Spring 7).
  3. AbstractConstraint.getDefaultMessageFromBundle interface static-init-order fallback (the surviving, non-@Builder half of the old ConfigurationBuilder row; defensive, needs a standalone reproducer).
  4. Validateable.resolveDefaultNullable() reflection - GROOVY-11985 / apache/groovy#2529 still OPEN.

Groovy-6-only blocker - still red

  • DefaultGrailsJsonViewHelper.groovy:67 Verifier "abstract method in non-abstract class" regression: re-confirmed failing on build Grails 2.5.0: Using ContainerRenderer for JSON responds with 404 #716 (all 5 render(...) overloads). GROOVY-12040 does not touch the Verifier path. No upstream ticket yet; grails-views-gson remains the working reproducer and the canary's blocking CI category.

Net

The canary now carries one fewer workaround than the Groovy 5 base - the GROOVY-12040 @Builder fix is in master/6.0.0 (build #716) but not in GROOVY_5_0_X, so the canary drops a workaround that 5.0.x must keep. apache/groovy#2529 (the only other candidate) is still open; if it merges to master before GROOVY_5_0_X, the Validateable reflection shim becomes the next canary-only removal.

The CI matrix on this push is the authoritative gate; the grails-views-gson Verifier blocker is expected to stay red until upstream.

Assisted-by: claude-code:claude-4.8-opus

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Blocker #6 resolved - Groovy 6 Verifier abstract-method regression worked around (8b349bcdb8)

After re-baselining the canary on the updated grails8-groovy5-sb4 (merge 3538600648, which brings in the GROOVY-12040 @Builder removal), :grails-views-gson:compileGroovy was the sole remaining failure - the entire rest of the dependency chain compiles. It is now fixed.

Root cause (confirmed)

The diagnostic comes from org.codehaus.groovy.classgen.ClassCompletionVerifier.checkNoAbstractMethodsNonAbstractClass, which iterates ClassNode.getAbstractMethods() built from getDeclaredMethodsMap(), keyed by MethodNode.getTypeDescriptor() - and the descriptor includes the return type. On Groovy 6 the concrete leaf render(...) overrides in DefaultGrailsJsonViewHelper resolve a different descriptor for the inner-class return type grails.plugin.json.builder.JsonOutput.JsonWritable than the abstract render(...) entries inherited from the GrailsJsonViewHelper interface, so they never displace the abstract entries, which then survive and are reported "unimplemented" - for all 5 overloads, including the two declared explicitly.

A key catalyst: groovy.json.JsonOutput.JsonWritable was removed in Groovy 6 (the class now declares only JsonUnescaped). The Grails JsonOutput.JsonWritable shadowed it on Groovy 5; on Groovy 6 there is nothing to shadow, which changes how the inner-class return type resolves. This is a Verifier-layer defect, not the static type checker - it reproduces with @CompileStatic removed.

The fix (workaround #12, the first that works)

Declare the 5 GrailsJsonViewHelper#render(...) methods as default (concrete, throwing UnsupportedOperationException). Because the bug is specifically in the abstract-method check, making the methods non-abstract removes them from getAbstractMethods() entirely - the verifier has nothing to flag. DefaultGrailsJsonViewHelper is the sole implementor and overrides all 5, so the throwing default bodies are never reached.

The twelve earlier attempts that did not work (this PR's workaround-attempts comment lists the first ten): explicit forwarders, fully-qualified return types, inner-class rename, removing @CompileStatic from class and interface, explicit constructor, abstract-parent + concrete-subclass, @CompileDynamic, diamond removal, diamond + covariant-getG removal, and - new this round - concrete render stubs on the intermediate superclass DefaultJsonViewHelper (Oracle's first suggestion; it failed because the stub gets the same mismatched descriptor).

Verification (Groovy 6.0.0-SNAPSHOT build #716 / Gradle 9.5.1 / Spring Boot 4.0.6, JDK 21)

Task Result
:grails-views-gson:compileGroovy green
:grails-views-gson:test all pass (render / HAL / JSON-API / template-inheritance incl. g.render(..)); 1 pre-existing @IgnoreIf skip
:grails-views-gson:codeStyle green

The render-path tests exercise the real implementations, confirming the default bodies are never hit at runtime.

Standalone reproducer status

I built a faithful structural mirror (repro6b): a Java outer class extending groovy.json.JsonOutput with a shadowing inner JsonWritable, the @CompileStatic interface chain JsonViewHelper extends ViewHelper extends LinkGen, the diamond (DefaultJsonViewHelperBase extends DefaultViewHelper implements ViewHelper; Impl extends ... implements JsonViewHelper), default-argument render, mixed void inline(...), joint Java+Groovy compilation, precompiled-jar split, and anonymous JsonWritable subclass instances. It compiles cleanly on build #716 - i.e. none of those ingredients in isolation trigger the defect (matching the earlier finding). The real grails-views-gson module remains the working in-tree reproducer; isolating the last differentiating element into a dependency-free case is still open and will accompany the upstream Apache Groovy ticket.

Net

The canary now builds, tests, and style-checks end-to-end on build #716; the only remaining non-production crutch is the Spock disableGroovyVersionCheck bridge (still DRAFT/DO-NOT-MERGE until a Spock *-groovy-6.0 artifact ships). Inherited workarounds are unchanged (#1 Validateable / GROOVY-11985, #2 VariableScopeVisitor canonicalization, #3 boot4-disabled-integration-test-config, #4 AbstractConstraint static-init).

Assisted-by: claude-code:claude-4.8-opus

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Canary update: downstream fixes after the Verifier workaround - CI now fully green

Following the blocker-#6 comment (which covered 8b349bcdb8 and the base re-baseline), clearing the DefaultGrailsJsonViewHelper compile blocker let CI run the full matrix for the first time. That surfaced a short series of real Groovy 6 issues - all now fixed. No tests are skipped or rewritten, and the full CI matrix is green on 78b62c57e4.

Fixes (in order)

  1. SBOM JLine 4.1.0 license mapping (04b50fbf2f) - Groovy 6's groovy-groovysh pulls the JLine 4.1.0 family transitively, but SbomPlugin.LICENSE_MAPPING only mapped 4.0.12, so :grails-shell-cli:cyclonedxDirectBom failed with BSD-4-Clause (cyclonedx-core-java#205 mis-reports JLine's actual BSD-3-Clause). Because build depends on cyclonedxDirectBom, this broke every build-dependent job (Core/Forge Projects, Functional, Hibernate5, Mongodb). Added the nine 4.1.0 coordinates; verified cyclonedxDirectBom green on shell-cli, console, and test-core.

  2. compileGsonViews Spock-fork flag (7daec90baa) - the forked JsonViewCompiler (AbstractGroovyTemplateCompileTask) carries Spock's global AST transform on its classpath, which aborts under Groovy 6. It now propagates spock.iKnowWhatImDoing.disableGroovyVersionCheck to the fork (a no-op when the property is unset, so it is safe for released builds). Fixes :grails-test-examples-*:compileGsonViews.

  3. XmlUtils secure-slurper feature URIs (a092b13f2f) - the SAX/Xerces feature identifiers were declared with https:// (silently unrecognised, so every feature was dropped); under JDK 21/25 secure-processing then disallowed DOCTYPE entirely. Corrected the scheme to http:// and added the accessExternalDTD/accessExternalSchema JAXP properties so an inline DOCTYPE with internal entities parses while external entities throw. Fixes XmlUtilsSpec / TestHttpResponseSpec (103 tests green).

  4. WriteFilteringMap @Delegate mutation tracking (78b62c57e4) - the significant one. @Delegate on the overlap field also generated non-tracking put(Object,Object)/putAll/remove that competed with the class's tracking overrides. On Groovy 6 a mutation can dispatch to the generated delegate instead of the override, so the write lands in overlap but is never recorded in nestedDestinationMap. This silently dropped values from real .groovy external-config loading (ExternalConfigRunListenergetConfigProperty(...) returning null) - not just a test artifact. A plain-Groovy reproduction of the class works, which is why it only surfaced through the full config-merge path and Spock-compiled specs. Excluding the three overridden mutators from @Delegate leaves only the tracking overrides; fixes WriteFilteringMapSpec, ExternalConfigSpec, and MergedConfigSpec.

Spock: is a Groovy-6 build needed right now? No.

Investigated thoroughly. No Groovy-6-compatible Spock artifact exists anywhere (verified 2026-05-29): Maven Central tops out at 2.4-groovy-5.0, and the Sonatype Central snapshot repo (https://central.sonatype.com/repository/maven-snapshots) tops out at 2.5-groovy-5.0-SNAPSHOT. I tried switching to 2.5-groovy-5.0-SNAPSHOT - it resolved (today's build, via spock-bom) but produced the identical failures, because it is still a groovy-5.0 variant compiled against Groovy 5; reverted.

The key realization: the "weird runtime errors" the disableGroovyVersionCheck bridge warns about were, in every case here, genuine Groovy 6 production bugs (above) rather than Spock failing to compile specs. With those fixed, the bridge is sufficient and all specs run and pass. A real spock-*-groovy-6.0 artifact is still wanted eventually so the bridge can be dropped; the snapshot repo is already wired in settings.gradle (includeGroup('org.spockframework')), so the switch is a one-line spock.version bump when one ships.

CI

Full matrix green on 78b62c57e4 (Build Grails-Core / Functional / Hibernate5 / Mongodb / Forge across Java 21/25, ubuntu/macos/windows, indy on/off): 26 checks success, 0 failures. One macOS Build Grails-Core run failed first as an infrastructure flake - GitHub uploaded no logs for it and the identical step passed on ubuntu 21, ubuntu 25, and windows - and it passed on re-run.

The PR description has been trimmed to track only the remaining workarounds.

Assisted-by: claude-code:claude-4.8-opus

@jamesfredley

jamesfredley commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Verified against a real Groovy-6 Spock (spockframework/spock#2363)

Confirmed locally that this canary builds and its tests pass against a genuine Groovy-6 Spock build - i.e. the eventual exit path for the disableGroovyVersionCheck bridge (remaining workaround #1) works. Local verification only; nothing committed.

Setup

  • Built spock PR [Canary] Add Groovy 6 support spockframework/spock#2363 (groovy-6-canary, "[Canary] Add Groovy 6 support"; its variantsList now includes 6.0) and published to mavenLocal:
    ./gradlew -Dvariant=6.0 -DjavaVersion=21 :spock-core:publishToMavenLocal :spock-spring:publishToMavenLocal :spock-bom:publishToMavenLocal
    org.spockframework:{spock-core,spock-spring,spock-bom}:2.5-groovy-6.0-SNAPSHOT (built against groovy 6.0.0-alpha-1; grails-core overrides the runtime groovy to its 6.0.0-SNAPSHOT).
  • Temporarily enabled mavenLocal() and set spock.version = 2.5-groovy-6.0-SNAPSHOT (both reverted afterwards).

Results - both ran on Spock 2.5.0-groovy-6.0-SNAPSHOT (confirmed in the logs):

  • :grails-core:test - 309 unit Spock specs, BUILD SUCCESSFUL.
  • :grails-test-examples-external-configuration:test - full Grails app + Spock functional specs, BUILD SUCCESSFUL.

One caveat worth recording: the Executing Spock ... NOT compatible Groovy version 6.0.0-SNAPSHOT warning still prints, because Spock 2.5-groovy-6.0's version check treats the pre-release 6.0.0-SNAPSHOT as below its 6.0.0 minimum. So while this branch tracks a Groovy snapshot, the disableGroovyVersionCheck flag is still required even with the real Groovy-6 Spock; it should drop once we build against a Groovy 6.0.0 release (or once Spock's check accepts the snapshot). The transform itself is correct - all specs compile and pass.

Takeaway: once a spock-*-groovy-6.0 artifact is published, adopting it here is a one-line spock.version bump (the Sonatype Central snapshot repo is already wired in settings.gradle), and the suite stays green.

Assisted-by: claude-code:claude-4.8-opus

Bumps groovy.version to 6.0.0-SNAPSHOT (from 5.0.3) to see what breaks.
Snapshot resolves from https://repository.apache.org/content/groups/snapshots
which was already configured in build-logic/GrailsRepoSettingsPlugin.groovy
for the org.apache.groovy.* group.

Changes needed on top of the Groovy 5.0.3 canary:

- gradle/test-config.gradle: apply
  '-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true' to every
  GroovyCompile task, not just compileGroovy/compileTestGroovy.
  Spock 2.4-groovy-5.0 is the latest available and refuses to run
  against Groovy 6 without this flag; since SpockTransform is
  registered via META-INF/services, the Groovy compiler loads it for
  every source set (including main) and main compiles fail without
  the flag being set globally.

- DefaultHalViewHelper.groovy: reorder the (association instanceof
  ToMany && !(association instanceof Basic)) / else if
  (association instanceof ToOne) cascade to check ToOne first.
  Groovy 6's flow typing narrows 'association' in the else branch in
  a way that conflicts with the later 'instanceof ToOne' check
  (Incompatible instanceof types: Basic and ToOne). The reordered
  form is equivalent because ToOne and ToMany are sibling Association
  subtypes.

- AbstractHibernateGormInstanceApi.groovy: fix a pre-existing
  operator-precedence bug caught by Groovy 6's stricter instanceof
  type checking.
    before: if (association instanceof ToOne && !association instanceof Embedded) {
    after:  if (association instanceof ToOne && !(association instanceof Embedded)) {
  Without the parentheses '!association' is evaluated first (to a
  boolean) and then 'instanceof Embedded' is checked against a
  boolean, which is always false - the whole left side of the && had
  been dead code. Groovy 6 now reports this as
  'Incompatible instanceof types: boolean and Embedded'.

Known still-failing: grails-geb:compileTestFixturesGroovy still
triggers the ASM Frame.putAbstractType bug that was the reason we
pinned to Groovy 5.0.3. Same bytecode-generation issue carries
forward to 6.0.0-SNAPSHOT.
Groovy 6.0.0-SNAPSHOT generates invalid bytecode for constructors that
use a default-valued List parameter inside @CompileStatic classes.
Decompiled stack frames show Object where ArrayList is expected:

  Type 'java/lang/Object' (current frame, stack[4]) is not assignable
  to 'java/util/ArrayList'
  at DefaultConstraintFactory.<init>(Class, MessageSource):V

This breaks every validateable. At runtime VerifyError is raised the
first time the default-parameter overload is constructed, which cascades
into Validateable.validate(), grails-datastore-core bean wiring, and
any test that exercises constraints.

Workaround: replace the default-parameter signature with two explicit
constructors (the 2-arg one delegates to the 3-arg one with
[Object.class] as List<Class>). This is compilation-compatible - users
were already allowed to construct with or without the targetTypes arg.
Add spock.iKnowWhatImDoing.disableGroovyVersionCheck to all shared
test configs (hibernate5, mongodb, mongodb-forked, functional) via
tasks.withType(GroovyCompile).configureEach. The flag was only in
test-config.gradle, so modules using other configs failed with
IncompatibleGroovyVersionException on Groovy 6.

In functional-test-config.gradle, replace the per-task-name flags
with the configureEach pattern to also cover
compileIntegrationTestGroovy and other custom source sets.

Add CycloneDX license override for org.jline/jansi@4.0.7 (BSD-3-Clause)
which is pulled in by Groovy 6.0.0-SNAPSHOT's jline dependency upgrade.

Assisted-by: Claude Code <Claude@Claude.ai>
jamesfredley and others added 16 commits July 6, 2026 04:20
Keep the Micronaut island out of the default build graph when the root build targets a different Groovy major, while keeping the Hibernate 5 Micronaut BOM aligned to the island's Groovy 5 dependency set.

Assisted-by: opencode:openai/gpt-5.5
Avoid static-context getClass() package lookups in the global Grails transform and expose the form content type from HttpClientSupport for merge-ref integration specs.

Assisted-by: Hephaestus:gpt-5.5
Wait for the Process wrapper to observe termination before asserting liveness after RunningApplicationProcess.stop reports STOPPED. This avoids a Windows CI race where ProcessHandle termination completed before Process.isAlive() observed the exit.

Assisted-by: opencode:gpt-5.5
Bring the Groovy 6.0.0-SNAPSHOT canary up to date with 8.0.x.

Conflict resolution:
- dependencies.gradle: keep groovy.version=6.0.0-SNAPSHOT and the
  grails-hibernate5-micronaut-bom Groovy 5 pin; adopt 8.0.x's Jackson
  management (jackson2 2.21.5 / jackson3 3.1.5 security overrides) and drop
  the canary's stale jackson 2.22.0 pins and jacksonBomDependencies block.
  This also clears the validateDependencyVersions failure, since 8.0.x now
  BOM-manages commons-codec at 1.22.0 (matching the transitive resolution).
- RunningApplicationProcessSpec: take 8.0.x's Windows-reaper-aware stop
  assertion.

Assisted-by: claude-code:claude-opus-4-8
Address failures surfaced after merging 8.0.x into the Groovy
6.0.0-SNAPSHOT canary:

- dependencies.gradle: manage the com.fasterxml Jackson 2 suite at 2.22.0.
  groovy-yaml 6.0.0-SNAPSHOT transitively pulls jackson-dataformat-yaml
  (dragging core/databind/datatype-jsr310) at 2.22.0 and jackson-annotations
  at 2.22, so the BOM must manage each at >= the resolved version (rule 14).
  2.22.0 is newer than the security-pinned 2.21.5 so it keeps the
  CVE-2026-54515 fix. Clears the validateDependencyVersions failure.

- GlobalGrailsClassInjectorTransformation: call cache.get(key) instead of the
  cache[key] subscript. Under @CompileStatic the subscript binds to
  DefaultGroovyMethods.getAt(Map, String), removed in Groovy 6, so the AST
  transform threw NoSuchMethodError while compiling every artefact - the root
  cause cascading across the spring-security, redis and hibernate7 build jobs.
  The withDefault wrapper overrides get(), preserving the lazy injector lookup.

- TestFormParamsControllerSpec: drop the redundant static FORM constant; it now
  inherits getFORM() from the HttpClientSupport trait. Groovy 6 rejects a static
  getFORM() alongside the trait's instance getFORM().

Assisted-by: claude-code:claude-opus-4-8
Replace version-specific JLine 4 license overrides with a constrained Maven JAR PURL matcher and cover its scope with regression tests.

Assisted-by: opencode:gpt-5.6-sol
Use BeanConfiguration APIs for core bean properties and parent metadata so registration remains reliable under Groovy 6.

Assisted-by: opencode:gpt-5.6-sol
Avoid closure delegation during static helper assertions so the existing include and exclude coverage executes reliably with Groovy 6.

Assisted-by: opencode:gpt-5.6-sol
Qualify the outer request-attribute constant for Groovy 6 and cover cleanup through the public handler-chain lifecycle.

Assisted-by: opencode:gpt-5.6-sol
Canonicalize CRLF pairs before checking rendered GSP text so the newline contract is tested consistently across platforms.

Assisted-by: opencode:gpt-5.6-sol
Bring the Groovy 6 canary branch to the current 8.0.x tip while preserving canary-specific compatibility fixes.

Assisted-by: opencode:gpt-5.6-sol
Fail closed when Spring cannot convert nested settings maps so typos no longer silently apply defaults, and cover the regression with a focused test.

Assisted-by: opencode:gpt-5.6-sol
Keep the Groovy 6 canary BOM ahead of the Jackson 2.22.1 suite resolved through groovy-yaml and hibernate7-dbmigration so validateDependencyVersions stays green.

Assisted-by: opencode:gpt-5.6-sol
Move every synced wrapper, tooling API pin, sdkmanrc entry, and Forge template to the current Gradle 9.6.1 release.

Assisted-by: opencode:gpt-5.6-sol
@jamesfredley jamesfredley self-assigned this Jul 22, 2026
@jamesfredley jamesfredley added this to the grails:9.0.0-M1 milestone Jul 22, 2026
@jamesfredley
jamesfredley changed the base branch from 8.0.x to 9.0.x July 23, 2026 12:10
@jamesfredley jamesfredley changed the title [Canary] Grails 8 on Groovy 6.0.0-SNAPSHOT [Canary] Grails 9 on Groovy 6.0.0-SNAPSHOT Jul 23, 2026
Override Spring Boot's imported Groovy version property so the canary
fixture runs with the same Groovy version used to compile the framework.

Extend the functional test to cover both controller and GSP responses.

Assisted-by: opencode:gpt-5.6-sol codegraph
@jamesfredley jamesfredley moved this to In Progress in Apache Grails Jul 24, 2026
@jamesfredley

Copy link
Copy Markdown
Contributor Author

2026-08-02 refresh

Trunk sync

  • Merged 8.0.x8.1.x and pushed
  • Merged 8.1.x9.0.x (kept 9.0 version bindings) and pushed
  • Merged 9.0.x into grails8-groovy6-canary and pushed (dd0f4a8075)

Conflict resolutions on canary

  • dependencies.gradle: kept Groovy 6.0.0-SNAPSHOT + Jackson 2.22.1 / annotations 2.22; took trunk Log4j 2.25.5 and related security pins; aligned Hibernate5 Micronaut island Groovy pin to 5.0.8
  • end-to-end/spring-dependency-management: took end-to-end layout from trunk; kept canary ext['groovy.version'] override
  • GlobalGrailsClassInjectorTransformation: took full 9.0.x rewrite; retained only classInjectorCache.get(...) (Groovy 6 @CompileStatic Map subscript workaround)

Groovy 6 workaround review

Surveyed recent Groovy / Spock status (Groovy 6 still pre-release / alpha-2 + master; Spock has no groovy-6 artifact yet). No workarounds removed today without a canary proof build. Confident keep: Spock bridge, Micronaut island, Jackson 2.22, WriteFilteringMap excludes, CoreGrailsPlugin beans, URL mapping qualify, SBOM JLine, joint CI→master. Candidates for later retest only: JSON views default methods (GROOVY-10687), MapConstructor / GORM staticPropertyMissing.

PR hygiene

  • Description rewritten to current tip state only (no historical narrative)
  • PR is mergeable against 9.0.x again

Still DRAFT / DO NOT MERGE.

t and others added 3 commits August 2, 2026 16:46
Groovy 6 moved classic (non-invokedynamic) call-site bytecode generation out
of the core groovy jar into the optional org.apache.groovy:groovy-callsite
module (GROOVY-11158). The Grails Gradle plugin disables indy by default (see
issue #15293), so every Grails plugin module in this build - and every Grails
application - fails class generation with:

  BUG! exception in phase 'class generation' ... Classic call-site bytecode
  generation requires the optional org.apache.groovy:groovy-callsite module on
  the classpath. Either leave invokedynamic enabled (the default since Groovy 4)
  or add groovy-callsite. See GROOVY-11158.

caused by ClassNotFoundException on
org.codehaus.groovy.runtime.callsite.CallSiteArray.

Manage groovy-callsite in the BOM alongside the other Groovy modules and
declare it as an api dependency of grails-common, so it reaches the compile
and runtime classpath of the framework and of consuming applications.

Assisted-by: claude-code:claude-5-opus
…n-indy

Groovy 6 classic-callsite compilation of interface default methods emits a
reference to a synthetic CallSite holder (GrailsApplicationLifeCycle$1) that
is never packaged, so apps fail at boot with NoClassDefFoundError under
-PgrailsIndy=false. Java default methods compile to plain bytecode and work
with both indy on and off. Verified with latency integration tests and a new
unit spec.
@testlens-app

testlens-app Bot commented Aug 3, 2026

Copy link
Copy Markdown

🚨 TestLens detected 1 failed test 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI / Functional Tests (Java 21, indy=true) > :grails-test-examples-app1:integrationTest

Test Runs Flakiness
RedirectWithAndWithoutParamsFunctionalSpec > Params are not added to the url after a redirect even if they are passed to the redirect 1% 🟡

🏷️ Commit: 20e8ec7
▶️ Tests: 66053 executed
⚪️ Checks: 62/62 completed

Test Failures

RedirectWithAndWithoutParamsFunctionalSpec > Params are not added to the url after a redirect even if they are passed to the redirect (:grails-test-examples-app1:integrationTest in CI / Functional Tests (Java 21, indy=true))
Condition not satisfied:

pageSource.contains('"id":')
|          |
|          false
<html><head></head><body><form action="save" method="post">
    <input type="text" name="name">
    <input type="submit">
</form>
</body></html>

	at functionaltests.RedirectWithAndWithoutParamsFunctionalSpec.$tt__$spock_feature_1_0(RedirectWithAndWithoutParamsFunctionalSpec.groovy:41)
	at functionaltests.RedirectWithAndWithoutParamsFunctionalSpec.Params are not added to the url after a redirect even if they are passed to the redirect_closure1(RedirectWithAndWithoutParamsFunctionalSpec.groovy)
	at grails.gorm.transactions.GrailsTransactionTemplate$1.doInTransaction(GrailsTransactionTemplate.groovy:72)
	at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:137)
	at grails.gorm.transactions.GrailsTransactionTemplate.executeAndRollback(GrailsTransactionTemplate.groovy:69)
	at functionaltests.RedirectWithAndWithoutParamsFunctionalSpec.Params are not added to the url after a redirect even if they are passed to the redirect(RedirectWithAndWithoutParamsFunctionalSpec.groovy)

Muted Tests

Select tests to mute in this pull request:

  • RedirectWithAndWithoutParamsFunctionalSpec > Params are not added to the url after a redirect even if they are passed to the redirect

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

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

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants