Skip to content

feat(deps): Bump Micronaut platform to 5.0.0 (GA)#15677

Open
jamesfredley wants to merge 15 commits into
8.0.xfrom
deps/micronaut-5.0.0
Open

feat(deps): Bump Micronaut platform to 5.0.0 (GA)#15677
jamesfredley wants to merge 15 commits into
8.0.xfrom
deps/micronaut-5.0.0

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

@jamesfredley jamesfredley commented May 22, 2026

Draft - stacked on deps/spring-boot-4.0.6 (#15676) so the Spring Boot 4.0.6 dependency fallout does not muddy this one.

Bumps micronautPlatformVersion from 5.0.0-M2 to 5.0.0 (GA).

Release notes: https://github.com/micronaut-projects/micronaut-platform/releases/tag/v5.0.0

The core problem

Micronaut 5.0.0 GA publishes JARs targeting JVM 25 bytecode and declares org.gradle.jvm.version=25. Grails 8 keeps its baseline at JDK 21. A sub-25 JDK cannot resolve or compile the Micronaut platform, so the Grails-Micronaut "island" - grails-micronaut, grails-micronaut-bom, and the micronaut-tied grails-test-examples-* - cannot be part of a JDK 21 build.

What this PR does

1. Platform bump

  • gradle.properties: micronautPlatformVersion 5.0.0-M2 -> 5.0.0.
  • dependencies.gradle: bump the grails-micronaut-bom Groovy override to 5.0.6 to match Micronaut 5 GA's managed groovy.version. Spock stays at 2.4-groovy-5.0.
  • grails-bom/micronaut/build.gradle: the tools.jackson exclude stays so spring-boot-dependencies remains the single source of truth for Jackson 3 (Micronaut 5 GA ships jackson-bom 3.1.3, Spring Boot 4.0.6 ships 3.1.2).

2. The island is toggled automatically by the build JDK

settings.gradle now decides island membership from the JDK running the build:

  • sub-25 JDK -> island auto-excluded. A plain ./gradlew build works on the Grails 8 baseline (JDK 21) with no flags.
  • JDK 25+ -> island auto-included. The same command transparently builds the Micronaut island too.

Two presence-based overrides always win over the auto-detection:

  • -PskipMicronautProjects - force-exclude on any JDK.
  • -PincludeMicronautProjects - force-include on a sub-25 JDK (escape hatch; the island still cannot compile there).
  • -PskipMicronautProjects wins if both are passed.

3. CI (gradle.yml)

  • Java 21 jobs build/test everything except the island; Java 25 jobs build the full graph. This now follows purely from the JDK each job pins via setup-java - the old matrix.java == 21 && -PskipMicronautProjects conditionals are gone.
  • Snapshot publish is split: the publish job (JDK 21) publishes everything except the island; a publishMicronaut job (JDK 25) publishes grails-micronaut + grails-micronaut-bom.

4. Apache release flow (release.yml) + reproducibility

ASF releases must be byte-for-byte reproducible against a pinned JDK. The flow keeps JAVA_VERSION=21.0.7 for everything outside the island and adds a second pin JAVA_VERSION_MICRONAUT=25.0.3 for the island:

  • JDK 21 staging-publish steps build everything except the island (auto-pruned on JDK 21); a dedicated step switches to JDK 25 to sign + stage the two island artifacts against the same staging repo, then restores JDK 21 for the close + checksum-combine steps.
  • etc/bin/Dockerfile: the verification container installs a second Liberica JDK 25 (pinned version + multi-arch SHA-512) exposed as $JDK_25_HOME; JDK 21 stays the default.
  • etc/bin/verify-reproducible.sh + etc/bin/test-reproducible-builds.sh: each runs a JDK 21 pass for everything outside the island, then a JAVA_HOME=$JDK_25_HOME pass for the two island artifacts. Both fail fast when $JDK_25_HOME is unset.
  • RELEASE.md documents the dual-JDK requirement.

The explicit -PskipMicronautProjects flag is retained only where it is not a JDK proxy:

5. Micronaut HTTP client removed from grails-data-graphql tests

GraphQLSpec previously pulled micronaut-rxjava2-http-client + micronaut-serde-jackson just to call the running app from integration tests. Micronaut 5 drops RxJava 2 from the BOM, so it now uses Spring Boot 4's RestClient with Groovy JsonOutput / JsonSlurper (no Jackson or Gson on the test classpath). gradle.properties loses the now-unused micronautRxjava2Version and micronautSerdeJacksonVersion pins. Nothing in grails-core now depends on Micronaut artifacts outside the island.

Consumer impact

The published grails-micronaut:8.0.0 artifact carries org.gradle.jvm.version=25. Grails 8 apps on JDK 21 will have Gradle variant resolution reject it - it is consumable only on JDK 25+. JDK 21 users get the rest of Grails 8 without the Micronaut integration. The release ships both audiences; choosing the right artifact set is on the consumer's build config.

Key versions in Micronaut 5.0.0 GA

Property Version
micronaut.core.version 5.0.0
micronaut.data.version 5.0.1
micronaut.serde.version 3.0.0
jackson.version 3.1.3
groovy.version 5.0.6
spring.boot.version 4.0.6
spring.version 7.0.7

Platform-level breaking changes in v5.0.0 that are landmines for downstream Grails-on-Micronaut apps (not consumed by grails-core itself): RxJava 2 and MicroStream removed from the BOM, jaxrs.api.version 3 -> 4, kafka.version 3.9 -> 4.2.

grails-forge stays on micronautVersion=4.10.10 (the Forge is itself a Micronaut 4.x app) - out of scope for this platform bump.

Verification

# JDK 21 (Grails baseline): builds the full graph minus the island, no flags needed
./gradlew validateDependencyVersions compileGroovy compileTestGroovy

# JDK 25: builds the full graph including the island
./gradlew build

Project-graph toggle verified on JDK 21 via ./gradlew projects: island absent by default, present with -PincludeMicronautProjects, absent with both override flags (skip wins).

Micronaut Platform 5.0.0 shipped on Maven Central
(io.micronaut.platform:micronaut-platform:5.0.0). Replace the
5.0.0-M2 milestone we were tracking with the released GA.

Accompanying alignments required to keep validateDependencyVersions
green:

- dependencies.gradle: bump the grails-micronaut-bom Groovy override
  from 5.0.5 to 5.0.6 so the strict pin matches Micronaut 5 GA's
  managed groovy.version. spock stays at 2.4-groovy-5.0 (unchanged
  in GA).

- grails-bom/micronaut/build.gradle: refresh the comment on the
  tools.jackson exclude. Micronaut 5.0.0 ships jackson-bom 3.1.3
  while Spring Boot 4.0.6 ships 3.1.2 - the two platforms still
  disagree on the patch, but the direction is now reversed
  (Micronaut ahead of Spring Boot rather than behind). The exclude
  itself stays in place so spring-boot-dependencies remains the
  single source of truth for Jackson 3.

This is the first commit of a draft PR; more fallout is expected as
downstream modules pick up the new platform.

Assisted-by: claude-code:claude-opus-4-7
Micronaut 5.0.0 GA publishes JARs with `org.gradle.jvm.version=25` and
bytecode targeting JVM 25. The Grails 8 baseline is JDK 21, so the
Java 21 matrix entries in `gradle.yml` cannot resolve or compile the
Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom,
the four micronaut-tied test-examples) without UnsupportedClassVersionError.

