Skip to content

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

Draft
jamesfredley wants to merge 60 commits into
grails8-groovy5-sb4from
grails8-groovy6-canary
Draft

[Canary] Grails 8 on Groovy 6.0.0-SNAPSHOT#15558
jamesfredley wants to merge 60 commits into
grails8-groovy5-sb4from
grails8-groovy6-canary

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

@jamesfredley jamesfredley commented Apr 5, 2026

Status

Canary / DRAFT - DO NOT MERGE. Layered on top of #15557 (Groovy 5 base); brings the framework up to Groovy 6.0.0-SNAPSHOT. As of 2026-05-29 the canary compiles, tests, and style-checks end-to-end on Groovy 6 with the full CI matrix green (build #716). It stays DRAFT only because of the temporary workarounds below - chiefly the Spock version-check bridge. No tests are skipped.

The per-cycle history (what was fixed/dropped and why) lives in this PR's comment thread; this description tracks only what currently remains.

Snapshot baseline

Component Version
Apache Groovy 6.0.0-SNAPSHOT (build #716, 6.0.0-20260527.104747-716; contains the GROOVY-12040 merge)
Spock 2.4-groovy-5.0 via the version-check bridge (no Groovy-6 Spock artifact exists yet)
Spring Boot 4.0.6 (Spring Framework 7.0.x)
Gradle 9.5.1
Jakarta EE 10
JDK 21+

Base PR

Stacked on #15557 - Groovy 5.0.x support for Grails 8 + Spring Boot 4 (grails8-groovy5-sb4), the authoritative source for the inherited workarounds' per-site analysis and reproducers. The base is merged forward regularly. The canary already carries one fewer Groovy-5 workaround: the @Builder-detection workaround is dropped here because GROOVY-12040 is in Groovy 6 / master but not yet backported to GROOVY_5_0_X.

Remaining workarounds

Each is a temporary measure to be removed once the corresponding upstream fix lands. Rows 3-6 are inherited from #15557 and fire identically on Groovy 5.0.7-SNAPSHOT and Groovy 6.0.0-SNAPSHOT.

# Workaround Removed when
1 Spock version-check bridge - -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true on every GroovyCompile/Test and on the forked gson/gsp view compiler (AbstractGroovyTemplateCompileTask) a spock-*-groovy-6.0 artifact ships - snapshot repo already wired in settings.gradle, so it is a one-line spock.version bump
2 GrailsJsonViewHelper's 5 render(...) declared default - sidesteps a Groovy 6 ClassCompletionVerifier false-positive (return-type-descriptor mismatch on the inner-class return type JsonOutput.JsonWritable); DefaultGrailsJsonViewHelper (sole implementor) overrides all 5 the upstream Groovy 6 Verifier regression is fixed (in-tree reproducer: grails-views-gson; analysis in the blocker-#6 comment)
3 Validateable.resolveDefaultNullable(Class) reflection-based dispatch GROOVY-11985 / apache/groovy#2529 (OPEN) lands; reproducer groovy-trait-static-method-override-bug
4 VariableScopeVisitor canonicalisation try/catch guards (GrailsASTUtils/AstUtils/AbstractMethodDecoratingTransformation) upstream ticket filed + fixed - BUG! exception in phase 'canonicalization' (not yet filed)
5 gradle/boot4-disabled-integration-test-config.gradle (5 grails-test-examples) controller-action param-scope loss under -PgrailsIndy=false and a SiteMesh3/Spring-7 decorator gap are resolved (passes under -PgrailsIndy=true)
6 AbstractConstraint.getDefaultMessageFromBundle MESSAGE_BUNDLE fallback the Groovy 5+ interface static-init-order regression is fixed (ConstrainedProperty.DEFAULT_MESSAGES initialises with null values; reproducer DefaultMessageResolutionSpec)

Build-infra note: SbomPlugin.LICENSE_MAPPING force-corrects the transitive JLine 4.x family to BSD-3-Clause (cyclonedx-core-java#205 mis-reports it as BSD-4-Clause) - a license-metadata correction, not a functional workaround.

CI status

Full CI matrix green on 78b62c57e4 - Build Grails-Core / Functional / Hibernate5 / Mongodb / Forge across Java 21/25, ubuntu/macos/windows, -PgrailsIndy=false and =true. The disableGroovyVersionCheck bridge keeps this DRAFT/DO-NOT-MERGE.

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.
@testlens-app

This comment has been minimized.

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>
…8-groovy6-canary

# Conflicts:
#	build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
…ORM entities

Groovy 6 registers GormEntity.get(Serializable) as the genericGetMethod
in MetaClassImpl, causing dynamic property access like Entity.name to
call get("name") instead of Class.getName(). This breaks all property
access on @entity classes that goes through Groovy's dynamic dispatch.

Root cause: Groovy 6 relaxed MetaClassImpl.isGenericGetMethod from
requiring get(String) to accepting get(Serializable), which matches
GormEntity's static get(Serializable) method. Confirmed by runtime
metaclass inspection showing genericGetMethod set to get(Serializable).

Fix: add a get(String) overload to GormEntity that intercepts the
genericGetMethod calls. When the argument matches a java.lang.Class
bean property (name, simpleName, etc.), it delegates to Class.class
metaclass. Otherwise it delegates to the GORM static API as before.

Also guard staticPropertyMissing with the same Class property check
for belt-and-suspenders coverage of the Groovy 6 property resolution
change.

Assisted-by: Claude Code <Claude@Claude.ai>
… is not initialized

When Groovy 6 calls get(String) as a genericGetMethod for property
resolution and GORM is not initialized, throw MissingPropertyException
instead of IllegalStateException. This matches the existing
staticPropertyMissing behavior and passes the GormEntityTransformSpec
test for unknown static properties.

Assisted-by: Claude Code <Claude@Claude.ai>
…rrides

Move spock.iKnowWhatImDoing.disableGroovyVersionCheck into the
build-logic CompilePlugin, which is applied to ALL modules. This
replaces the per-test-config additions and covers modules like
grails-datamapping-tck and grails-test-suite-base that don't
apply any shared test config.

Add CycloneDX BSD-3-Clause license overrides for all jline 4.0.7
artifacts pulled by Groovy 6 (builtins, console, console-ui,
native, reader, shell, style, terminal, terminal-jni).

Assisted-by: Claude Code <Claude@Claude.ai>
Change outputTagResult from private to protected in
AbstractGrailsTagTests - Groovy 6 restricts private method access
from nested closures.

Set spock.iKnowWhatImDoing.disableGroovyVersionCheck on Test tasks
(not just GroovyCompile) so runtime Groovy compilation inside tests
(e.g., BeanBuilder.loadBeans) doesn't trigger Spock's version check.

Restore try-catch in GormEntity.get(String) to convert
IllegalStateException to MissingPropertyException when GORM is not
initialized, matching staticPropertyMissing behavior.

Assisted-by: Claude Code <Claude@Claude.ai>
DataBindingTests: replace old-style Author.metaClass.static.get mock
with Spock GroovySpy. Groovy 6 changed MetaClass dispatch precedence
for trait-provided static methods, so dynamically-added MetaClass
closures no longer intercept calls to compiled trait methods.

grails-views-gson StreamingJsonBuilder ClassCastException: the Groovy
parent's call(Closure) creates groovy.json.StreamingJsonDelegate via
private cloneDelegateAndGetContent, but compiled .gson templates cast
the delegate to grails.plugin.json.builder.StreamingJsonDelegate.
Fix: override call(Closure) in the Grails StreamingJsonBuilder to use
the Grails delegate subclass, and fix JsonViewWritableScript.json() to
create Grails delegates directly instead of the Groovy parent type.

Assisted-by: Claude Code <Claude@Claude.ai>
Replace Object.class with Object in the constructor delegation call.

Assisted-by: Claude Code <Claude@Claude.ai>
…RM properties

When Groovy 6's genericGetMethod calls get(String) for property
resolution, GORM-managed properties like datasource qualifiers
(e.g., Book.moreBooks) were being treated as entity-by-ID lookups
instead of routing through staticPropertyMissing.

Fix: try staticPropertyMissing first (handles GORM property
resolution including datasource qualifiers and dynamic properties),
then fall back to get(Serializable) for entity-by-ID lookups.
This preserves both property resolution and data binding paths.

Assisted-by: Claude Code <Claude@Claude.ai>
Resolves three conflicts from upstream changes on the Groovy 5 base:

- dependencies.gradle: keep groovy.version=6.0.0-SNAPSHOT (PR purpose);
  drop the jackson.version override since base now relies on Spring Boot 4
  to manage Jackson.
- SbomPlugin.groovy: union the jline 4.0.7 entries (Groovy 6 transitively
  pulls these via groovy-groovysh) with base's updated jline 3.30.x
  entries, drop the stale jline@3.23.0 entry, and adopt base's more
  accurate "transitively via groovy-groovysh; main org.jline:jline pinned
  at 3.30.6 directly" comment style for both 3.30.x and 4.0.7 entries.
- GormEntity.groovy: improve the genericGetMethod regression docstrings
  to reference the actual upstream issue (GROOVY-11829) instead of a
  placeholder, document the dispatch flow on get(String), and explain
  why this guard is necessary. Auto-merge of staticPropertyMissing was
  already correct.

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

Three improvements driven by an architectural review of the Groovy 6
canary work and a fresh build that surfaced new SNAPSHOT-related issues.

1) SbomPlugin: introduce LICENSE_GROUP_MAPPING fallback (build fix)

The Groovy 6.0.0-SNAPSHOT just bumped its transitive jline pull from
4.0.7 to 4.0.12, which broke `cyclonedxBom` for grails-shell-cli,
grails-console, and grails-dependencies-starter-web with:

  Unpermitted License found for bom dependency:
  pkg:maven/org.jline/jansi@4.0.12?type=jar : BSD-4-Clause

The previous fix added per-version entries for 4.0.7 only. Per-version
entries for an entire dependency group that drifts on every SNAPSHOT
bump is unmaintainable.

Replace the per-version `pkg:maven/org.jline/*` entries with a single
group-level mapping that forces BSD-3-Clause for the whole group. The
fallback kicks in only after the exact-match LICENSE_MAPPING fails, so
existing per-version overrides keep their fast path. Verified locally:

  Forcing license for pkg:maven/org.jline/jansi@4.0.12?type=jar
  to BSD-3-Clause via group rule pkg:maven/org.jline/
  ...
  BUILD SUCCESSFUL in 42s

The criteria for adding a group rule are documented inline (stable
license + cyclonedx-core-java#205 misreport + SNAPSHOT version drift),
so future maintainers know when to extend it and when to stick with
per-version entries.

2) MappingContextAwareConstraintFactory: defensive sibling fix

Architectural review flagged this class as carrying the same
default-valued `List<Class>` constructor parameter that triggered the
Groovy 6 VerifyError in DefaultConstraintFactory. The class itself is
not @CompileStatic, so the bug does not currently fire here, but the
parent constructor it delegates to is, and it is cheaper to apply the
same explicit two-constructor pattern now than to reproduce the same
debugging session if a future Groovy 6 alpha tightens bytecode rules.

3) GormEntityTransformSpec: regression tests for the GROOVY-11829 shim

The original PR added a `get(String)` overload to GormEntity to work
around Groovy 6's relaxed `MetaClassImpl.isGenericGetMethod`, but did
not add focused tests. Architectural review correctly pointed out that
the shim has user-visible behavioral consequences for String-id
entities (e.g. `Book.get("simpleName")` no longer means "load the
entity whose id is the string 'simpleName'") and those need test
coverage so the regression surface is documented and any future change
is caught.

Add three feature methods to GormEntityTransformSpec:

- "test Groovy 6 genericGetMethod regression workaround (GROOVY-11829)"
  asserts the new `get(String)` exists and is @generated alongside the
  original `get(Serializable)`, and that Class bean property access
  (`Book.simpleName`, `Book.name`) still resolves through the
  workaround.
- "test get(String) throws MissingPropertyException when GORM not
  initialized and string is not a Class property" pins the contract
  that genuinely-missing names raise MissingPropertyException, not the
  IllegalStateException that an uninitialised GORM static API would
  otherwise leak.
- "test get(String) returns Class bean property when name matches
  Class property and GORM not initialized" pins the user-visible
  behavior change vs Groovy 5: `Book.get("simpleName")` returns the
  Class.simpleName, not an entity-by-id lookup. The test docstring
  references GormEntity.get(String) and GROOVY-11829 so the trade-off
  is discoverable from the test rather than buried in commit history.

All three new tests pass against Groovy 6.0.0-SNAPSHOT locally:
  ./gradlew :grails-datamapping-core:test \
      --tests "org.grails.compiler.gorm.GormEntityTransformSpec"
  -> 12 tests, 0 failures, BUILD SUCCESSFUL in 36s

Assisted-by: claude-code:claude-opus-4-7
A fresh Groovy 6.0.0-SNAPSHOT pull broke grails-rest-transforms compile:

  Execution failed for task ':grails-rest-transforms:compileGroovy'.
  > Unrecoverable compilation error: startup failed:
    General error during semantic analysis: No signature of method:
    doCall for class: ControllerActionTransformer$1 is applicable for
    argument types: (org.codehaus.groovy.ast.MethodNode) values:
    [org.codehaus.groovy.ast.MethodNode@... index(java.lang.Integer)
    from grails.rest.RestfulController]

The transformer used `DefaultGroovyMethods.count(Iterable, Closure)` with
an inline anonymous Closure subclass that overrode `call(Object)`. Under
Groovy 5 that dispatched via Closure.call(Object) directly. Under Groovy 6
the count helper now goes through MOP `doCall` lookup first, and a Java
inner class overriding `call(Object)` does not advertise a matching
`doCall(MethodNode)`, so dispatch fails at compile time when the AST
transform itself runs against any controller subclass that has typed
overload methods on the supertype (e.g. RestfulController.index(Integer)).

The Closure roundtrip is unnecessary here. Replace it with a plain Java
counting loop. This is shorter, allocates no Closure, removes the
implicit MOP dependency entirely, and works on every Groovy version. The
DefaultGroovyMethods import is no longer used in this file, so remove it
too.

Verified locally:
  ./gradlew :grails-rest-transforms:compileGroovy -PskipCodeStyle
  -> BUILD SUCCESSFUL in 29s

Other `new Closure(this)` sites in the codebase use either no-arg call()
or call(Object...) varargs and were not affected by the new MOP path; if
that changes those should get the same treatment.

Assisted-by: claude-code:claude-opus-4-7
Comment thread build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy Outdated
@bito-code-review

This comment was marked as outdated.

CI surfaced a regression in every Hibernate5 / Functional / Mongodb test
suite that exercised connection-aware entities, all failing with:

  java.lang.IllegalArgumentException: Unknown entity: java.util.LinkedHashMap
    at org.hibernate.internal.SessionImpl.fireDelete(...)
    at AbstractHibernateGormInstanceApi.delete(...)
    at GormStaticApi.delete(GormStaticApi.groovy:536)
    at DataServiceConnectionRoutingSpec.deleteAllFromConnection (line 280)

That stack maps onto the cleanup helper

  DataServiceRoutingProduct."secondary".list().each {
      it."secondary".delete(flush: true)
  }

The class-level `DataServiceRoutingProduct.secondary` was being routed
through the existing GROOVY-11829 workaround on the GormEntity trait
(`static Object get(String nameOrId)`) and correctly returned a
connection-scoped `GormStaticApi`. The instance-level `it.secondary`
however - which should resolve through the entity's
`propertyMissing(String)` to a `DelegatingGormEntityApi` - was finding
the SAME static method as its instance generic-getter under Groovy 6.
Verified directly:

  metaClass.respondsTo(entity, 'get', String) ->
    [public static java.lang.Object DataServiceRoutingProduct.get(java.lang.String)]

So `it.secondary` returned a `GormStaticApi` instead of a
`DelegatingGormEntityApi`. The subsequent `.delete(flush: true)` then
matched `GormStaticApi.delete(D instance)` with the `[flush: true]`
LinkedHashMap cast as `D`, which Hibernate finally rejected at
`session.delete(LinkedHashMap)`.

The same misrouting also explained the secondary failure pattern seen
across CrossLayerMultiDataSourceSpec:

  java.lang.NullPointerException: Cannot invoke
    "org.springframework.validation.Errors.getFieldErrors()"
    because "originalErrors" is null
    at HibernateRuntimeUtils.setupErrorsProperty(...:79)

`it.errors` was being similarly hijacked by the static `get(String)`
on a multi-datasource entity, leaving the `getErrors()` accessor used
by `setupErrorsProperty` returning `null` instead of a real `Errors`.

Fix
---
Drop the trait-level `static Object get(String nameOrId)` and instead
have `GormEntityTransformation` add an INSTANCE `Object get(String name)`
method directly to every `@Entity` class. Its body is a one-line
delegate to the existing `propertyMissing(String)`:

  // generated on every @entity class
  public Object get(String name) { propertyMissing(name) }

Why this works:

  1. Trait-merge no longer rejects the trait. We could not declare BOTH
     `static get(String)` and instance `get(String)` on the trait
     itself - Groovy reports "static and instance methods having the
     same signature". Adding the instance overload via AST keeps it on
     the entity class, where static + instance with the same name and
     params is legal.

  2. Instance dispatch picks the more specific candidate. Because the
     instance method now lives directly on the entity class (not just
     on the trait), Groovy's instance MOP finds it before falling back
     to any trait-static `get(...)` method, so `it.secondary` routes
     through the existing `propertyMissing` and yields the correct
     `DelegatingGormEntityApi`.

  3. Class-level dynamic property access still works. `Class` bean
     properties (`simpleName`, `name`, `canonicalName`, ...) are
     resolved by Groovy's normal Class metaclass before any
     genericGetMethod is consulted, and connection-name lookups like
     `Book.secondary` continue to land on the existing
     `staticPropertyMissing` in GormEntity.

The trait keeps its original `static D get(Serializable id)` (the
public entity-by-id API) untouched.

Tests
-----
Updated `GormEntityTransformSpec` to assert the new shape:
  - the AST-added instance `get(String)` exists and is `@Generated`,
  - it is NOT static,
  - the original `get(Serializable)` is still present.
The earlier tests that documented the old static-overload behaviour
(`Book.get('simpleName') == 'Book'`, etc.) were specific to the
removed shim and have been deleted alongside it.

Verified locally on Groovy 6.0.0-SNAPSHOT:
  ./gradlew :grails-datamapping-core:test \
            :grails-data-hibernate5-core:test \
      --tests 'org.grails.compiler.gorm.GormEntityTransformSpec' \
      --tests 'org.apache.grails.data.testing.tck.tests.Domain*' \
      --tests 'org.apache.grails.data.testing.tck.tests.CrossLayer*' \
      --tests 'org.apache.grails.data.testing.tck.tests.DataService*'
  -> 42 tests, 0 failures, BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
Every CI job that compiled GSPs against Groovy 6.0.0-SNAPSHOT failed
with a Groovy compiler stack like:

  General error during instruction selection: Index 3 out of bounds for length 3
  java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    at org.codehaus.groovy.util.ListHashMap.toMap(ListHashMap.java:207)
    at org.codehaus.groovy.util.ListHashMap.put(ListHashMap.java:146)
    at java.base/java.util.Map.computeIfAbsent(Map.java:1067)
    at org.codehaus.groovy.ast.NodeMetaDataHandler.getNodeMetaData(NodeMetaDataHandler.java:65)
    at org.codehaus.groovy.ast.AnnotationNode.isTargetAllowed(AnnotationNode.java:168)
    at org.codehaus.groovy.classgen.ExtendedVerifier.visitAnnotations(ExtendedVerifier.java:354)
    at org.codehaus.groovy.classgen.ExtendedVerifier.visitConstructor(ExtendedVerifier.java:216)
  ...
  at org.grails.web.pages.GroovyPageForkedCompiler.main(GroovyPageForkedCompiler.groovy:106)

`AnnotationNode.isTargetAllowed` was added in Groovy 6 (GROOVY-11838) to
honour the new default annotation targets and uses
`NodeMetaDataHandler.getNodeMetaData` (a `Map.computeIfAbsent` over an
internal `ListHashMap`) on shared `Annotation*` AST nodes. That cache
is touched concurrently by the Grails `GroovyPageCompiler` thread pool
(`Executors.newFixedThreadPool(availableProcessors() * 2)`) once shared
annotations like `@Inject`, `@CompileStatic`, etc. are seen by more
than one GSP compile at the same time, which is exactly the case for
test apps that pull in Spring/Grails compiled output. `ListHashMap` is
not designed for concurrent mutation, so the resize fails with an
`ArrayIndexOutOfBoundsException` and the entire GSP compile aborts.

Replace the unconditional `availableProcessors() * 2` thread pool with
a small `computeGspCompilerParallelism()` helper that:

* defaults to 1 worker on Groovy 6 (eliminates the race),
* defaults to `availableProcessors() * 2` on Groovy 5 and earlier
  (preserves prior behaviour),
* honours `-Dgrails.gsp.compiler.parallelism=N` so callers can opt back
  into parallel GSP compilation once Groovy 6 fixes the race (or
  experimentally tune it down on Groovy 5).

Trade-off: a small wall-clock increase on Groovy 6 GSP compilation in
exchange for deterministic behaviour. The control knob is a single
system property, so this is easy to revert once the upstream Groovy
fix is available.

Verified locally:
  ./gradlew :grails-gsp-core:compileGroovy --rerun-tasks -> BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
Removing the obsolete static get(String) Groovy 6 workaround in 8e9cdbc
left a doubled blank line above the read(Serializable) method, which the
Core Projects CI job flagged via the CodeNarc ConsecutiveBlankLines rule:

  GormEntity.groovy:608 - File GormEntity.groovy has consecutive blank lines

Tighten back to a single blank separator. No semantic change.

Verified locally:
  ./gradlew :grails-datamapping-core:codenarcMain :grails-gsp-core:codenarcMain
  -> BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
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
… build

The Build Grails Forge CI jobs have been failing on this PR with:

  CreateControllerCommandSpec > test app with controller FAILED
    Condition not satisfied after 240.00 seconds and 240 attempts
    output.toString().contains(value)
    | false BUILD SUCCESSFUL
    | ...
    | > Task :compileTestGroovy FAILED
    | gradle/actions: Writing build results to ...

We can see compileTestGroovy fails in the generated app, but the actual
compiler error message is not visible anywhere in the CI log. The
PollingConditions assertion only inspects what is captured in `output`,
and `executeCommand` here only consumes the forked Gradle process's
*stdout* (process.consumeProcessOutputStream(output)). Compile-error
diagnostics from groovyc / Spock are written to *stderr* and are
therefore silently dropped on every failed run.

Switch to consumeProcessOutput(stdout, stderr) with the same
StringBuilder for both streams so the next CI run surfaces the actual
compiler error in the assertion failure (and in any future debugging).
This is a test-only change to test infrastructure; production code is
unaffected.

Once the underlying compile failure is identified and fixed, this can
stay (it is the more useful default) or be reverted at the maintainer's
discretion.

Assisted-by: claude-code:claude-opus-4-7
Apache Groovy fixed the genericGetMethod over-permissive registration in:

  apache/groovy 999f6dcd "GROOVY-11986: genericGetMethod registration too
    permissive: matches any get(X) where X is a supertype of String"
  apache/groovy a4caaa4b "GROOVY-11986: ... (test)"

Both committed shortly after build #571 (the audit baseline at canary
commit a69a157). Latest 6.0.0-SNAPSHOT publication on Apache snapshots
is build #609 (timestamp 20260508.194756) which includes the fix.

Removed:
- GormEntityTransformation: per-entity AST INSTANCE Object get(String)
  shim (lines around 295-320). The shim was added to give Groovy 6's
  instance MOP a more specific candidate than the inherited
  GormEntity.get(Serializable) so dynamic property reads on @entity
  instances would fall through to propertyMissing(String) instead of
  being hijacked by the generic-getter. With GROOVY-11986 in, the
  generic-getter is no longer registered for the supertype Serializable
  signature, so the dispatch routes correctly without the shim.
- GormEntity: stale doc comment block on get(Serializable) describing
  the now-resolved Groovy 6 dispatch hijack and the AST workaround that
  replaced an earlier trait-static guard.
- GormEntityTransformSpec: 'test Groovy 6 generic-getter instance-
  dispatch guard' regression test. It only verified the AST shim was
  added (Book.getDeclaredMethod('get', String) != null), so it has no
  meaning once the shim is gone. The actual dispatch behaviour is
  exercised by the Hibernate5 / Functional / Mongodb integration suites
  (DataServiceConnectionRoutingSpec, CrossLayerMultiDataSourceSpec)
  which originally surfaced the regression and will continue to gate
  the canary CI matrix.

Verified locally on JDK 21 against the latest 6.0.0-SNAPSHOT cached
from Apache snapshots (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 PR.
@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.

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

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.

Resolve conflict in dependencies.gradle by keeping the
'groovy.version' at 6.0.0-SNAPSHOT (required by the Groovy 6 canary
branch) while accepting the new 'graphql-java.version' and
'graphql-java-extended-scalars.version' entries introduced on
grails8-groovy5-sb4.

Assisted-by: claude-code:claude-4.7-opus
Conflict in dependencies.gradle was the `groovy.version` line, which
both branches edited:

  - grails8-groovy5-sb4: 5.0.6 -> 5.0.7-SNAPSHOT (f1b78b7)
  - grails8-groovy6-canary: stays on 6.0.0-SNAPSHOT

Resolved by keeping `6.0.0-SNAPSHOT` (the canary's whole point); the
5.0.7-SNAPSHOT bump from the base branch does not apply here.

Verified `:grails-bootstrap:dependencies` still resolves Groovy
6.0.0-SNAPSHOT through `org.apache.groovy:groovy-bom:6.0.0-SNAPSHOT`
after the merge.

Assisted-by: claude-code:claude-opus-4-7
Conflict in dependencies.gradle was the `groovy.version` pin inside the
grails-micronaut-bom customBomVersions block, which both branches
edited:

  - grails8-groovy5-sb4: 5.0.6 -> 5.0.7-SNAPSHOT (f1b78b7 path)
  - grails8-groovy6-canary: stays on 6.0.0-SNAPSHOT

Resolved by keeping `6.0.0-SNAPSHOT` in the micronaut bom block (the
canary's whole point); same precedent as 93fdd60 / b98f0cf /
0ea4f26. The accompanying `// Groovy 6, so the micronaut bom must
match` comment is already correct on the canary and is preserved.

Other changes flow in clean from grails8-groovy5-sb4:

  - graphql-java 24.3 -> 25.0 (top-level bomDependencyVersions block,
    same line resolved by git as a one-sided change)
  - grails-data-graphql / grails-data-mongodb / grails-shell-cli /
    grails-data-hibernate5 dbmigration + assorted grails-test-examples
    graphql multi-datastore-app updates including the logback.groovy ->
    logback.xml swap; all auto-merged by git

`groovy.version` at the top-level bomDependencyVersions block stays at
6.0.0-SNAPSHOT (not in conflict; git resolved as a one-sided change
inside the combined diff).

Assisted-by: claude-code:claude-opus-4-7
…ovy6-canary

Four mechanical fixes addressing CI failures observed on the merged
canary branch. Each is scoped to the smallest change that restores the
failing job to green; all were verified locally on
Groovy 6.0.0-SNAPSHOT / JDK 21 / Windows before committing.

1. Code Style / Forge Projects: `org.jline:jansi@4.1.0` license

   `:grails-core:grails-shell-cli:cyclonedxDirectBom` and the same task
   in `:grails-console` and `:grails-dependencies-starter-web` failed
   with:

       Unpermitted License found for bom dependency:
       pkg:maven/org.jline/jansi@4.1.0?type=jar : BSD-4-Clause

   jline 4.1.0 LICENSE.txt
   (https://github.com/jline/jline3/blob/jline-parent-4.1.0/LICENSE.txt)
   confirms BSD-3-Clause. CycloneDX misreports as BSD-4-Clause per
   cyclonedx-core-java#205, identical to the existing 4.0.12 entry
   already in `SbomPlugin.LICENSE_MAPPING`. Added the 4.1.0 entry with
   the same justification.

   Verified

       .\gradlew :grails-shell-cli:cyclonedxDirectBom    # BUILD SUCCESSFUL

2. Validate Dependency Versions: asm 9.10 vs 9.9.1 in 4 micronaut
   test-examples

   `:grails-test-examples-micronaut:validateDependencyVersions` and the
   same task in `-micronaut-groovy-only`, `-issue-11767`, and
   `-plugins-micronaut-singleton` failed with:

       org.ow2.asm:asm - resolved 9.10, expected 9.9.1
       org.ow2.asm:asm-util - resolved 9.10, expected 9.9.1

   Groovy 6.0.0-SNAPSHOT requires asm 9.10 (for JDK 27 bytecode
   support); Spring Boot 4 / Micronaut platform's BOM pin is still
   9.9.1. The divergence is intentional on this canary.

   Added `ext.allowedBomOverrides = ['org.ow2.asm:asm', 'org.ow2.asm:asm-util']`
   to each of the 4 affected projects' `build.gradle`. This uses the
   existing contract documented on
   `GrailsDependencyValidatorPlugin.ALLOWED_OVERRIDES_EXT`.

   Verified

       .\gradlew :grails-test-examples-micronaut:validateDependencyVersions       \
                 :grails-test-examples-micronaut-groovy-only:validateDependencyVersions \
                 :grails-test-examples-issue-11767:validateDependencyVersions     \
                 :grails-test-examples-plugins-micronaut-singleton:validateDependencyVersions
       # BUILD SUCCESSFUL

3. Code Style / Core Projects: `TemplateRenderer.groovy` 5 abstract
   render() methods

   `:grails-views-gson:compileGroovy` failed at
   `TemplateRenderer.groovy:33` with 5 errors of the form:

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

   (+ 4 more `render(...)` overloads, all returning the inner abstract
   class `JsonOutput.JsonWritable`.)

   Under Groovy 6.0.0-SNAPSHOT + `@CompileStatic`, the `@Delegate` AST
   transform on `GrailsJsonViewHelper jsonViewHelper` does not satisfy
   the abstract-method-implementation check for interface methods whose
   return type is an inner abstract class. The 5 `inline(...)`
   overloads return void and are unaffected, so `@Delegate` still
   handles them.

   Added explicit forwarders for the 5 `GrailsJsonViewHelper#render(...)`
   overloads, each one a single-line delegate to `jsonViewHelper`.
   Behaviour is identical to what `@Delegate` generates on Groovy 5.

   This fix surfaces the next compile error in `grails-views-gson` at
   `DefaultGrailsJsonViewHelper.groovy:67`, which is the same class of
   Groovy 6 STC bug applied to a class that inherits from
   `DefaultJsonViewHelper` and implements `GrailsJsonViewHelper`. That
   one does not yield to the same fix (explicit overloads, fully
   qualified return types, removing @CompileStatic from the interface
   were all attempted and rejected); it is deferred as a follow-up
   workaround item on this PR.

   Verified

       .\gradlew :grails-views-gson:compileGroovy
       # progresses past TemplateRenderer; now fails at DefaultGrailsJsonViewHelper

4. Build Grails-Core: `BeanPropertyAccessorImpl` Map constructor

   `:grails-fields:compileGroovy` failed at
   `BeanPropertyAccessorFactory.groovy:83` with:

       Target constructor for constructor call expression hasn't been set

   The call site is `new BeanPropertyAccessorImpl(params)` where
   `params` is a `Map<String, Object>`. The target class is annotated
   `@Canonical @TupleConstructor(includes = [...])`. Under Groovy 6
   `@Canonical` no longer implicitly includes `@MapConstructor` under
   `@CompileStatic`, so the named-arg call site can't bind to a
   constructor.

   Declared `@MapConstructor` explicitly. Restores the Groovy 4 / 5
   behaviour without changing the positional `@TupleConstructor` or the
   `@Canonical`-generated toString / equals / hashCode contract.

   Verified

       .\gradlew :grails-fields:compileGroovy    # BUILD SUCCESSFUL

Remaining CI failures after this commit

The merge of grails8-groovy5-sb4 + these 4 fixes also fixes the
graphql-java 24.3 vs 25.0 BOM mismatch (2 docs projects) and the 3
`cyclonedxDirectBom` license failures (all sites resolve via the
single `LICENSE_MAPPING` entry). The remaining red CI category is the
`DefaultGrailsJsonViewHelper`-flavoured Groovy 6 STC bug on
`grails-views-gson`, which will cascade into the Build Grails-Core /
Functional Tests / Mongodb / Hibernate5 matrix until it is resolved.
The test failures (`:grails-core:test`,
`:grails-testing-support-http-client:test`) are post-compile and
expected to clear once the views-gson compile is restored.

Assisted-by: claude-code:claude-opus-4-7
@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.

…12040 fixed in Groovy 6)

GROOVY-12040 (apache/groovy#2565, merged to master 2026-05-27, present in
6.0.0-SNAPSHOT build #716) restores @builder to @retention(RUNTIME). The
isLikelyBuilderType() heuristic was introduced on the Groovy 5 line because
Class.getAnnotation(Builder) returned null under the SOURCE-retention regression.
With the upstream fix, runtime annotation detection works again, so the heuristic
and its three call-site disjuncts are removed and builder detection reverts to the
pre-Groovy-5 getAnnotation(Builder) form.

The Spring 7 Map-to-typed-config conversion fallbacks (handleConverterNotFoundException,
handleConversionException) are retained - they are independent of the Groovy version
and required regardless of @builder annotation retention.

GROOVY-12040 is not yet backported to GROOVY_5_0_X, so this workaround remains required
on the grails8-groovy5-sb4 base branch (5.0.7-SNAPSHOT); it is removed here only on the
Groovy 6 canary.

Verified on Groovy 6.0.0-SNAPSHOT build #716 / Gradle 9.5.1 / Spring Boot 4.0.6:
:grails-datastore-core:test --tests ConfigurationBuilderSpec (4/4 passed) and
:grails-datastore-core:codeStyle green.

Assisted-by: claude-code:claude-4.8-opus
@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

Assisted-by: claude-code:claude-4.8-opus
…sion (blocker #6)

Under Groovy 6.0.0-SNAPSHOT, ClassCompletionVerifier.checkNoAbstractMethodsNonAbstractClass
spuriously reports all 5 GrailsJsonViewHelper#render(...) overloads as unimplemented on
DefaultGrailsJsonViewHelper, even though they are declared/overridden on the class. The check
iterates ClassNode.getDeclaredMethodsMap() keyed by MethodNode.getTypeDescriptor() (which
includes the return type); the concrete leaf render(...) overrides resolve a different
return-type descriptor than the interface's abstract render(...) entries for the inner-class
return type grails.plugin.json.builder.JsonOutput.JsonWritable, so they do not displace the
abstract entries and survive as "unimplemented". (groovy.json.JsonOutput.JsonWritable, which the
Grails inner class shadowed on Groovy 5, was removed on Groovy 6 - JsonOutput now only declares
JsonUnescaped - which changes inner-class resolution.) It is a Verifier-layer defect, not the
static type checker: it reproduces with @CompileStatic removed.

Eleven earlier source-level workarounds were rejected (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 concrete render stubs on the intermediate superclass
DefaultJsonViewHelper). The fix here targets the actual defect: the bug lives in the
*abstract*-method check, so the 5 render(...) methods on GrailsJsonViewHelper are declared as
`default` (concrete). They are then absent from getAbstractMethods(), the verifier has nothing
to flag, and DefaultGrailsJsonViewHelper - the sole implementor - overrides all 5, so the
throwing default bodies are never reached.

Verified on Groovy 6.0.0-SNAPSHOT build #716 / Gradle 9.5.1 / Spring Boot 4.0.6:
  :grails-views-gson:compileGroovy  -> green
  :grails-views-gson:test           -> all pass (render / HAL / JSON-API / template-inheritance),
                                       1 pre-existing @IgnoreIf skip
  :grails-views-gson:codeStyle      -> green

Remove once the upstream Groovy 6 Verifier regression is fixed. In-tree reproducer:
grails-views-gson itself; a dependency-free standalone reproduction is still being isolated.

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

The Groovy 6 snapshot's groovy-groovysh now pulls the JLine 4.1.0 family transitively
(grails-shell-cli, grails-console), but SbomPlugin.LICENSE_MAPPING only mapped the 4.0.12
family plus jansi@4.1.0. cyclonedx-core-java#205 misreports JLine's BSD-3-Clause as
BSD-4-Clause, so :grails-shell-cli:cyclonedxDirectBom failed with "Unpermitted License found
for bom dependency: ... jline-builtins@4.1.0 : BSD-4-Clause". Because `build` depends on
cyclonedxDirectBom, this broke every CI job that runs build (Core Projects, Forge Projects,
Functional, Hibernate5, Mongodb). It surfaced only after the views-gson Groovy 6 compile
blocker was fixed and CI could finally reach the SBOM stage.

Add the remaining 9 JLine 4.1.0 coordinates (builtins, console, console-ui, native, reader,
shell, style, terminal, terminal-jni) -> BSD-3-Clause, mirroring the existing 4.0.12 entries.
Each module LICENSE.txt at https://github.com/jline/jline3/blob/jline-parent-4.1.0/LICENSE.txt
confirms BSD-3-Clause.

Verified on Groovy 6.0.0-SNAPSHOT: :grails-shell-cli:cyclonedxDirectBom,
:grails-console:cyclonedxDirectBom and :grails-test-core:cyclonedxDirectBom all green.

Assisted-by: claude-code:claude-4.8-opus
…compiler (Groovy 6 canary)

compileGsonViews runs JsonViewCompiler in a forked JVM (AbstractGroovyTemplateCompileTask). The
view-template classpath carries Spock's global AST transform (spock-core), which under Groovy 6
aborts compilation: "Could not instantiate global transform class SpockTransform ...
IncompatibleGroovyVersionException: Spock 2.4.0-groovy-5.0 is not compatible with Groovy
6.0.0-SNAPSHOT". Every other compile/test fork already sets
-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true, but this fork did not, so
:grails-test-examples-*:compileGsonViews failed - which broke the Build Grails-Core, Functional,
Hibernate5 and Mongodb jobs that build the test-example apps' gson views. It only surfaced now
that the views-gson compile blocker and the SBOM license gate were cleared and CI could reach it.

AbstractGroovyTemplateCompileTask now propagates the build JVM's
spock.iKnowWhatImDoing.disableGroovyVersionCheck system property into the fork (a no-op when the
property is unset, so it is safe for released builds), and the canary build JVM carries the flag
via org.gradle.jvmargs so it is available to propagate.

Verified on Groovy 6.0.0-SNAPSHOT:
:grails-test-examples-graphql-grails-multi-datastore-app:compileGsonViews now succeeds.

Assisted-by: claude-code:claude-4.8-opus
…s and block external entities loudly

XmlUtils declared the SAX/Xerces feature identifiers with an https scheme
(https://apache.org/xml/features/..., https://xml.org/sax/features/...). The parser matches these
by exact string, so setFeature threw SAXNotRecognizedException for each and the catch block
swallowed it, leaving the parser at JDK defaults. On JDK 21/25 the FEATURE_SECURE_PROCESSING
default disallows DOCTYPE entirely, so XmlUtilsSpec / TestHttpResponseSpec failed: an inline
DOCTYPE with internal entities was rejected ("DOCTYPE is disallowed ...").

- Correct the identifiers to the http scheme so they are actually applied; disallow-doctype-decl is
  now explicitly false, so inline DOCTYPE with internal entities parses.
- Leave external general entities enabled and instead block them via the JAXP accessExternalDTD /
  accessExternalSchema properties (set to ""), so a SYSTEM reference is attempted and then blocked
  with a thrown SAXParseException ("External Entity: ... access is not allowed") instead of being
  silently dropped (external-general-entities=false skips without throwing). Net external access is
  still fully blocked - it now fails loud, matching the specs.

Verified on Groovy 6.0.0-SNAPSHOT: :grails-testing-support-http-client:test (103 tests) and
:grails-testing-support-http-client:codeStyle are green.

Assisted-by: claude-code:claude-4.8-opus
…uding overridden methods from @DeleGate

WriteFilteringMap overrides put(String,Object), putAll(Map) and remove(Object) to record writes
into the shared nestedDestinationMap (exposed via getWrittenValues()). But @DeleGate on the
`overlap` field also generated put(Object,Object)/putAll(Map)/remove(Object) forwarding straight
to `overlap`, competing with those overrides. Under Groovy 6 a mutation can dispatch to the
generated delegate method instead of the override, so the value lands in `overlap` but is never
recorded in nestedDestinationMap.

Effect: external .groovy config loading/merging silently lost values on Groovy 6
(ExternalConfigRunListener -> WriteFilteringMap), so getConfigProperty(...) returned null; and
WriteFilteringMapSpec failed with getWrittenValues() empty. A plain-Groovy reproduction of the
class works correctly, which is why it only surfaced through the full config-merge path and the
Spock groovy-5.0 artifact's spec compilation - this is a genuine Groovy 6 production bug, not a
test-only workaround.

Exclude the three overridden mutators from @DeleGate so only the tracking overrides (plus their
compiler bridge methods) exist; every mutation is now recorded regardless of dispatch.

Verified on Groovy 6.0.0-SNAPSHOT:
  :grails-core:test (309 tests) green, including WriteFilteringMapSpec
  :grails-test-examples-external-configuration:test green (ExternalConfigSpec, MergedConfigSpec)
  :grails-core:codeStyle green

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

testlens-app Bot commented May 29, 2026

✅ All tests passed ✅

⚠️ TestLens detected flakiness ⚠️

Test Summary

Check Project/Task Test Runs
CI / Build Grails-Core (macos-latest, 21) :grails-core:test DevelopmentModeWatchSpec > test root watchPattern ❌ ✅

🏷️ Commit: 78b62c5
▶️ Tests: 5007 executed
⚪️ Checks: 31/31 completed


Learn more about TestLens at testlens.app.

@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
Copy link
Copy Markdown
Contributor Author

jamesfredley commented May 29, 2026

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

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.

2 participants