The `-PskipMicronautProjects` mechanism already exists in
`settings.gradle` (added in #15613) and is already wired into
`groovy-joint-workflow.yml` (line 175). Mirror that pattern in the
main `gradle.yml` matrix: pass the flag on Java 21 jobs only and let
Java 25 jobs build the full graph including the Micronaut island.

Jobs touched:

- `build` - conditional flag, was failing on Java 21
- `buildRerunTasks` - Java 21 only, unconditional flag
- `functional` - conditional flag
- `mongodbFunctional` - conditional flag
- `hibernate5Functional` - conditional flag

`buildGradle` (grails-gradle composite build) and `buildForge`
(grails-forge composite build) are left untouched - both are
separate composite builds that do not include the Grails-Micronaut
island and were not failing for this reason.

Verified locally:

```
JAVA_HOME=corretto-21 ./gradlew validateDependencyVersions \
    compileGroovy compileTestGroovy -PskipMicronautProjects
# BUILD SUCCESSFUL

JAVA_HOME=corretto-25 ./gradlew :grails-micronaut:compileGroovy \
    :grails-test-examples-micronaut:compileGroovy ...
# BUILD SUCCESSFUL

JAVA_HOME=corretto-25 ./gradlew validateDependencyVersions
# BUILD SUCCESSFUL
```

The `publish` job (line 388) still pins `java-version: 21` and would
fail to publish Micronaut artifacts after this change. That is a
release-flow gap that needs a separate follow-up (likely a parallel
`publishMicronaut` job on JDK 25, or moving the existing job to JDK
25), tracked in the PR description.

Assisted-by: claude-code:claude-opus-4-7
@jamesfredley jamesfredley self-assigned this May 22, 2026
@jamesfredley jamesfredley moved this to In Progress in Apache Grails May 22, 2026
@jamesfredley jamesfredley added this to the grails:8.0.0-M2 milestone May 22, 2026
`micronautSerdeJacksonVersion=2.11.0` was pinned in January 2024 and
has missed 15 patch releases on the Micronaut Serde 2.x line. Bump
to the latest 2.x release, 2.16.2 (2025-11-10).

Stays on the 2.x line deliberately. Micronaut Serde 3.0.0 (which
the Micronaut 5 platform manages) has a hard compile dependency on
`micronaut-core:5.0.0` and would drag the Micronaut Core 5 / JVM 25
constraint into the main Grails graph. The consumers of this pin
(`grails-data-graphql/plugin` and the four `grails-test-examples/
graphql/*` apps) live outside the Grails-Micronaut "island" and
build on JDK 21, so they cannot take Serde 3.0.0.

The pin is intentionally floated outside the BOM today (per the
comment in `gradle.properties` line 55: "micronaut libraries not in
the bom due to the potential for spring mismatches"). That stays
the same; just the version moves.

`micronautRxjava2Version=2.9.0` is already the latest published
release on the Micronaut RxJava 2 line and is not bumped here. That
project has not been ported to Micronaut 5 (the platform removed
`io.micronaut.rxjava2` in PR micronaut-platform#1427) and 2.9.x
remains the current default branch.

Verified locally on JDK 21:

```
./gradlew validateDependencyVersions \
    :grails-data-graphql:compileGroovy \
    :grails-test-examples-graphql-grails-docs-app:compileGroovy \
    :grails-test-examples-graphql-grails-docs-app:compileIntegrationTestGroovy \
    :grails-test-examples-graphql-grails-multi-datastore-app:compileGroovy \
    :grails-test-examples-graphql-grails-multi-datastore-app:compileIntegrationTestGroovy \
    :grails-test-examples-graphql-grails-test-app:compileGroovy \
    :grails-test-examples-graphql-grails-test-app:compileIntegrationTestGroovy \
    :grails-test-examples-graphql-grails-tenant-app:compileGroovy \
    :grails-test-examples-graphql-grails-tenant-app:compileIntegrationTestGroovy \
    -PskipMicronautProjects
# BUILD SUCCESSFUL
```

Assisted-by: claude-code:claude-opus-4-7
The Forge composite build includes the root grails-core build via
`includeBuild('..')` in `grails-forge/settings.gradle:75`. When the
`buildForge` job in `.github/workflows/gradle.yml` runs `./gradlew
build` on Java 21, that triggers task resolution in the included
grails-core build - which pulls the JVM-25 Micronaut island and
fails with `Could not resolve io.micronaut:micronaut-inject-groovy`
(only compatible with JVM 25+).

Apply the same conditional `-PskipMicronautProjects` flag as the
other Java 21 matrix jobs. Forge itself stays on Micronaut 4.10.10
regardless; the flag only affects the included grails-core build's
Micronaut island, not Forge's own Micronaut 4 modules.

Verified locally on JDK 21:

```
cd grails-forge && JAVA_HOME=corretto-21 \
    ./gradlew assemble -PgrailsIndy=false -PskipCodeStyle \
    -PskipTests -PskipMicronautProjects
# BUILD SUCCESSFUL
```

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

The `GraphQLSpec` test trait in `grails-data-graphql/plugin` shipped a
Micronaut RxJava2 HTTP client (`io.micronaut.rxjava2:micronaut-rxjava2-http-client`)
purely to exercise the GraphQL controller from integration tests. Spring
Boot 4 already provides everything that trait needs via Spring's
`RestClient` (org.springframework.web.client.RestClient) - there is no
reason for a Grails GraphQL plugin to depend on the Micronaut HTTP stack,
and `io.micronaut.rxjava2` has not been ported to Micronaut 5 anyway.

Replace the Micronaut HTTP client with Spring `RestClient`:

- `GraphQLSpec.groovy` is rewritten to use `RestClient.builder()` with a
  single `StringHttpMessageConverter` (configured to accept all media
  types). JSON encoding/decoding is handled by Groovy's `JsonOutput` and
  `JsonSlurper` so the trait does not pull a Jackson or Gson runtime
  onto the consuming apps - the graphql test apps use `grails-views-gson`
  but do not actually ship the Gson library, and Grails 8 does not ship
  Jackson on the default classpath either.
- Helper return type changes from `io.micronaut.http.HttpResponse<Map>`
  to `org.springframework.http.ResponseEntity<Map>`. Spring's
  `ResponseEntity.getBody()` returns `T` directly (not `Optional<T>`),
  so the 109 `resp.body()` call sites collapse to `resp.body` (Groovy
  property access) across 17 integration test specs.
- The two-arg `graphql(String, Class<T>)` overload survives but is now
  String-only (the one caller, `InheritanceIntegrationSpec`, asserts on
  the raw JSON string body). The single remaining `resp.getBody().get()`
  call site (from Micronaut's `Optional`-returning getter) becomes
  `resp.getBody()`.

The unused-after-removal version pins are dropped from `gradle.properties`:

- `micronautRxjava2Version=2.9.0` - was the rxjava2 client pin.
- `micronautSerdeJacksonVersion=2.16.2` - was only there as the JSON
  mapper for the rxjava2 client. No production code uses
  `@Serdeable` or any `io.micronaut.serde.*` API; confirmed via grep
  across `grails-data-graphql/` and `grails-test-examples/graphql/`.

Each of the 4 graphql test apps loses both `implementation` lines (rxjava2
+ serde-jackson) - they brought no Spring Boot-side value once the Mn
HTTP client is gone.

Verified locally on JDK 21:

```
./gradlew validateDependencyVersions \
    :grails-test-examples-graphql-grails-test-app:integrationTest \
    :grails-test-examples-graphql-grails-docs-app:integrationTest \
    :grails-test-examples-graphql-grails-multi-datastore-app:integrationTest \
    :grails-test-examples-graphql-grails-tenant-app:integrationTest \
    -PskipMicronautProjects
# BUILD SUCCESSFUL - all integration tests pass across all 4 apps
```

Side note: this closes the long-standing "out-of-band Micronaut pin"
comment in `gradle.properties`: nothing in grails-core now depends on
Micronaut artifacts outside the Grails-Micronaut "island" managed by
`grails-micronaut-bom`.

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

The existing `publish` job runs on JDK 21 and invokes the aggregate
`publish` task across the whole project. With the Micronaut 5 platform
bump that island now requires JVM 25 bytecode, so the JDK 21 publish
would fail trying to compile `grails-micronaut` / `grails-micronaut-bom`.

Mirror the matrix-job split into the publish side:

- Existing `publish` job (Java 21): pass `-PskipMicronautProjects` so
  it publishes everything EXCEPT the Micronaut island. Same task list
  (`publish aggregateChecksums aggregatePublishedArtifacts`), same
  retry harness, same secrets.

- New `publishMicronaut` job (Java 25): publishes the two island
  artifacts via the explicit task list
  `:grails-micronaut:publish :grails-micronaut-bom:publish`. The three
  test-example projects in the island are not published. Same retry
  harness and secrets as the sibling job. Same `needs:` / `if:` gates
  so both jobs fire from the same successful upstream matrix runs.

Going with the explicit task-list approach instead of adding a
complementary `-PonlyMicronautProjects` flag to `settings.gradle` -
the island only publishes two artifacts and the symmetric flag would
require inverting the gating logic in `settings.gradle:88`, which is
more moving parts than the situation warrants.

Verified locally:

```
# JDK 25 - publishes the Micronaut island
JAVA_HOME=corretto-25 ./gradlew :grails-micronaut:publishToMavenLocal \
    :grails-micronaut-bom:publishToMavenLocal --no-build-cache --rerun-tasks
# BUILD SUCCESSFUL
# Artifacts confirmed:
#   ~/.m2/.../grails-micronaut/8.0.0-SNAPSHOT/grails-micronaut-8.0.0-SNAPSHOT.pom
#   ~/.m2/.../grails-micronaut-bom/8.0.0-SNAPSHOT/grails-micronaut-bom-8.0.0-SNAPSHOT.pom

# JDK 21 + -PskipMicronautProjects - the parallel non-Micronaut publish path
JAVA_HOME=corretto-21 ./gradlew :grails-core:publishToMavenLocal \
    -PskipMicronautProjects --no-build-cache --rerun-tasks
# BUILD SUCCESSFUL
```

Also verified that the JDK-25 Micronaut island runs full tests (not
just compile) cleanly: `./gradlew :grails-micronaut:test
:grails-test-examples-micronaut:test
:grails-test-examples-micronaut-groovy-only:test
:grails-test-examples-plugins-micronaut-singleton:test` -> BUILD
SUCCESSFUL.

Assisted-by: claude-code:claude-opus-4-7
The Micronaut 5 platform GA targets JVM 25 bytecode, so the
`grails-micronaut` and `grails-micronaut-bom` artifacts cannot be
assembled or staged on the existing JDK 21.0.7 reproducibility pin.
Apply the same JDK-split pattern already in place for the CI matrix
(see `.github/workflows/gradle.yml`) to the Apache release flow:

- New `JAVA_VERSION_MICRONAUT=25.0.3` env var alongside the existing
  `JAVA_VERSION=21.0.7`. This is the SECOND reproducibility pin for
  this repository - any verifier wanting to reproduce the Micronaut
  artifacts must use this exact JDK (Liberica, version `25.0.3+11`).

- `release.yml`'s `publish` job now runs the existing pre-publish
  smoke checks and the three sibling publishToSonatype blocks
  (Gradle plugins, Grails Core, Grails Forge) on JDK 21 with
  `-PskipMicronautProjects`. The `assemble`, `grails-doc:build`,
  and forge composite builds all need the flag - the Forge composite
  includes `..` so it transitively pulls the Micronaut island.

- A new `Publish Grails-Micronaut to Staging Repository` step
  switches to JDK 25 via `setup-java@v4` and stages the two island
  artifacts via the explicit task list
  `:grails-micronaut:publishToSonatype :grails-micronaut-bom:publishToSonatype`.
  It uses `findSonatypeStagingRepository` to land in the same staging
  repository that the JDK 21 blocks already populated (matched by
  `NEXUS_PUBLISH_DESCRIPTION`).

- A trailing `setup-java@v4` step restores JDK 21 before the
  `closeSonatypeStagingRepository` and checksum/artifact-list
  combination steps. Symmetric, keeps any future steps that touch
  the repository's own Gradle config on the documented JDK.

- `release-publish-docs.yml` and the `docs` job in `release.yml` both
  build `grails-doc` from the root project; they now pass
  `-PskipMicronautProjects` to keep Gradle from resolving the
  island's JVM 25 variants on the JDK 21 runner. The SDKMAN release
  steps (`sdkMinorRelease` / `sdkMajorRelease`, run from the Forge
  composite) also get the flag - they do not compile anything, but
  the include-build dependency on the Micronaut island makes the
  flag defensive.

`.sdkmanrc` is documented as the primary JDK source; a comment block
now explicitly tells local committers that the Micronaut island
requires a separate `JAVA_VERSION_MICRONAUT` JDK installed via
`sdk install java <version>-librca`, and points to the relevant
RELEASE.md verification section (to be written in a follow-up
commit on this PR).

Downstream consumer impact (documented in the PR description):
the published `grails-micronaut:8.0.0` artifact will carry
`org.gradle.jvm.version=25` in its Gradle module metadata, so
Grails 8 apps running on JDK 21 cannot depend on it. JDK-25
consumers can use the Micronaut integration; JDK-21 consumers use
the rest of Grails 8.

Phase 1 of 3 for the dual-JDK release flow. Follow-up commits
update `etc/bin/Dockerfile`, the verify scripts, and `RELEASE.md`
so verifiers can actually reproduce the JDK 25 artifacts.

Assisted-by: claude-code:claude-opus-4-7
Verifiers reproduce release artifacts by running etc/bin/verify-reproducible.sh
inside the etc/bin/Dockerfile container. With Micronaut 5 now staged from
JDK 25, the verifier needs the matching Liberica JDK on disk and the
verify script needs to switch into it for the island only.

`etc/bin/Dockerfile`:

- Adds a second Liberica JDK install, version pinned via `JDK_25_VERSION`
  and exposed via `JDK_25_HOME=/opt/liberica-jdk25`. Keep
  `JDK_25_VERSION` synced with `JAVA_VERSION_MICRONAUT` in
  `.github/workflows/release.yml`. Liberica's amd64 + arm64 tarballs are
  downloaded from `download.bell-sw.com` and verified against the
  SHA-512 checksums baked into the Dockerfile (multi-arch via
  `dpkg --print-architecture`). The primary JDK 21.0.7 stays the
  default - everything in `PATH`, `JAVA_HOME` is unchanged. The
  secondary JDK is opt-in via `JDK_25_HOME`.

`etc/bin/verify-reproducible.sh`:

- The single-pass build (`grails-gradle → root → grails-forge`) splits
  into two passes:
  1. Default JDK 21: builds `grails-gradle` composite as-is, then
     `./gradlew publishToMavenLocal -PskipMicronautProjects` in root,
     then `grails-forge` composite (also with the skip flag because
     Forge does `includeBuild('..')` and would otherwise pull the
     Micronaut island through the transitive evaluation).
  2. JDK 25 via `JDK_25_HOME` override: builds the two Micronaut
     island artifacts only -
     `:grails-micronaut:publishToMavenLocal :grails-micronaut-bom:publishToMavenLocal`.
- Script fails fast with a clear error if `JDK_25_HOME` is not set
  (the container exports it; manual verifiers outside the container
  must install Liberica JDK matching `$JAVA_VERSION_MICRONAUT` from
  `release.yml` and export the env var).

`etc/bin/test-reproducible-builds.sh`:

- Same dual-pass structure, extracted into a `build_all()` helper so
  both reproducibility passes (`first` and `second`) execute the same
  sequence. The Micronaut island gets the same dual-build treatment
  as the rest of the artifacts so any JDK-25-specific reproducibility
  hazards (timestamp ordering in class files, constant pool ordering,
  stack map frame layout) surface locally before they reach a
  release vote.

Phase 2 of 3. Phase 3 updates RELEASE.md to teach maintainers and
ASF voters the dual-JDK requirement.

Assisted-by: claude-code:claude-opus-4-7
Three sections of RELEASE.md teach maintainers and ASF voters the
new dual-JDK requirement that this PR introduces:

1. Section 2's "Manual Verification: Reproducible Jar Files" gets a
   new "Dual-JDK requirement for the Grails-Micronaut island"
   sub-section. Explains that Grails 8 release artifacts now come
   from two reproducibility pins ($JAVA_VERSION for everything
   except the Micronaut island, $JAVA_VERSION_MICRONAUT for
   grails-micronaut + grails-micronaut-bom) and tells outside-the-
   container verifiers how to install the secondary JDK and
   export $JDK_25_HOME before running `verify-reproducible.sh`.

2. The "Appendix: Verification from a Container" section gains a
   paragraph noting that the container now ships both JDKs and that
   `verify-reproducible.sh` switches between them automatically -
   no manual JDK juggling inside the container.

3. The "Step 1: Ensuring we are reproducible" appendix gets a note
   above the existing reproducibility gotchas list explaining the
   dual-JDK split, where both pins live, and the cross-file sync
   requirement: bumping either pin requires bumping the matching
   one in etc/bin/Dockerfile so the verification container can
   reproduce the new artifacts.

Phase 3 of 3 - completes the dual-JDK release-flow work in this PR.

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

Micronaut-leak audit (post-228f9a52ce)

Did a full grep for io.micronaut across every build.gradle in the main grails-core multi-project to confirm nothing JDK-25 lives outside the -PskipMicronautProjects gating. Clean result.

Every build.gradle referencing io.micronaut in the main build

build.gradle Project In the island gating?
grails-bom/micronaut/build.gradle :grails-micronaut-bom
grails-micronaut/build.gradle :grails-micronaut
grails-test-examples/micronaut/build.gradle :grails-test-examples-micronaut
grails-test-examples/plugins/issue-11767/build.gradle :grails-test-examples-plugins-issue-11767
grails-test-examples/plugins/micronaut-singleton/build.gradle :grails-test-examples-plugins-micronaut-singleton

Plus the two transitive consumers that depend on :grails-micronaut without naming io.micronaut directly:

  • :grails-test-examples-issue-11767 (grails-test-examples/issue-11767/)
  • :grails-test-examples-micronaut-groovy-only (grails-test-examples/micronaut-groovy-only/)

All seven live inside the if (!skipMicronautProjects) { ... } block at settings.gradle:511-520.

Heads up: island is 7 projects, not 5

I'd been referring to "5 island projects" in earlier commits and the PR description. The correct count from settings.gradle is 7 build-graph projects in the island (the two missing in my count were :grails-test-examples-issue-11767 and :grails-test-examples-plugins-issue-11767). The two issue-11767 projects exist to repro apache/grails-core#11767 and depend on :grails-micronaut + :grails-micronaut-bom, so they correctly belong in the island gating.

The PUBLISHED subset is still just 2 (grails-micronaut plugin + grails-micronaut-bom), which is what publishMicronaut in gradle.yml and the new JDK 25 publish step in release.yml stage. The three test-examples + two issue-11767 projects build inside the island but aren't published.

grails-data-graphql was the only real leak; it's fixed

grails-data-graphql/plugin was the only non-island main-build project with a direct io.micronaut.* dependency, via the rxjava2 HTTP client compileOnly in GraphQLSpec.groovy. Removed in 228f9a52ce (replaced with Spring RestClient + Groovy JsonSlurper).

grails-forge/ is intentionally separate

The 50+ io.micronaut.* references in grails-forge/*/build.gradle are all in the separate composite build which is itself a Micronaut 4.10.10 app (grails-forge/gradle.properties:27). Out of scope - the Forge stays on Micronaut 4.x regardless of what the main grails-core multi-project does with Micronaut 5.

Net

Nothing in grails-core outside the explicitly-gated 7-project Micronaut island pulls Micronaut 5 / JDK 25 onto the JDK 21 classpath. The Forge composite is its own Micronaut 4.x universe. Audit clean.

@jdaugherty
Copy link
Copy Markdown
Contributor

Is this supposed to still be draft?

@jamesfredley jamesfredley marked this pull request as ready for review May 26, 2026 21:05
Copilot AI review requested due to automatic review settings May 26, 2026 21:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR bumps the Micronaut platform to 5.0.0 GA and adapts the build, CI, release, and GraphQL test infrastructure for Micronaut’s JDK 25 requirement while keeping the rest of Grails on the JDK 21 baseline.

Changes:

  • Updates Micronaut/Groovy dependency versions and removes obsolete RxJava2/Serde test dependencies.
  • Splits CI/release/reproducibility flows so non-Micronaut projects run on JDK 21 and the Grails-Micronaut island runs on JDK 25.
  • Replaces GraphQL integration-test HTTP usage from Micronaut client APIs to Spring RestClient.

Reviewed changes

Copilot reviewed 34 out of 37 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
RELEASE.md Documents the dual-JDK reproducibility and verification process.
.sdkmanrc Documents the primary JDK pin and secondary JDK requirement.
gradle.properties Bumps micronautPlatformVersion and removes unused Micronaut pins.
dependencies.gradle Aligns the Micronaut BOM Groovy override with Micronaut 5 GA.
grails-bom/micronaut/build.gradle Updates Jackson exclusion rationale.
grails-data-graphql/plugin/build.gradle Replaces Micronaut client compile dependency with Spring Web.
grails-data-graphql/plugin/src/main/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpec.groovy Rewrites GraphQL test helper to use RestClient and Groovy JSON parsing.
grails-test-examples/graphql/grails-test-app/build.gradle Removes Micronaut HTTP client and Serde dependencies.
grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/*.groovy Updates GraphQL response body access for ResponseEntity.
grails-test-examples/graphql/grails-tenant-app/build.gradle Removes Micronaut test HTTP dependencies.
grails-test-examples/graphql/grails-tenant-app/src/integration-test/groovy/grails/tenant/app/UserIntegrationSpec.groovy Updates GraphQL response body access.
grails-test-examples/graphql/grails-multi-datastore-app/build.gradle Removes Micronaut test HTTP dependencies.
grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/BarIntegrationSpec.groovy Updates GraphQL response body access.
grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/FooIntegrationSpec.groovy Updates GraphQL response body access.
grails-test-examples/graphql/grails-docs-app/build.gradle Removes unused Micronaut test HTTP dependencies.
etc/bin/Dockerfile Adds pinned secondary Liberica JDK 25 for reproducible Micronaut builds.
etc/bin/verify-reproducible.sh Splits reproducibility verification into JDK 21 and JDK 25 passes.
etc/bin/test-reproducible-builds.sh Splits local reproducibility testing into JDK 21 and JDK 25 passes.
.github/workflows/gradle.yml Skips Micronaut projects on JDK 21 jobs and adds JDK 25 Micronaut snapshot publishing.
.github/workflows/release.yml Splits release publishing between JDK 21 and JDK 25.
.github/workflows/release-publish-docs.yml Skips Micronaut projects for JDK 21 docs publishing.

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

Comment thread .github/workflows/release.yml
@bito-code-review
Copy link
Copy Markdown

The PR diff shows that the grails-micronaut and grails-micronaut-bom projects are skipped in Java 21 jobs using the -PskipMicronautProjects flag. This flag is applied in multiple build jobs to avoid compiling JVM-25-specific Micronaut code on Java 21. However, the final release step does not include these projects in the checksum and artifact aggregation, leading to incomplete CHECKSUMS.txt and PUBLISHED_ARTIFACTS.txt files. To fix this, the aggregation tasks aggregateChecksums and aggregatePublishedArtifacts should be added to the JDK 25 step to ensure the Micronaut artifacts are included in the final release files.

.github/workflows/gradle.yml

build:
      name: "Build Grails-Core (Java ${{ matrix.java }}, indy=${{ matrix.indy }})"
      strategy:
        matrix:
          java: [21, 25]
          indy: [true, false]
      runs-on: ubuntu-latest
      steps:
        - name: "Checkout code"
          uses: actions/checkout@v2
        - name: "🔍 Setup TestLens"
          uses: testlens-app/setup-testlens@v1
        - name: "🔨 Build project"
          run: >
            ./gradlew build :grails-shell-cli:installDist groovydoc
            --continue
            --stacktrace
            -PonlyCoreTests
            -PskipCodeStyle
            ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }}

Comment thread etc/bin/verify-reproducible.sh Outdated
Comment thread etc/bin/verify-reproducible.sh
- release.yml: append the Micronaut island (grails-micronaut,
  grails-micronaut-bom) checksums and published-artifact entries to the
  combined CHECKSUMS.txt / PUBLISHED_ARTIFACTS.txt. The grails-core
  aggregation runs with -PskipMicronautProjects on JDK 21 and therefore
  omits the island. The island's per-project files are produced by
  publishedChecksums / savePublishedArtifacts as finalizers of the JDK 25
  publishToSonatype step, so they are appended here rather than re-running
  aggregateChecksums on JDK 25 - which would re-fingerprint the JDK 21
  artifacts against a JDK 25 compiler and break reproducibility.
- etc/bin/verify-reproducible.sh, etc/bin/test-reproducible-builds.sh:
  add --no-daemon to all gradle invocations.
- grails-data-graphql: document that the GraphQLSpec testing trait
  serializes and parses JSON with Groovy JsonOutput / JsonSlurper instead
  of the application's configured object mapper, so trait assertions may
  differ from production response rendering.

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

Forge behavior: the grails-micronaut feature and Java versions

Documenting the current Forge behavior (asked about earlier - it was already in place on this branch, committed in 5f94d86f79).

What happens

When the grails-micronaut feature is selected in Grails Forge (CLI, web UI, or API) and the requested target JDK is below 25, app generation is rejected - no project is produced. The feature's processSelectedFeatures hook throws:

grails-micronaut requires JDK 25 or later (selected: JDK <NN>).

Selecting the feature with JDK 25+ generates the app normally.

Where it is enforced

org.grails.forge.feature.micronaut.GrailsMicronaut#processSelectedFeatures:

JdkVersion jdk = featureContext.getJavaVersion();
if (jdk.majorVersion() < JdkVersion.JDK_25.majorVersion()) {
    throw new IllegalArgumentException(
        getName() + " requires JDK 25 or later (selected: JDK " + jdk.majorVersion() + ").");
}

Why JDK 25

micronaut-core's ScopedValues references java.lang.ScopedValue.CallableOp, which only exists on JDK 25+ (JEP 506). More broadly, the Micronaut 5.0.0 GA platform bumped in this PR publishes JARs with org.gradle.jvm.version=25 and JVM 25 bytecode. This is the same constraint that drives the consumer-side note in the PR description: the published grails-micronaut:8.0.0 artifact carries org.gradle.jvm.version=25, so Grails 8 apps on JDK 21 cannot resolve it. Forge fails fast at generation time rather than letting users discover the incompatibility at build time.

Related Forge validation

GrailsMicronautValidator#validatePreProcessing additionally rejects combining grails-micronaut with Spring Boot DevTools (see micronaut-projects/micronaut-spring#769):

Spring Boot DevTools are not supported with Grails Micronaut

Note on test coverage

The JDK 25 guard itself currently has no dedicated negative test - existing specs that exercise grails-micronaut simply supply JdkVersion.JDK_25 so they pass. If desired, I can add a Forge spec asserting that selecting grails-micronaut with a sub-25 JDK throws the expected IllegalArgumentException. Let me know and I'll include it.

Add GrailsMicronautSpec asserting that the grails-micronaut Forge feature:
- renders the org.apache.grails:grails-micronaut dependency when JDK 25 is
  selected, and
- rejects generation with the expected IllegalArgumentException when a
  sub-25 JDK (JDK 21) is selected.

This closes the test gap noted during PR #15677 review - the guard in
GrailsMicronaut#processSelectedFeatures previously had no dedicated
negative test.

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

Added the negative test in cb75043 (GrailsMicronautSpec). It covers both directions of the guard:

  • grails-micronaut + JDK 25 -> the org.apache.grails:grails-micronaut dependency is rendered.
  • grails-micronaut + JDK 21 -> generation is rejected with IllegalArgumentException: grails-micronaut requires JDK 25 or later (selected: JDK 21).

Verified locally (run with -PskipMicronautProjects, which the Forge composite needs on JDK 21 because of includeBuild('..')):

GrailsMicronautSpec > test grails-micronaut adds the dependency when JDK 25 is selected PASSED
GrailsMicronautSpec > test grails-micronaut is rejected when the selected JDK is below 25 PASSED
BUILD SUCCESSFUL

settings.gradle now decides whether the Grails-Micronaut "island"
(grails-micronaut, grails-micronaut-bom, the micronaut-tied test-examples)
is part of the build graph based on the build JDK:

  - sub-25 JDK : island auto-excluded (Micronaut 5 GA targets JVM 25 bytecode
                 and declares org.gradle.jvm.version=25, which a sub-25 JDK
                 cannot resolve or compile)
  - JDK 25+    : island auto-included

A plain ./gradlew build therefore works on the Grails 8 baseline (JDK 21) and
transparently picks up the island on JDK 25+. Two presence-based overrides
still win over the auto-detection:

  -PskipMicronautProjects     force-exclude on any JDK (used by
                              groovy-joint-workflow.yml, where the island's
                              Groovy 5 pin clashes with the Groovy 4 snapshot
                              that build swaps in, independent of the JDK)
  -PincludeMicronautProjects  force-include on a sub-25 JDK (escape hatch; the
                              island still cannot compile there)

skip wins if both are present.

Because the CI jobs pin a specific JDK via setup-java, the per-job
matrix.java == 21 conditionals and the unconditional JDK-21 skip flags became
redundant and are removed from gradle.yml, release.yml, and
release-publish-docs.yml. The explicit flag is kept where it is NOT merely a
JDK proxy:

  - groovy-joint-workflow.yml excludes the island for a Groovy-version reason,
    not a JDK reason (issue #15613)
  - etc/bin/verify-reproducible.sh and etc/bin/test-reproducible-builds.sh are
    standalone scripts whose first pass must exclude the island regardless of
    the ambient default JDK; their JDK 25 pass addresses the island tasks
    directly and resolves them via the auto-include

Verified on JDK 21 with ./gradlew projects: island absent by default, present
with -PincludeMicronautProjects, absent with both flags.

Assisted-by: claude-code:claude-opus-4-8
…tion

The comment described only the manual -PskipMicronautProjects flag. Update it
to match the build-JDK auto-detection added in settings.gradle and mention the
-PincludeMicronautProjects override.

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

Local build now keys off the build JDK

Pushed a change to settings.gradle so the Grails-Micronaut island is selected by whatever JDK runs the build, instead of having to remember a flag:

  • ./gradlew build on JDK 21 (the Grails 8 baseline) just works. The island is auto-excluded because Micronaut 5 GA bytecode targets JVM 25 and a sub-25 JDK cannot resolve or compile it. No -PskipMicronautProjects needed anymore.
  • ./gradlew build on JDK 25+ also builds Micronaut locally. When the build JDK is 25 or newer the island is auto-included, so the same command builds the full graph - no flag, no separate task list.

Explicit flags still win over the detection:

  • -PskipMicronautProjects force-excludes the island on any JDK (this is what groovy-joint-workflow.yml uses - that exclusion is a Groovy-version reason, not a JDK reason, so it must hold even on JDK 25).
  • -PincludeMicronautProjects force-includes the island on a sub-25 JDK (escape hatch; it still won't compile there, but the tasks become addressable).
  • If both are passed, -PskipMicronautProjects wins.

Because each CI job pins its JDK via setup-java, the now-redundant matrix.java == 21 && -PskipMicronautProjects conditionals and the unconditional JDK-21 skip flags were removed from gradle.yml, release.yml, and release-publish-docs.yml - the JDK the job runs on decides it. The explicit flag is kept only where it is not a JDK proxy: groovy-joint-workflow.yml (Groovy reason) and the standalone etc/bin verify scripts (their first pass must exclude the island regardless of the ambient default JDK).

Verified on JDK 21 with ./gradlew projects: island absent by default, present with -PincludeMicronautProjects, absent with both flags.

Base automatically changed from deps/spring-boot-4.0.6 to 8.0.x May 29, 2026 04:00
Resolve conflict in .github/workflows/gradle.yml: keep the JDK 25 Micronaut
island publish comment while adopting the nick-fields/retry v4.0.0 pin from
8.0.x. Also align the new publishMicronaut job's actions with the SHA-pinned
versions used elsewhere in the workflow (checkout v6.0.2, setup-java v5.2.0,
setup-gradle v6.1.0 with cache-provider: basic, setup-testlens v1.9.2,
retry v4.0.0) per the ASF action-pinning policy.

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

testlens-app Bot commented May 29, 2026

🔎 No tests executed 🔎

🏷️ Commit: 40d0bc6
▶️ Tests: 0 executed
⚪️ Checks: 35/35 completed


Learn more about TestLens at testlens.app.

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

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants