From 2e5d8dbd9190f96bbd19f1292e644a468112f73b Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 13:35:27 -0400 Subject: [PATCH 01/13] feat(deps): Bump Micronaut platform 5.0.0-M2 to 5.0.0 (GA) 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 --- dependencies.gradle | 2 +- gradle.properties | 2 +- grails-bom/micronaut/build.gradle | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index d2a51fb8e09..0b25eb7df20 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -219,7 +219,7 @@ ext { // enforcedPlatform(:grails-micronaut-bom). else if (project.name == 'grails-micronaut-bom') { customBomVersions = [ - 'groovy.version' : '5.0.5', + 'groovy.version' : '5.0.6', 'spock.version' : '2.4-groovy-5.0', 'protobuf.version': '4.30.2', ] diff --git a/gradle.properties b/gradle.properties index 547692b3272..71000d8a346 100644 --- a/gradle.properties +++ b/gradle.properties @@ -53,7 +53,7 @@ gradleChecksumPluginVersion=1.4.0 gradleCycloneDxPluginVersion=3.0.0 # micronaut libraries not in the bom due to the potential for spring mismatches -micronautPlatformVersion=5.0.0-M2 +micronautPlatformVersion=5.0.0 micronautRxjava2Version=2.9.0 micronautSerdeJacksonVersion=2.11.0 diff --git a/grails-bom/micronaut/build.gradle b/grails-bom/micronaut/build.gradle index 3923b3f114a..f3eda80c75e 100644 --- a/grails-bom/micronaut/build.gradle +++ b/grails-bom/micronaut/build.gradle @@ -76,9 +76,11 @@ dependencies { // managed versions transitively and don't need to declare the platform themselves. // Exclude Groovy since we declare the required version explicitly below via customBomDependencies. // Exclude Spock since the base BOM manages that version. - // Exclude Jackson 3 (tools.jackson) since spring-boot-dependencies manages that version, - // and Micronaut can lag behind Spring Boot's patch bumps (e.g. SB 4.0.6 ships - // jackson-bom 3.1.2 while micronaut-platform 5.0.0-M2 still pins 3.1.0). + // Exclude Jackson 3 (tools.jackson) since spring-boot-dependencies manages that version + // and the two platforms routinely disagree on the patch (e.g. SB 4.0.6 ships + // jackson-bom 3.1.2 while micronaut-platform 5.0.0 pins 3.1.3). Let Spring Boot + // remain the single source of truth so the dependency-version validator does not + // see drift between probe and project resolution. api(platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")) { exclude group: 'org.apache.groovy' exclude group: 'org.spockframework' From b81f2fb8f7f934122c34dbc6d78f0c87119cab40 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 14:05:35 -0400 Subject: [PATCH 02/13] ci: Skip Micronaut island on Java 21 jobs in gradle.yml 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 --- .github/workflows/gradle.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 8132d637e28..22b3d995f6e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -125,12 +125,16 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "🔨 Build project" + # Micronaut 5 platform GA targets JVM 25 bytecode, so on the Java 21 jobs we + # skip the Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, + # the micronaut-tied test-examples). Java 25 jobs build the full graph. run: > ./gradlew build :grails-shell-cli:installDist groovydoc --continue --stacktrace -PonlyCoreTests -PskipCodeStyle + ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} buildRerunTasks: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: 'Build Grails-Core Rerunning all Tasks' @@ -158,6 +162,8 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "🔨 Build project" + # This job only runs on Java 21; skip the Micronaut island because the + # Micronaut 5 platform GA targets JVM 25 bytecode (see comment on `build`). run: > ./gradlew build :grails-shell-cli:installDist groovydoc --continue @@ -165,6 +171,7 @@ jobs: --stacktrace -PonlyCoreTests -PskipCodeStyle + -PskipMicronautProjects buildForge: name: "Build Grails Forge (Java ${{ matrix.java }}, indy=${{ matrix.indy }})" strategy: @@ -257,6 +264,8 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "🏃 Run Functional Tests" + # Micronaut 5 platform GA targets JVM 25 bytecode, so skip the Micronaut + # island on Java 21 jobs (see comment on `build`). run: > ./gradlew bootJar check --continue @@ -268,6 +277,7 @@ jobs: -PskipCodeStyle -PskipHibernate5Tests -PskipMongodbTests + ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} mongodbFunctional: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: "Mongodb Functional Tests (Java ${{ matrix.java }}, MongoDB ${{ matrix.mongodb-version }}, indy=${{ matrix.indy }})" @@ -299,6 +309,8 @@ jobs: - name: "🏃 Run Functional Tests" env: GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + # Micronaut 5 platform GA targets JVM 25 bytecode, so skip the Micronaut + # island on Java 21 jobs (see comment on `build`). run: > ./gradlew bootJar check --continue @@ -308,6 +320,7 @@ jobs: -PonlyMongodbTests -PmongodbContainerVersion=${{ matrix.mongodb-version }} -PskipCodeStyle + ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} hibernate5Functional: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: "Hibernate5 Functional Tests (Java ${{ matrix.java }}, indy=${{ matrix.indy }})" @@ -337,6 +350,8 @@ jobs: - name: "🏃 Run Functional Tests" env: GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + # Micronaut 5 platform GA targets JVM 25 bytecode, so skip the Micronaut + # island on Java 21 jobs (see comment on `build`). run: > ./gradlew bootJar check --continue @@ -345,6 +360,7 @@ jobs: -PgrailsIndy=${{ matrix.indy }} -PonlyHibernate5Tests -PskipCodeStyle + ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} publishGradle: if: github.repository_owner == 'apache' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') needs: [ buildGradle ] From 9bdf78439f112dc7428121bab0e9fdb03b4a842f Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 14:17:00 -0400 Subject: [PATCH 03/13] fix(deps): Bump micronaut-serde-jackson 2.11.0 to 2.16.2 `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 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 71000d8a346..1acf0948d5d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -55,7 +55,7 @@ gradleCycloneDxPluginVersion=3.0.0 # micronaut libraries not in the bom due to the potential for spring mismatches micronautPlatformVersion=5.0.0 micronautRxjava2Version=2.9.0 -micronautSerdeJacksonVersion=2.11.0 +micronautSerdeJacksonVersion=2.16.2 # Pass -PskipMicronautProjects (presence-based, like skipFunctionalTests / skipCodeStyle) # to drop the Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, and From a2ac447dc9a20ad30e41217fcdb75dff698e8ce9 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 14:19:10 -0400 Subject: [PATCH 04/13] ci: Propagate -PskipMicronautProjects to Forge Java 21 jobs 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 --- .github/workflows/gradle.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 22b3d995f6e..b566e5d0992 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -202,6 +202,10 @@ jobs: - name: "🔨 Build project without tests" if: ${{ contains(github.event.head_commit.message, '[skip tests]') }} working-directory: 'grails-forge' + # The Forge composite build includes the root grails-core via + # `includeBuild('..')` in grails-forge/settings.gradle, so on Java 21 + # we must propagate -PskipMicronautProjects to keep the included + # grails-core build from trying to compile the JVM-25 Micronaut island. run: > ./gradlew build --continue @@ -209,9 +213,11 @@ jobs: -PgrailsIndy=${{ matrix.indy }} -PskipCodeStyle -PskipTests + ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} - name: "🔨 Build project with tests" if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} working-directory: 'grails-forge' + # See comment above on '-PskipMicronautProjects'. run: > ./gradlew build --continue @@ -219,6 +225,7 @@ jobs: --stacktrace -PgrailsIndy=${{ matrix.indy }} -PskipCodeStyle + ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} - name: "✅ Verify combined CLI" run: | cd grails-forge From 228f9a52ce58e0a4f675d60d9ef1e7e209025001 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 14:56:43 -0400 Subject: [PATCH 05/13] refactor(deps): Remove Micronaut HTTP client from grails-data-graphql 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` to `org.springframework.http.ResponseEntity`. Spring's `ResponseEntity.getBody()` returns `T` directly (not `Optional`), 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)` 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 --- gradle.properties | 3 - grails-data-graphql/plugin/build.gradle | 17 ++- .../graphql/plugin/testing/GraphQLSpec.groovy | 106 ++++++++++++------ .../graphql/grails-docs-app/build.gradle | 3 - .../grails-multi-datastore-app/build.gradle | 3 - .../groovy/myapp/BarIntegrationSpec.groovy | 2 +- .../groovy/myapp/FooIntegrationSpec.groovy | 2 +- .../graphql/grails-tenant-app/build.gradle | 3 - .../tenant/app/UserIntegrationSpec.groovy | 8 +- .../graphql/grails-test-app/build.gradle | 3 - .../app/ArguedFieldIntegrationSpec.groovy | 6 +- .../test/app/ArtistIntegrationSpec.groovy | 2 +- .../test/app/AuthorIntegrationSpec.groovy | 14 +-- .../test/app/BookIntegrationSpec.groovy | 2 +- .../test/app/CommentIntegrationSpec.groovy | 20 ++-- .../GrailsTeamMemberIntegrationSpec.groovy | 4 +- .../app/InheritanceIntegrationSpec.groovy | 2 +- .../app/NumberLengthIntegrationSpec.groovy | 10 +- .../test/app/PaymentIntegrationSpec.groovy | 24 ++-- .../test/app/PostIntegrationSpec.groovy | 20 ++-- .../test/app/RestrictedIntegrationSpec.groovy | 12 +- .../app/SimpleCompositeIntegrationSpec.groovy | 10 +- .../test/app/SoftDeleteIntegrationSpec.groovy | 12 +- .../grails/test/app/TagIntegrationSpec.groovy | 16 +-- .../test/app/TypeTestIntegrationSpec.groovy | 4 +- .../test/app/UserIntegrationSpec.groovy | 24 ++-- .../test/app/UserRoleIntegrationSpec.groovy | 26 ++--- 27 files changed, 190 insertions(+), 168 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1acf0948d5d..2dc0a0232be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,10 +52,7 @@ apacheRatVersion=0.8.1 gradleChecksumPluginVersion=1.4.0 gradleCycloneDxPluginVersion=3.0.0 -# micronaut libraries not in the bom due to the potential for spring mismatches micronautPlatformVersion=5.0.0 -micronautRxjava2Version=2.9.0 -micronautSerdeJacksonVersion=2.16.2 # Pass -PskipMicronautProjects (presence-based, like skipFunctionalTests / skipCodeStyle) # to drop the Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, and diff --git a/grails-data-graphql/plugin/build.gradle b/grails-data-graphql/plugin/build.gradle index c32901e795b..77988f52b69 100644 --- a/grails-data-graphql/plugin/build.gradle +++ b/grails-data-graphql/plugin/build.gradle @@ -72,16 +72,13 @@ dependencies { // api: HttpServletRequest/Response in GraphqlController } - // GraphQLSpec test trait imports types from io.micronaut.http.* and - // io.micronaut.rxjava2.http.client.* so the rxjava2 client (which transitively - // pulls micronaut-http-client and micronaut-http) is required to compile the - // trait. The trait is only useful from integration tests; the runtime - // dependency is therefore deferred to consumers (the example apps already - // declare it as `implementation`). Keeping it `compileOnly` here avoids - // shipping an unused micronaut HTTP client on every Grails app's runtime - // classpath - test dependencies must not leak onto the production classpath - // post Grails 7. - compileOnly "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version" + // GraphQLSpec test trait talks to the running app over HTTP via + // org.springframework.web.client.RestClient (Spring Boot 4 / Spring 7). The + // trait is only useful from integration tests, and every Grails app already + // has spring-web on its runtime classpath via grails-web-boot, so the + // dependency stays `compileOnly` to avoid shipping anything new on the + // production classpath. + compileOnly 'org.springframework:spring-web' testImplementation project(':grails-testing-support-web') testImplementation 'net.bytebuddy:byte-buddy' diff --git a/grails-data-graphql/plugin/src/main/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpec.groovy b/grails-data-graphql/plugin/src/main/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpec.groovy index 3b6464bf722..32334f5beb4 100644 --- a/grails-data-graphql/plugin/src/main/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpec.groovy +++ b/grails-data-graphql/plugin/src/main/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpec.groovy @@ -19,13 +19,14 @@ package org.grails.gorm.graphql.plugin.testing -import groovy.json.StreamingJsonBuilder +import groovy.json.JsonOutput +import groovy.json.JsonSlurper import groovy.transform.TupleConstructor -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.uri.UriBuilder -import io.micronaut.rxjava2.http.client.RxHttpClient import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.StringHttpMessageConverter +import org.springframework.web.client.RestClient trait GraphQLSpec { @@ -37,7 +38,15 @@ trait GraphQLSpec { GraphQLRequestHelper getGraphQL() { if (_graphql == null) { - _graphql = new GraphQLRequestHelper(rest: RxHttpClient.create(new URL(getServerUrl()))) + StringHttpMessageConverter stringConverter = new StringHttpMessageConverter() + stringConverter.supportedMediaTypes = [MediaType.ALL] + _graphql = new GraphQLRequestHelper(rest: RestClient.builder() + .baseUrl(getServerUrl()) + .messageConverters({ converters -> + converters.clear() + converters.add(stringConverter) + }) + .build()) } _graphql } @@ -56,59 +65,90 @@ trait GraphQLSpec { @TupleConstructor static class GraphQLRequestHelper { - RxHttpClient rest + private static final MediaType APPLICATION_GRAPHQL = MediaType.parseMediaType('application/graphql') + private static final JsonSlurper SLURPER = new JsonSlurper() - HttpResponse graphql(String requestBody) { - rest.exchange(HttpRequest.POST('/graphql', requestBody).contentType('application/graphql'), Map) - .firstOrError().blockingGet() + RestClient rest + + ResponseEntity graphql(String requestBody) { + wrapJson(exchangeGraphql(requestBody)) + } + + // Overload that returns the raw body for callers asserting on the + // unparsed JSON payload (only String is supported - tests asserting on + // a structured body should use the no-class overload above which parses + // into a Map). + @SuppressWarnings('unchecked') + def ResponseEntity graphql(String requestBody, Class bodyType) { + if (bodyType != String) { + throw new IllegalArgumentException( + "graphql(String, Class) only supports String.class; got ${bodyType.name}") + } + (ResponseEntity) exchangeGraphql(requestBody) } - def HttpResponse graphql(String requestBody, Class bodyType) { - rest.exchange(HttpRequest.POST('/graphql', requestBody).contentType('application/graphql'), bodyType) - .firstOrError().blockingGet() + private ResponseEntity exchangeGraphql(String requestBody) { + rest.post() + .uri('/graphql') + .contentType(APPLICATION_GRAPHQL) + .body(requestBody) + .retrieve() + .toEntity(String) } - private HttpResponse buildJsonRequest(Map data) { - rest.exchange(HttpRequest.POST('/graphql', data), Map).firstOrError().blockingGet() + private ResponseEntity buildJsonRequest(Map data) { + wrapJson(rest.post() + .uri('/graphql') + .contentType(MediaType.APPLICATION_JSON) + .body(JsonOutput.toJson(data)) + .retrieve() + .toEntity(String)) } - private HttpResponse buildGetRequest(Map data) { + + private ResponseEntity buildGetRequest(Map data) { if (data.containsKey('variables')) { - StringWriter sw = new StringWriter() - new StreamingJsonBuilder(sw).call(data.variables) - data.put('variables', sw.toString()) + data.put('variables', JsonOutput.toJson(data.variables)) } + wrapJson(rest.get() + .uri('/', { uriBuilder -> + data.each { key, value -> + uriBuilder.queryParam(key, value) + } + uriBuilder.build() + }) + .retrieve() + .toEntity(String)) + } - UriBuilder uriBuilder = UriBuilder.of('/') - data.forEach({ key, value -> - uriBuilder.queryParam(key, value) - }) - - rest.exchange(HttpRequest.GET(uriBuilder.build()), Map).firstOrError().blockingGet() + private static ResponseEntity wrapJson(ResponseEntity raw) { + String body = raw.body + Map parsed = (body == null || body.isEmpty()) ? null : (Map) SLURPER.parseText(body) + new ResponseEntity(parsed, raw.headers, raw.statusCode) } - HttpResponse json(String query) { + ResponseEntity json(String query) { buildJsonRequest([query: query]) } - HttpResponse json(String query, String operationName) { + ResponseEntity json(String query, String operationName) { buildJsonRequest([query: query, operationName: operationName]) } - HttpResponse json(String query, Map variables) { + ResponseEntity json(String query, Map variables) { buildJsonRequest([query: query, variables: variables]) } - HttpResponse json(String query, Map variables, String operationName) { + ResponseEntity json(String query, Map variables, String operationName) { buildJsonRequest([query: query, operationName: operationName, variables: variables]) } - HttpResponse get(String query) { + ResponseEntity get(String query) { buildGetRequest([query: query]) } - HttpResponse get(String query, String operationName) { + ResponseEntity get(String query, String operationName) { buildGetRequest([query: query, operationName: operationName]) } - HttpResponse get(String query, Map variables) { + ResponseEntity get(String query, Map variables) { buildGetRequest([query: query, variables: variables]) } - HttpResponse get(String query, Map variables, String operationName) { + ResponseEntity get(String query, Map variables, String operationName) { buildGetRequest([query: query, operationName: operationName, variables: variables]) } } diff --git a/grails-test-examples/graphql/grails-docs-app/build.gradle b/grails-test-examples/graphql/grails-docs-app/build.gradle index b9cfdb411ef..dc65eb109a8 100644 --- a/grails-test-examples/graphql/grails-docs-app/build.gradle +++ b/grails-test-examples/graphql/grails-docs-app/build.gradle @@ -54,9 +54,6 @@ dependencies { implementation 'org.apache.grails:grails-data-mongodb-gson-templates' implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version" - implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version" - // JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait. - implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion" console 'org.apache.grails:grails-console' profile 'org.apache.grails.profiles:rest-api' diff --git a/grails-test-examples/graphql/grails-multi-datastore-app/build.gradle b/grails-test-examples/graphql/grails-multi-datastore-app/build.gradle index 74d697bc1df..a62db2edf36 100644 --- a/grails-test-examples/graphql/grails-multi-datastore-app/build.gradle +++ b/grails-test-examples/graphql/grails-multi-datastore-app/build.gradle @@ -55,9 +55,6 @@ dependencies { implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version" implementation 'com.graphql-java:graphql-java' - implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version" - // JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait. - implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion" implementation 'com.h2database:h2' implementation 'org.apache.tomcat:tomcat-jdbc' diff --git a/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/BarIntegrationSpec.groovy b/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/BarIntegrationSpec.groovy index 1c1bd3baa97..13f9633e8eb 100644 --- a/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/BarIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/BarIntegrationSpec.groovy @@ -42,7 +42,7 @@ class BarIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.barCreate + Map obj = resp.body.data.barCreate then: 'bar is created in the Mongo datastore with a valid ObjectId' new ObjectId((String) obj.id) diff --git a/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/FooIntegrationSpec.groovy b/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/FooIntegrationSpec.groovy index 10a2644be20..5fa27c3e31c 100644 --- a/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/FooIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/FooIntegrationSpec.groovy @@ -41,7 +41,7 @@ class FooIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.fooCreate + Map obj = resp.body.data.fooCreate then: 'foo is created in the Hibernate datastore' obj.id == 1 diff --git a/grails-test-examples/graphql/grails-tenant-app/build.gradle b/grails-test-examples/graphql/grails-tenant-app/build.gradle index b9cfdb411ef..dc65eb109a8 100644 --- a/grails-test-examples/graphql/grails-tenant-app/build.gradle +++ b/grails-test-examples/graphql/grails-tenant-app/build.gradle @@ -54,9 +54,6 @@ dependencies { implementation 'org.apache.grails:grails-data-mongodb-gson-templates' implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version" - implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version" - // JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait. - implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion" console 'org.apache.grails:grails-console' profile 'org.apache.grails.profiles:rest-api' diff --git a/grails-test-examples/graphql/grails-tenant-app/src/integration-test/groovy/grails/tenant/app/UserIntegrationSpec.groovy b/grails-test-examples/graphql/grails-tenant-app/src/integration-test/groovy/grails/tenant/app/UserIntegrationSpec.groovy index a8a581e5b7f..4d6e840c010 100644 --- a/grails-test-examples/graphql/grails-tenant-app/src/integration-test/groovy/grails/tenant/app/UserIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-tenant-app/src/integration-test/groovy/grails/tenant/app/UserIntegrationSpec.groovy @@ -45,7 +45,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userCreate + Map obj = resp.body.data.userCreate then: "The company is supplied via multi-tenancy" obj.id == 1 @@ -77,7 +77,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data + Map obj = resp.body.data then: "The company is supplied via multi-tenancy" obj.john.name == 'John' @@ -98,7 +98,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List obj = resp.body().data.userList + List obj = resp.body.data.userList then: "The list is filtered by the company" obj.size() == 1 @@ -117,7 +117,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List obj = resp.body().data.userList + List obj = resp.body.data.userList then: "The list is filtered by the company" obj.size() == 2 diff --git a/grails-test-examples/graphql/grails-test-app/build.gradle b/grails-test-examples/graphql/grails-test-app/build.gradle index 9213618f444..5acad3ec71e 100644 --- a/grails-test-examples/graphql/grails-test-app/build.gradle +++ b/grails-test-examples/graphql/grails-test-app/build.gradle @@ -55,9 +55,6 @@ dependencies { implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version" implementation 'com.graphql-java:graphql-java' - implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version" - // JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait. - implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion" console 'org.apache.grails:grails-console' profile 'org.apache.grails.profiles:rest-api' diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArguedFieldIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArguedFieldIntegrationSpec.groovy index 3448cd35cf2..192302cae09 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArguedFieldIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArguedFieldIntegrationSpec.groovy @@ -47,7 +47,7 @@ class ArguedFieldIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.arguedField + def obj = resp.body.data.arguedField then: obj.withArgument == "PONG" @@ -62,7 +62,7 @@ class ArguedFieldIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.arguedField + def obj = resp.body.data.arguedField then: obj.withArgumentList == "P-O-N-G" @@ -77,7 +77,7 @@ class ArguedFieldIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.arguedField + def obj = resp.body.data.arguedField then: obj.withCustomArgument == "PONG" diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArtistIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArtistIntegrationSpec.groovy index 310ab3d9b45..106e6a1ed0d 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArtistIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/ArtistIntegrationSpec.groovy @@ -51,7 +51,7 @@ class ArtistIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def json = resp.body() + def json = resp.body println json.toString() def artists = json.data.artistList def artist = artists[0] diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/AuthorIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/AuthorIntegrationSpec.groovy index 402003798b7..d17c9bb85a5 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/AuthorIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/AuthorIntegrationSpec.groovy @@ -50,7 +50,7 @@ class AuthorIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.authorCreate + def obj = resp.body.data.authorCreate then: obj.id == 1 @@ -82,7 +82,7 @@ class AuthorIntegrationSpec extends Specification implements GraphQLSpec { """) - def obj = resp.body().data.authorCreate + def obj = resp.body.data.authorCreate then: obj.id == 2 @@ -108,7 +108,7 @@ class AuthorIntegrationSpec extends Specification implements GraphQLSpec { """) - def obj = resp.body().data.authorCreate.errors + def obj = resp.body.data.authorCreate.errors then: obj.size() == 1 @@ -130,7 +130,7 @@ class AuthorIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def json = resp.body() + def json = resp.body println json.toString() def authors = json.data.authorList def author1 = authors[0] @@ -166,7 +166,7 @@ class AuthorIntegrationSpec extends Specification implements GraphQLSpec { } """) - def author = resp.body().data.author + def author = resp.body.data.author then: author.id == 2 @@ -197,7 +197,7 @@ class AuthorIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.authorUpdate + def obj = resp.body.data.authorUpdate then: obj.id == 1 @@ -218,7 +218,7 @@ class AuthorIntegrationSpec extends Specification implements GraphQLSpec { """) then: - resp.body().data.authorDelete.success == true + resp.body.data.authorDelete.success == true } void cleanupSpec() { diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/BookIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/BookIntegrationSpec.groovy index 7e4ad9ed8a9..fd714d2fb63 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/BookIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/BookIntegrationSpec.groovy @@ -36,7 +36,7 @@ class BookIntegrationSpec extends Specification implements GraphQLSpec { } """) - def result = resp.body() + def result = resp.body then: result.errors.size() == 1 diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/CommentIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/CommentIntegrationSpec.groovy index 4a0b0ade814..5eab8757b81 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/CommentIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/CommentIntegrationSpec.groovy @@ -46,7 +46,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.commentCreate + Map obj = resp.body.data.commentCreate then: obj.id == 1 @@ -75,7 +75,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.commentCreate + Map obj = resp.body.data.commentCreate then: obj.id == 2 @@ -97,7 +97,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.comment + Map obj = resp.body.data.comment then: obj.id == 1 @@ -129,7 +129,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.comment + Map obj = resp.body.data.comment then: //The parent comment object is not queried obj.parentComment.id == 1 @@ -150,7 +150,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body().data.comment + obj = resp.body.data.comment then: //The parent comment object is queried obj.parentComment.id == 1 @@ -179,7 +179,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.commentCreate + Map obj = resp.body.data.commentCreate then: obj.id == 3 @@ -200,7 +200,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body().data.comment + obj = resp.body.data.comment then: obj.id == 1 @@ -225,7 +225,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.commentUpdate + Map obj = resp.body.data.commentUpdate then: obj.id == 3 @@ -248,7 +248,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List obj = resp.body().data.commentList + List obj = resp.body.data.commentList then: obj[0].id == 1 @@ -278,7 +278,7 @@ class CommentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.commentDelete + Map obj = resp.body.data.commentDelete then: obj.success diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/GrailsTeamMemberIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/GrailsTeamMemberIntegrationSpec.groovy index 36ae5486d02..8824ecc55f8 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/GrailsTeamMemberIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/GrailsTeamMemberIntegrationSpec.groovy @@ -39,7 +39,7 @@ class GrailsTeamMemberIntegrationSpec extends Specification implements GraphQLSp } } """) - Map data = resp.body().data.grailsTeamMemberList + Map data = resp.body.data.grailsTeamMemberList JSONArray results = data.results expect: @@ -63,7 +63,7 @@ class GrailsTeamMemberIntegrationSpec extends Specification implements GraphQLSp } } """) - Map data = resp.body().data.grailsTeamMemberList + Map data = resp.body.data.grailsTeamMemberList JSONArray results = data.results expect: diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/InheritanceIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/InheritanceIntegrationSpec.groovy index f59615c9cb4..f3afca76407 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/InheritanceIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/InheritanceIntegrationSpec.groovy @@ -50,7 +50,7 @@ class InheritanceIntegrationSpec extends Specification implements GraphQLSpec { } } """, String.class) - String data = resp.getBody().get() + String data = resp.getBody() then: data == '{"data":{"mammalList":[{"id":1,"name":"Spot","barks":true},{"id":2,"name":"Chloe","cutenessLevel":100},{"id":3,"name":"Kotlin Ken","language":true}]}}' diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/NumberLengthIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/NumberLengthIntegrationSpec.groovy index 50dedcb8a93..97e534cd92d 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/NumberLengthIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/NumberLengthIntegrationSpec.groovy @@ -42,7 +42,7 @@ class NumberLengthIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map data = resp.body() + Map data = resp.body then: data.data.numberLengthCreate.id @@ -62,7 +62,7 @@ class NumberLengthIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map data = resp.body() + Map data = resp.body then: data.data == null @@ -83,7 +83,7 @@ class NumberLengthIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map data = resp.body() + Map data = resp.body then: data.data == null @@ -104,7 +104,7 @@ class NumberLengthIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map data = resp.body() + Map data = resp.body then: data.data == null @@ -125,7 +125,7 @@ class NumberLengthIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map data = resp.body() + Map data = resp.body then: data.data == null diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PaymentIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PaymentIntegrationSpec.groovy index 057211dbf9a..397f8301083 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PaymentIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PaymentIntegrationSpec.groovy @@ -40,7 +40,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } """) - Map result = resp.body() + Map result = resp.body then: result.errors.size() == 1 @@ -65,7 +65,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.creditCardPaymentCreate + Map obj = resp.body.data.creditCardPaymentCreate then: obj.id @@ -85,7 +85,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.creditCardPayment + Map obj = resp.body.data.creditCardPayment then: obj.id @@ -104,7 +104,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map json = resp.body() + Map json = resp.body obj = json.data.payment then: @@ -122,7 +122,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body() + obj = resp.body then: 'An error is returned' obj.data == null @@ -153,7 +153,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List obj = resp.body().data.creditCardPaymentList + List obj = resp.body.data.creditCardPaymentList then: obj.size() == 2 @@ -169,7 +169,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body().data.paymentList + obj = resp.body.data.paymentList then: obj.size() == 2 @@ -191,7 +191,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.creditCardPaymentUpdate + Map obj = resp.body.data.creditCardPaymentUpdate then: obj.id == 1 @@ -210,7 +210,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body() + obj = resp.body then: 'An error is thrown' obj.data == null @@ -229,7 +229,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body().data.paymentUpdate + obj = resp.body.data.paymentUpdate then: obj.amount == new BigDecimal('2') @@ -244,7 +244,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.creditCardPaymentDelete + Map obj = resp.body.data.creditCardPaymentDelete then: obj.success @@ -257,7 +257,7 @@ class PaymentIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body().data.paymentDelete + obj = resp.body.data.paymentDelete then: obj.success diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PostIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PostIntegrationSpec.groovy index 409bcdeef17..d746ffe2778 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PostIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/PostIntegrationSpec.groovy @@ -54,7 +54,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.postCreate + def obj = resp.body.data.postCreate then: obj.id @@ -96,7 +96,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.postCreate + def obj = resp.body.data.postCreate postId = obj.id tagId = obj?.tags?.find { it.name == 'Grails' }?.id tag2Id = obj?.tags?.find { it.name == 'Groovy' }?.id @@ -137,7 +137,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.postCreate + def obj = resp.body.data.postCreate post2Id = obj.id then: @@ -172,7 +172,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.postUpdate + def obj = resp.body.data.postUpdate then: obj.id @@ -196,7 +196,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.postList + def obj = resp.body.data.postList then: obj.size() == 2 @@ -215,7 +215,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.postList + def obj = resp.body.data.postList then: obj.size() == 1 @@ -229,7 +229,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body().data.postList + obj = resp.body.data.postList then: obj.size() == 1 @@ -245,7 +245,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.post + def obj = resp.body.data.post then: obj.title == 'Grails 3.5 Release' @@ -260,7 +260,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def obj = resp.body().data.postDelete + def obj = resp.body.data.postDelete then: obj.success @@ -284,7 +284,7 @@ class PostIntegrationSpec extends Specification implements GraphQLSpec { } } """) - resp.body().data.tagList.each { + resp.body.data.tagList.each { graphQL.graphql(""" mutation { tagDelete(id: ${it.id}) { diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/RestrictedIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/RestrictedIntegrationSpec.groovy index bb543d482f4..0d24ae57d0c 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/RestrictedIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/RestrictedIntegrationSpec.groovy @@ -41,7 +41,7 @@ class RestrictedIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.restrictedCreate + def obj = resp.body.data.restrictedCreate then: obj.id == 1 @@ -58,7 +58,7 @@ class RestrictedIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.restrictedDelete + def obj = resp.body.data.restrictedDelete then: "the registered interceptor prevented the action" obj == null @@ -77,7 +77,7 @@ class RestrictedIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.restrictedUpdate + def obj = resp.body.data.restrictedUpdate then: "the registered interceptor prevented the action" obj == null @@ -94,7 +94,7 @@ class RestrictedIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.restricted + def obj = resp.body.data.restricted then: "the registered interceptor prevented the action" obj.id == 1 @@ -109,7 +109,7 @@ class RestrictedIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.restrictedCount + def obj = resp.body.data.restrictedCount then: "the registered interceptor prevented the action" obj == 1 @@ -126,7 +126,7 @@ class RestrictedIntegrationSpec extends Specification implements GraphQLSpec { } """) - def obj = resp.body().data.restrictedList + def obj = resp.body.data.restrictedList then: "the registered interceptor prevented the action" obj.size() == 1 diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SimpleCompositeIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SimpleCompositeIntegrationSpec.groovy index ef4edeb821d..d93ae386af2 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SimpleCompositeIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SimpleCompositeIntegrationSpec.groovy @@ -45,7 +45,7 @@ class SimpleCompositeIntegrationSpec extends Specification implements GraphQLSpe } } """) - Map obj = resp.body().data.simpleCompositeCreate + Map obj = resp.body.data.simpleCompositeCreate then: obj.title == 'x' @@ -66,7 +66,7 @@ class SimpleCompositeIntegrationSpec extends Specification implements GraphQLSpe } } """) - Map obj = resp.body().data.simpleCompositeUpdate + Map obj = resp.body.data.simpleCompositeUpdate then: obj.title == 'x' @@ -85,7 +85,7 @@ class SimpleCompositeIntegrationSpec extends Specification implements GraphQLSpe } } """) - Map obj = resp.body().data.simpleComposite + Map obj = resp.body.data.simpleComposite then: obj.title == 'x' @@ -104,7 +104,7 @@ class SimpleCompositeIntegrationSpec extends Specification implements GraphQLSpe } } """) - List obj = resp.body().data.simpleCompositeList + List obj = resp.body.data.simpleCompositeList then: obj.size() == 1 @@ -122,7 +122,7 @@ class SimpleCompositeIntegrationSpec extends Specification implements GraphQLSpe } } """) - Map obj = resp.body().data.simpleCompositeDelete + Map obj = resp.body.data.simpleCompositeDelete then: obj.success diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SoftDeleteIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SoftDeleteIntegrationSpec.groovy index b1d1b9a8a7d..180512a0004 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SoftDeleteIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/SoftDeleteIntegrationSpec.groovy @@ -43,7 +43,7 @@ class SoftDeleteIntegrationSpec extends Specification implements GraphQLSpec { } } """) - id = resp.body().data.softDeleteCreate.id + id = resp.body.data.softDeleteCreate.id assert id != null } @@ -56,7 +56,7 @@ class SoftDeleteIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def json = resp.body().data.softDelete + def json = resp.body.data.softDelete then: json.name == 'foo' @@ -71,7 +71,7 @@ class SoftDeleteIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List json = resp.body().data.softDeleteList + List json = resp.body.data.softDeleteList then: json.size() == 1 @@ -87,7 +87,7 @@ class SoftDeleteIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def json = resp.body().data.softDeleteDelete + def json = resp.body.data.softDeleteDelete SoftDelete softDelete SoftDelete.withNewSession { softDelete = SoftDelete.get(id) @@ -108,7 +108,7 @@ class SoftDeleteIntegrationSpec extends Specification implements GraphQLSpec { } } """) - def json = resp.body().data.softDelete + def json = resp.body.data.softDelete then: json == null @@ -123,7 +123,7 @@ class SoftDeleteIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List json = resp.body().data.softDeleteList + List json = resp.body.data.softDeleteList then: json.empty diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TagIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TagIntegrationSpec.groovy index 7b8cfe1c98f..0c6c5f92b0d 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TagIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TagIntegrationSpec.groovy @@ -53,7 +53,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List obj = resp.body().data.postCreate.tags + List obj = resp.body.data.postCreate.tags def grails = obj.find { it.name == 'Grails' }.id grailsId = grails def groovy = obj.find { it.name == 'Groovy' }.id @@ -76,7 +76,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { } } """) - assert resp.body().data.postCreate.tags.size() == 3 + assert resp.body.data.postCreate.tags.size() == 3 } void "test getting the count"() { @@ -86,7 +86,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { tagCount } """) - def obj = resp.body().data.tagCount + def obj = resp.body.data.tagCount then: obj == 4 @@ -105,7 +105,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List obj = resp.body().data.tagList + List obj = resp.body.data.tagList then: obj.size() == 4 @@ -141,7 +141,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.tag + Map obj = resp.body.data.tag then: //queries.size() == 2 ignored due to GORM issue https://github.com/apache/grails-data-mapping/issues/989 @@ -166,7 +166,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.tagUpdate + Map obj = resp.body.data.tagUpdate then: obj.id == grailsId @@ -187,7 +187,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { } } """) - resp.body().data.postList.each { + resp.body.data.postList.each { graphQL.graphql(""" mutation { postDelete(id: ${it.id}) { @@ -203,7 +203,7 @@ class TagIntegrationSpec extends Specification implements GraphQLSpec { } } """) - resp.body().data.tagList.each { + resp.body.data.tagList.each { graphQL.graphql(""" mutation { tagDelete(id: ${it.id}) { diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TypeTestIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TypeTestIntegrationSpec.groovy index d1552929c62..6267e7d29c9 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TypeTestIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/TypeTestIntegrationSpec.groovy @@ -81,7 +81,7 @@ class TypeTestIntegrationSpec extends Specification implements GraphQLSpec { } """) - Map json = resp.body().data.typeTestCreate + Map json = resp.body.data.typeTestCreate then: json.id @@ -139,7 +139,7 @@ class TypeTestIntegrationSpec extends Specification implements GraphQLSpec { charPrimitive: "x", booleanPrimitive: true]]) - Map json = resp.body().data.typeTestCreate + Map json = resp.body.data.typeTestCreate then: json.id diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy index c0dd26b71b5..87f29c71a4e 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy @@ -50,7 +50,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body() + Map obj = resp.body then: obj.data == null @@ -78,7 +78,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body() + obj = resp.body then: obj.data == null @@ -104,7 +104,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body() + Map obj = resp.body then: obj.data == null @@ -132,7 +132,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body() + obj = resp.body then: obj.data == null @@ -177,7 +177,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userCreate + Map obj = resp.body.data.userCreate managerId = obj.id as Long then: @@ -231,7 +231,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userCreate + Map obj = resp.body.data.userCreate subordinateId = obj.id as Long then: @@ -282,7 +282,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userUpdate + Map obj = resp.body.data.userUpdate then: obj.id == subordinateId @@ -320,7 +320,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - List obj = resp.body().data.userList + List obj = resp.body.data.userList then: JSONObject subordinate = obj.find { it.id == subordinateId } @@ -372,7 +372,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map json = resp.body() + Map json = resp.body JSONObject obj = json.data.user then: @@ -397,7 +397,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userDelete + Map obj = resp.body.data.userDelete then: !obj.success @@ -412,7 +412,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userDelete + Map obj = resp.body.data.userDelete then: obj.success @@ -427,7 +427,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userDelete + Map obj = resp.body.data.userDelete then: obj.success diff --git a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserRoleIntegrationSpec.groovy b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserRoleIntegrationSpec.groovy index de7b00b2c43..14f4e341822 100644 --- a/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserRoleIntegrationSpec.groovy +++ b/grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserRoleIntegrationSpec.groovy @@ -58,7 +58,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userCreate + Map obj = resp.body.data.userCreate userId = obj.id resp = graphQL.graphql(""" @@ -70,7 +70,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """) - obj = resp.body().data.roleCreate + obj = resp.body.data.roleCreate roleId = obj.id } @@ -97,7 +97,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Map obj = resp.body().data.userRoleCreate + Map obj = resp.body.data.userRoleCreate then: obj.user.profile.email == 'admin@email.com' @@ -130,7 +130,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - Map obj = resp.body().data.userRole + Map obj = resp.body.data.userRole then: obj.user.id == userId @@ -154,7 +154,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - obj = resp.body().data.userRole + obj = resp.body.data.userRole then: 'The user and role will be fetched with the same query' obj.user.profile.email == 'admin@email.com' @@ -182,7 +182,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - Map result = resp.body() + Map result = resp.body then: result.errors.size() == 1 @@ -205,7 +205,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - List obj = resp.body().data.userRoleList + List obj = resp.body.data.userRoleList then: obj.size() == 1 @@ -224,7 +224,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - List obj = resp.body().data.usersByRole + List obj = resp.body.data.usersByRole then: obj.size() == 1 @@ -242,7 +242,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """) - Long newRoleId = resp.body().data.roleCreate.id + Long newRoleId = resp.body.data.roleCreate.id graphQL.graphql(""" mutation { userRoleCreate(userRole: { @@ -273,7 +273,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - List list = resp.body().data.userRoleList + List list = resp.body.data.userRoleList then: list.size() == 2 @@ -286,7 +286,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - Map obj = resp.body().data.revokeAllRoles + Map obj = resp.body.data.revokeAllRoles then: obj.success @@ -304,7 +304,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - list = resp.body().data.userRoleList + list = resp.body.data.userRoleList then: 'Check if the delete worked' list.empty @@ -340,7 +340,7 @@ class UserRoleIntegrationSpec extends Specification implements GraphQLSpec { } } """.toString()) - Map obj = resp.body().data.userRoleDelete + Map obj = resp.body.data.userRoleDelete then: obj.success From d6bfccbc7a15c01178030ce2a717605dbfa94246 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 15:04:23 -0400 Subject: [PATCH 06/13] ci: Add publishMicronaut job on JDK 25, skip island on the JDK 21 publish 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 --- .github/workflows/gradle.yml | 53 +++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b566e5d0992..dfc83c1cb15 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -439,6 +439,10 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "📤 Publish Grails-Core Snapshot Artifacts" + # -PskipMicronautProjects keeps this Java 21 publish from trying to compile + # the Micronaut 5 / JVM 25 island. The Micronaut artifacts (grails-micronaut, + # grails-micronaut-bom) are published by the parallel publishMicronaut job + # below, which runs on JDK 25. uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 env: GRAILS_PUBLISH_RELEASE: 'false' @@ -449,7 +453,7 @@ jobs: timeout_seconds: 1200 # normal range 14min if build is not cached (no tests running) max_attempts: 3 # Attempts to address: Could not write to resource 'https://repository.apache.org/content/repositories/snapshots/...' Read timed out retry_wait_seconds: 180 - command: ./gradlew publish aggregateChecksums aggregatePublishedArtifacts --no-build-cache --rerun-tasks + command: ./gradlew publish aggregateChecksums aggregatePublishedArtifacts --no-build-cache --rerun-tasks -PskipMicronautProjects - name: "📤 Upload grails-core checksums" uses: actions/upload-artifact@v7.0.1 with: @@ -472,6 +476,53 @@ jobs: with: name: apache-grails-wrapper-SNAPSHOT-bin path: build/tmp/wrapper + publishMicronaut: + # Micronaut 5.0.0 publishes JARs targeting JVM 25 bytecode, so the Micronaut + # "island" (grails-micronaut, grails-micronaut-bom) must publish from a JDK 25 + # runner. The sibling `publish` job runs on JDK 21 with + # -PskipMicronautProjects to publish everything else; this job publishes the + # two Micronaut artifacts. The two test-example projects in the island are + # not published. + needs: [ publishGradle, build, functional, hibernate5Functional, mongodbFunctional ] + if: >- + ${{ always() && + github.repository_owner == 'apache' && + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + needs.publishGradle.result == 'success' && + (needs.build.result == 'success' || needs.build.result == 'skipped') && + (needs.functional.result == 'success' || needs.functional.result == 'skipped') && + (needs.hibernate5Functional.result == 'success' || needs.hibernate5Functional.result == 'skipped') && + (needs.mongodbFunctional.result == 'success' || needs.mongodbFunctional.result == 'skipped') + }} + runs-on: ubuntu-24.04 + steps: + - name: "Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + - name: "☕️ Setup JDK" + uses: actions/setup-java@v4 + with: + distribution: liberica + java-version: 25 + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + - name: "🔍 Setup TestLens" + uses: testlens-app/setup-testlens@v1 + - name: "📤 Publish Grails-Micronaut Snapshot Artifacts" + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + env: + GRAILS_PUBLISH_RELEASE: 'false' + MAVEN_PUBLISH_URL: ${{ secrets.GRAILS_NEXUS_PUBLISH_SNAPSHOT_URL }} + MAVEN_PUBLISH_USERNAME: ${{ secrets.NEXUS_USER }} + MAVEN_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PW }} + with: + timeout_seconds: 1200 + max_attempts: 3 + retry_wait_seconds: 180 + command: ./gradlew :grails-micronaut:publish :grails-micronaut-bom:publish --no-build-cache --rerun-tasks publishForge: if: github.repository_owner == 'apache' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') needs: [ buildForge, publishGradle, publish ] From 79b826098687a61b53d5448d47381c2b6ea9f765 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 15:30:05 -0400 Subject: [PATCH 07/13] ci(release): Split publish flow into JDK 21 + JDK 25 (Micronaut island) 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 -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 --- .github/workflows/release-publish-docs.yml | 6 ++- .github/workflows/release.yml | 62 +++++++++++++++++++--- .sdkmanrc | 8 ++- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-publish-docs.yml b/.github/workflows/release-publish-docs.yml index 44ff29980bc..47111fedb96 100644 --- a/.github/workflows/release-publish-docs.yml +++ b/.github/workflows/release-publish-docs.yml @@ -67,7 +67,11 @@ jobs: with: develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: "📖 Generate Documentation" - run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} + # The Grails-Micronaut "island" requires JDK 25 (Micronaut 5 bytecode); + # this docs build runs on the JDK 21 reproducibility pin, so prune the + # island from the project graph to avoid Gradle resolving its JVM 25 + # variants. The docs themselves have no code dependency on Micronaut. + run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects - name: "🚀 Publish to GitHub Pages" uses: apache/grails-github-actions/deploy-github-pages@asf env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d9d0f5bd92d..5a2bfc8682d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,7 @@ env: GRAILS_PUBLISH_RELEASE: 'true' JAVA_DISTRIBUTION: liberica JAVA_VERSION: 21.0.7 # this must be a specific version for reproducible builds, keep it synced with .sdkmanrc and verification container + JAVA_VERSION_MICRONAUT: 25.0.3 # the Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom) is built against Micronaut 5 which targets JVM 25 bytecode. Keep this synced with the secondary JDK installed in etc/bin/Dockerfile and the JDK_25_HOME branch in etc/bin/verify-reproducible.sh. PROJECT_DESC: > Grails is a powerful Groovy-based web application framework for the JVM, built on top of Spring Boot, and supported by a rich ecosystem of plugins @@ -92,12 +93,19 @@ jobs: - name: "🔍 Validate dependency versions" run: ./gradlew validateDependencyVersions - name: "🧩 Run grails-core assemble" - run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} + # Pre-publish smoke check on JDK 21. The Micronaut island is built and + # smoke-checked implicitly by the JDK 25 publishToSonatype step below; + # no separate JDK-25 assemble step is needed here. + run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects - name: "🧩 Run grails-forge assemble" working-directory: grails-forge - run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} + run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects - name: "📦 Generate grails-core docs (to assert that is works, before proceeding)" - run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} + # grails-doc has no code dependency on the Micronaut island, but the + # configuration phase still evaluates all sibling projects in the + # multi-project build. -PskipMicronautProjects prunes the island from + # the project graph so Gradle does not try to resolve its JDK 25 deps. + run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects - name: "🔏 Sign grails-wrapper ZIP" run: > gpg @@ -173,6 +181,10 @@ jobs: aggregateChecksums aggregatePublishedArtifacts - name: "📤 Publish Grails Core to Staging Repository" + # -PskipMicronautProjects excludes the Grails-Micronaut "island" from + # this JDK 21 publish. The two island artifacts (grails-micronaut, + # grails-micronaut-bom) are signed and staged by the + # `Publish Grails-Micronaut` step below, which switches to JDK 25. env: NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} @@ -187,7 +199,11 @@ jobs: publishToSonatype aggregateChecksums aggregatePublishedArtifacts + -PskipMicronautProjects - name: "📤 Publish Grails Forge to Staging Repository" + # The Forge composite build does includeBuild('..') so it pulls in the + # root grails-core build. Without -PskipMicronautProjects the Micronaut + # island would be evaluated on JDK 21 and fail to resolve. env: NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} @@ -203,6 +219,40 @@ jobs: publishToSonatype aggregateChecksums aggregatePublishedArtifacts + -PskipMicronautProjects + - name: "☕️ Switch to JDK 25 for Micronaut publish" + # Micronaut 5 platform GA targets JVM 25 bytecode, so the island + # artifacts (grails-micronaut, grails-micronaut-bom) must be built and + # staged from a JDK 25 runner. This is a NEW reproducibility pin - + # keep $JAVA_VERSION_MICRONAUT synced with the secondary JDK in + # etc/bin/Dockerfile so verifiers can reproduce the resulting JARs. + uses: actions/setup-java@v4 + with: + distribution: liberica + java-version: ${{ env.JAVA_VERSION_MICRONAUT }} + - name: "📤 Publish Grails-Micronaut to Staging Repository" + env: + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} + NEXUS_PUBLISH_URL: ${{ vars.STAGING_URL }} + NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.STAGING_PROFILE_ID }} + NEXUS_PUBLISH_DESCRIPTION: '${{ env.REPO_NAME }}:${{ env.VERSION }}' + SIGNING_KEY: ${{ secrets.GPG_KEY_ID }} + run: > + ./gradlew + -x initializeSonatypeStagingRepository + findSonatypeStagingRepository + :grails-micronaut:publishToSonatype + :grails-micronaut-bom:publishToSonatype + - name: "☕️ Restore JDK 21 for staging-repo close" + # Symmetry with the rest of the release flow - the close step and the + # downstream checksum/artifact-list combination steps all expect the + # default JDK 21 toolchain. Also keeps any future steps that touch the + # repository's own (non-Micronaut) Gradle config on the documented JDK. + uses: actions/setup-java@v4 + with: + distribution: liberica + java-version: ${{ env.JAVA_VERSION }} - name: "✅ Close Staging Repository" env: NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} @@ -610,7 +660,7 @@ jobs: with: develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: "📖 Generate Documentation" - run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} + run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects - name: "🚀 Publish to GitHub Pages" uses: apache/grails-github-actions/deploy-github-pages@asf env: @@ -650,14 +700,14 @@ jobs: - name: "🚀 Grails SDK Minor Release" if: contains(env.VERSION, 'M') || contains(env.VERSION, 'RC') working-directory: grails-forge - run: ./gradlew sdkMinorRelease + run: ./gradlew sdkMinorRelease -PskipMicronautProjects env: GVM_SDKVENDOR_KEY: ${{ secrets.GVM_SDKVENDOR_KEY }} GVM_SDKVENDOR_TOKEN: ${{ secrets.GVM_SDKVENDOR_TOKEN }} - name: "🚀 Grails SDK Major Release" if: startsWith(env.VERSION, '7.') && !contains(env.VERSION, 'M') && !contains(env.VERSION, 'RC') working-directory: grails-forge - run: ./gradlew sdkMajorRelease + run: ./gradlew sdkMajorRelease -PskipMicronautProjects env: GVM_SDKVENDOR_KEY: ${{ secrets.GVM_SDKVENDOR_KEY }} GVM_SDKVENDOR_TOKEN: ${{ secrets.GVM_SDKVENDOR_TOKEN }} diff --git a/.sdkmanrc b/.sdkmanrc index 58b9f3d4e9b..7b10919442f 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,4 +1,10 @@ -# Keep java version synced with .github/workflows/release.yml and etc/bin/Dockerfile +# Keep java version synced with .github/workflows/release.yml ($JAVA_VERSION) and etc/bin/Dockerfile (primary JDK). +# This is the default JDK for all of grails-core EXCEPT the Grails-Micronaut "island" +# (grails-micronaut, grails-micronaut-bom), which is built against Micronaut 5 / JVM 25 +# bytecode. The release workflow installs a secondary Liberica JDK pinned via +# $JAVA_VERSION_MICRONAUT in release.yml; for local verification, install that JDK 25 +# alongside this one (sdk install java -librca) and follow the dual-JDK +# instructions in RELEASE.md "Manual Verification: Reproducible Jar Files". java=21.0.7-librca # Keep gradle version synced with gradle.properties (gradleToolingApiVersion). # Update the gradle-bootstrap project to propagate the version to all gradle-wrapper.properties files. From e7552a729e371669e678e17a7ec0f6f1862dd934 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 15:34:14 -0400 Subject: [PATCH 08/13] ci(release): Dockerfile + verify scripts learn the dual-JDK flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- etc/bin/Dockerfile | 26 ++++++++++++++++- etc/bin/test-reproducible-builds.sh | 44 ++++++++++++++++++----------- etc/bin/verify-reproducible.sh | 27 ++++++++++++++++-- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/etc/bin/Dockerfile b/etc/bin/Dockerfile index 675049eb75e..5215e39a1d5 100644 --- a/etc/bin/Dockerfile +++ b/etc/bin/Dockerfile @@ -16,12 +16,36 @@ # for testing in a container that is similar to the grails github action linux build environment # run this from the root of the project # `docker build -t grails:testing -f etc/bin/Dockerfile . && docker run -it --rm -v $(pwd):/home/groovy/project grails:testing bash` -# Keep java version synced with .sdkmanrc and .github/workflows/release.yml +# Keep java version synced with .sdkmanrc and .github/workflows/release.yml ($JAVA_VERSION) FROM bellsoft/liberica-openjdk-debian:21.0.7 USER root RUN apt-get update && apt-get install -y curl unzip coreutils libdigest-sha-perl gpg vim sudo psmisc locales groovy rsync nano +# Secondary Liberica JDK for the Grails-Micronaut "island" (grails-micronaut, +# grails-micronaut-bom). Micronaut 5 platform GA targets JVM 25 bytecode, so +# those two artifacts cannot be built or reproduced on the primary JDK 21 +# above. The verify scripts (etc/bin/verify-reproducible.sh, +# etc/bin/test-reproducible-builds.sh) switch JAVA_HOME to ${JDK_25_HOME} when +# building the island, and switch back to the default for everything else. +# Keep $JDK_25_VERSION synced with $JAVA_VERSION_MICRONAUT in +# .github/workflows/release.yml. +ENV JDK_25_VERSION=25.0.3+11 +ENV JDK_25_HOME=/opt/liberica-jdk25 +RUN set -eu; \ + DPKG_ARCH=$(dpkg --print-architecture); \ + case "${DPKG_ARCH}" in \ + amd64) JDK_ARCH=linux-amd64; JDK_SHA512=54e58ec3f34a20dcf6f0bd607e15c47d1f3c26ff1cfe1ecf107a862c3df7b58a9d39c0a8edf01038e83d838c28956b607e7382dda0059c04c2be5b9c0bdfa7c3 ;; \ + arm64) JDK_ARCH=linux-aarch64; JDK_SHA512=fb22b6f50186d76e19adbf990d83de4c32fee940b1f1150756529c7722254da36ff9872c4cdd342346e59d2211ae9f41d73ddf79b612dce4d7294d15c37c9349 ;; \ + *) echo "Unsupported architecture for JDK 25 install: ${DPKG_ARCH}" >&2; exit 1 ;; \ + esac; \ + curl -fsSL -o /tmp/liberica-jdk25.tar.gz "https://download.bell-sw.com/java/${JDK_25_VERSION}/bellsoft-jdk${JDK_25_VERSION}-${JDK_ARCH}.tar.gz"; \ + echo "${JDK_SHA512} /tmp/liberica-jdk25.tar.gz" | sha512sum -c -; \ + mkdir -p "${JDK_25_HOME}"; \ + tar -xzf /tmp/liberica-jdk25.tar.gz -C "${JDK_25_HOME}" --strip-components=1; \ + rm /tmp/liberica-jdk25.tar.gz; \ + "${JDK_25_HOME}/bin/java" -version + RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8 diff --git a/etc/bin/test-reproducible-builds.sh b/etc/bin/test-reproducible-builds.sh index 6ebc60cd0e9..99b1126425a 100755 --- a/etc/bin/test-reproducible-builds.sh +++ b/etc/bin/test-reproducible-builds.sh @@ -28,28 +28,40 @@ cd "${SCRIPT_DIR}/../.." rm -rf "${SCRIPT_DIR}/results" || true mkdir -p "${SCRIPT_DIR}/results" +if [[ -z "${JDK_25_HOME:-}" ]]; then + echo "❌ JDK_25_HOME is not set; the Grails-Micronaut island requires a separate Liberica JDK 25 install." + echo " Install Liberica JDK matching JAVA_VERSION_MICRONAUT in .github/workflows/release.yml," + echo " then export JDK_25_HOME=/path/to/jdk before running this script." + exit 1 +fi + +build_all() { + # JDK 21 (default) pass across the three composites, Micronaut island skipped. + killall -e java || true + cd grails-gradle + ./gradlew build --rerun-tasks -PskipTests --no-build-cache + cd .. + ./gradlew build --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects + cd grails-forge + ./gradlew build --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects + cd .. + + # JDK 25 pass: the Grails-Micronaut island only. + killall -e java || true + JAVA_HOME="${JDK_25_HOME}" PATH="${JDK_25_HOME}/bin:${PATH}" \ + ./gradlew :grails-micronaut:build :grails-micronaut-bom:build \ + --rerun-tasks -PskipTests --no-build-cache + killall -e java || true +} + git clean -xdf --exclude='etc/bin' --exclude='.idea' --exclude='.gradle' -killall -e java || true -cd grails-gradle -./gradlew build --rerun-tasks -PskipTests --no-build-cache -cd .. -./gradlew build --rerun-tasks -PskipTests --no-build-cache -cd grails-forge -./gradlew build --rerun-tasks -PskipTests --no-build-cache -cd .. +build_all "${SCRIPT_DIR}/generate-build-artifact-hashes.groovy" > "${SCRIPT_DIR}/results/first.txt" mkdir -p "${SCRIPT_DIR}/results/first" find . -path ./etc -prune -o -type f -path '*/build/libs/*.jar' -print0 | xargs -0 cp --parents -t "${SCRIPT_DIR}/results/first/" git clean -xdf --exclude='etc/bin' --exclude='.idea' --exclude='.gradle' -killall -e java || true -cd grails-gradle -./gradlew build --rerun-tasks -PskipTests --no-build-cache -cd .. -./gradlew build --rerun-tasks -PskipTests --no-build-cache -cd grails-forge -./gradlew build --rerun-tasks -PskipTests --no-build-cache -cd .. +build_all "${SCRIPT_DIR}/generate-build-artifact-hashes.groovy" > "${SCRIPT_DIR}/results/second.txt" mkdir -p "${SCRIPT_DIR}/results/second" find . -path ./etc -prune -o -type f -path '*/build/libs/*.jar' -print0 | xargs -0 cp --parents -t "${SCRIPT_DIR}/results/second/" diff --git a/etc/bin/verify-reproducible.sh b/etc/bin/verify-reproducible.sh index 824253b7143..0e9028f596d 100755 --- a/etc/bin/verify-reproducible.sh +++ b/etc/bin/verify-reproducible.sh @@ -58,13 +58,36 @@ else fi killall -e java || true + +# JDK 21 (default) pass: grails-gradle composite (no Micronaut island), root +# (Micronaut island skipped), grails-forge composite (transitively pulls in +# the root build via includeBuild('..'), island skipped there too). cd grails-gradle ./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache cd .. -./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects cd grails-forge -./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects cd .. + +# JDK 25 pass: the Grails-Micronaut "island" only (grails-micronaut, +# grails-micronaut-bom). Micronaut 5 platform GA targets JVM 25 bytecode so +# these two artifacts cannot be reproduced on JDK 21. The verification +# container provides ${JDK_25_HOME}; for local verification outside the +# container, install Liberica JDK matching $JAVA_VERSION_MICRONAUT in +# release.yml and export JDK_25_HOME before running this script. +if [[ -z "${JDK_25_HOME:-}" ]]; then + echo "❌ JDK_25_HOME is not set; the Grails-Micronaut island requires a separate Liberica JDK 25 install." + echo " In the verification container this is set automatically. Outside the container, install Liberica JDK" + echo " matching JAVA_VERSION_MICRONAUT in .github/workflows/release.yml and export JDK_25_HOME=/path/to/jdk." + exit 1 +fi +killall -e java || true +echo "Switching to JDK 25 at ${JDK_25_HOME} for the Micronaut island..." +JAVA_HOME="${JDK_25_HOME}" PATH="${JDK_25_HOME}/bin:${PATH}" \ + ./gradlew :grails-micronaut:publishToMavenLocal :grails-micronaut-bom:publishToMavenLocal \ + --rerun-tasks -PskipTests --no-build-cache +killall -e java || true echo "Generating Checksums for Built Jars" "${SCRIPT_DIR}/generate-build-artifact-hashes.groovy" "${DOWNLOAD_LOCATION}/grails" > "${DOWNLOAD_LOCATION}/grails/etc/bin/results/second.txt" if [ -e "${DOWNLOAD_LOCATION}/grails/etc/bin/results/second.txt" ] && [ ! -s "${DOWNLOAD_LOCATION}/grails/etc/bin/results/second.txt" ]; then From 72a3c0a514aa5b70f5af83f191073e749f8d0ef6 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 22 May 2026 15:35:26 -0400 Subject: [PATCH 09/13] docs(release): Document the dual-JDK reproducibility flow 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 --- RELEASE.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 9903369da42..94ffdeb0860 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -142,6 +142,15 @@ After all jar files are verified to be signed by a valid Grails key, we need to Further details on the building can be found in the [INSTALL](INSTALL) document. Otherwise, run the `verify-reproducible.sh` shell script to compare the published jar files to a locally built version of them. +#### Dual-JDK requirement for the Grails-Micronaut island + +Grails 8 release artifacts come from TWO different JDKs and therefore TWO different reproducibility pins: + +- **Primary JDK (`$JAVA_VERSION` in `release.yml`, also in `.sdkmanrc` and the primary `FROM` in `etc/bin/Dockerfile`)**: builds every published artifact EXCEPT the Grails-Micronaut "island" (`grails-micronaut`, `grails-micronaut-bom`). +- **Secondary JDK (`$JAVA_VERSION_MICRONAUT` in `release.yml`, installed alongside the primary in `etc/bin/Dockerfile` and exposed as `$JDK_25_HOME`)**: builds the two Micronaut island artifacts. The Micronaut 5 platform GA targets JVM 25 bytecode, so these two JARs cannot be reproduced on the primary JDK. + +The verify script understands both. Inside the verification container, `JDK_25_HOME` is already set so `verify-reproducible.sh` "just works". Outside the container, manual verifiers MUST install Liberica JDK matching `$JAVA_VERSION_MICRONAUT` from `release.yml` (for example via `sdk install java -librca`) and export `JDK_25_HOME=/path/to/jdk25` before running the script - otherwise the script fails fast with a clear error. + If there are any jar file differences, confirm they are relevant by following the following steps: 1. Extract the differing jar file using the `etc/bin/extract-build-artifact.sh ` 2. In IntelliJ, under `etc/bin/results` there will now be a `firstArtifact` & `secondArtifact` folder. Select them both, right click, and select `Compared Directories` @@ -401,6 +410,12 @@ Setup the key for validity: The Grails image is officially built on linux in a GitHub action using an Ubuntu container. To run a linux container locally, you can use the following command (substitute `` with the tag name): +The verification container ships with BOTH JDKs needed for full reproducible verification: the primary Liberica JDK +(`$JAVA_VERSION`, default on `PATH`/`JAVA_HOME`) and the secondary Liberica JDK 25 for the Grails-Micronaut "island" +(installed at `$JDK_25_HOME`). `verify-reproducible.sh` uses both automatically - no manual JDK switching required +inside the container. Both pins live in `etc/bin/Dockerfile` and must stay synced with +`$JAVA_VERSION` / `$JAVA_VERSION_MICRONAUT` in `.github/workflows/release.yml`. + **macOS/Linux** ```bash docker build -t grails:testing -f etc/bin/Dockerfile . && docker run -it --rm -v $(pwd):/home/groovy/project -p 8080:8080 grails:testing bash @@ -494,6 +509,14 @@ To test reproducibility locally, running etc/bin/test-reproducible-builds.sh wil to build the three gradle projects Grails uses. The artifacts are then saved off, and built again. Finally, the hashes are generated to ensure the artifacts are the same. +Note that Grails 8 release artifacts come from TWO pinned JDKs: the primary one (used for everything except the +Grails-Micronaut island) and a secondary Liberica JDK 25 (used only for `grails-micronaut` and `grails-micronaut-bom`, +because Micronaut 5 platform GA targets JVM 25 bytecode). Both pins live in `.github/workflows/release.yml` +(`JAVA_VERSION` and `JAVA_VERSION_MICRONAUT` respectively) and are installed side-by-side in `etc/bin/Dockerfile`. The +verify scripts switch `JAVA_HOME` to `$JDK_25_HOME` for the island and back to the default for everything else. Anyone +bumping either pin must also bump the matching pin in the Dockerfile so the verification container can reproduce the +new artifacts. + Some common gotchas with Java build reproducibility problems: 1. Most tools support a `SOURCE_DATE_EPOCH` environment variable that can be set to a fixed time to ensure timestamps From 92f8cf0ad39020449bb8a15f6b733fcd63d4c72f Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 28 May 2026 17:38:09 -0400 Subject: [PATCH 10/13] fix(deps): Address PR #15677 review feedback - 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 --- .github/workflows/release.yml | 28 +++++++++++++++++++ etc/bin/test-reproducible-builds.sh | 8 +++--- etc/bin/verify-reproducible.sh | 8 +++--- .../docs/src/main/docs/guide/otherNotes.adoc | 6 ++++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a2bfc8682d..52c852bce1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -271,12 +271,40 @@ jobs: cat build/grails-core-checksums.txt > "$combined_file" cat grails-gradle/build/grails-gradle-checksums.txt >> "$combined_file" cat grails-forge/build/grails-forge-checksums.txt >> "$combined_file" + # The grails-core aggregation above ran with -PskipMicronautProjects (JDK 21), + # so it excludes the Micronaut "island" (grails-micronaut, grails-micronaut-bom). + # Those artifacts were built/signed on JDK 25 and their per-project checksums + # were generated by publishedChecksums (finalizer of publishToSonatype). Append + # them 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. Format matches gradle/publish-root-config.gradle: " ". + for checksum_dir in grails-micronaut/build/checksums grails-bom/micronaut/build/checksums; do + [ -d "$checksum_dir" ] || continue + for checksum_file in "$checksum_dir"/*.sha512; do + [ -e "$checksum_file" ] || continue + jar_name="$(basename "$checksum_file" .sha512)" + checksum="$(awk '{print $1; exit}' "$checksum_file")" + echo "$jar_name $checksum" >> "$combined_file" + done + done - name: "🩹 Combine published artifacts" run: | combined_file="build/PUBLISHED_ARTIFACTS.txt" cat build/grails-core-artifacts.txt > "$combined_file" cat grails-gradle/build/grails-gradle-artifacts.txt >> "$combined_file" cat grails-forge/build/grails-forge-artifacts.txt >> "$combined_file" + # Append the Micronaut island artifacts (see "Combine checksums" above for why + # they are not in build/grails-core-artifacts.txt). The per-project lists were + # written by savePublishedArtifacts (finalizer of publishedChecksums) on JDK 25. + # Format matches gradle/publish-root-config.gradle: " ". + for artifacts_dir in grails-micronaut/build/artifacts grails-bom/micronaut/build/artifacts; do + [ -d "$artifacts_dir" ] || continue + for artifact_file in "$artifacts_dir"/*.txt; do + [ -e "$artifact_file" ] || continue + artifact_name="$(basename "$artifact_file" .txt)" + echo "$artifact_name $(cat "$artifact_file")" >> "$combined_file" + done + done - name: "📅 Generate build date file" run: echo "$SOURCE_DATE_EPOCH" >> build/BUILD_DATE.txt - name: "📤 Upload build date, checksums and published artifact files" diff --git a/etc/bin/test-reproducible-builds.sh b/etc/bin/test-reproducible-builds.sh index 99b1126425a..3f4d053568b 100755 --- a/etc/bin/test-reproducible-builds.sh +++ b/etc/bin/test-reproducible-builds.sh @@ -39,18 +39,18 @@ build_all() { # JDK 21 (default) pass across the three composites, Micronaut island skipped. killall -e java || true cd grails-gradle - ./gradlew build --rerun-tasks -PskipTests --no-build-cache + ./gradlew build --rerun-tasks -PskipTests --no-build-cache --no-daemon cd .. - ./gradlew build --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects + ./gradlew build --rerun-tasks -PskipTests --no-build-cache --no-daemon -PskipMicronautProjects cd grails-forge - ./gradlew build --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects + ./gradlew build --rerun-tasks -PskipTests --no-build-cache --no-daemon -PskipMicronautProjects cd .. # JDK 25 pass: the Grails-Micronaut island only. killall -e java || true JAVA_HOME="${JDK_25_HOME}" PATH="${JDK_25_HOME}/bin:${PATH}" \ ./gradlew :grails-micronaut:build :grails-micronaut-bom:build \ - --rerun-tasks -PskipTests --no-build-cache + --rerun-tasks -PskipTests --no-build-cache --no-daemon killall -e java || true } diff --git a/etc/bin/verify-reproducible.sh b/etc/bin/verify-reproducible.sh index 0e9028f596d..e35263ff254 100755 --- a/etc/bin/verify-reproducible.sh +++ b/etc/bin/verify-reproducible.sh @@ -63,11 +63,11 @@ killall -e java || true # (Micronaut island skipped), grails-forge composite (transitively pulls in # the root build via includeBuild('..'), island skipped there too). cd grails-gradle -./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache --no-daemon cd .. -./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache --no-daemon -PskipMicronautProjects cd grails-forge -./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache -PskipMicronautProjects +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache --no-daemon -PskipMicronautProjects cd .. # JDK 25 pass: the Grails-Micronaut "island" only (grails-micronaut, @@ -86,7 +86,7 @@ killall -e java || true echo "Switching to JDK 25 at ${JDK_25_HOME} for the Micronaut island..." JAVA_HOME="${JDK_25_HOME}" PATH="${JDK_25_HOME}/bin:${PATH}" \ ./gradlew :grails-micronaut:publishToMavenLocal :grails-micronaut-bom:publishToMavenLocal \ - --rerun-tasks -PskipTests --no-build-cache + --rerun-tasks -PskipTests --no-build-cache --no-daemon killall -e java || true echo "Generating Checksums for Built Jars" "${SCRIPT_DIR}/generate-build-artifact-hashes.groovy" "${DOWNLOAD_LOCATION}/grails" > "${DOWNLOAD_LOCATION}/grails/etc/bin/results/second.txt" diff --git a/grails-data-graphql/docs/src/main/docs/guide/otherNotes.adoc b/grails-data-graphql/docs/src/main/docs/guide/otherNotes.adoc index 5a8bae2f988..a4fcd3cb388 100644 --- a/grails-data-graphql/docs/src/main/docs/guide/otherNotes.adoc +++ b/grails-data-graphql/docs/src/main/docs/guide/otherNotes.adoc @@ -134,3 +134,9 @@ And here is the expected response: ---- include::{sourcedir}/examples/grails-docs-app/src/integration-test/groovy/demo/AuthorIntegrationSpec.groovy[tags=createResponse] ---- + +=== Testing and JSON Serialization + +The `GraphQLSpec` testing trait talks to a running application over HTTP and serializes request bodies and parses responses using Groovy's `groovy.json.JsonOutput` and `groovy.json.JsonSlurper`, not the JSON object mapper your application is configured with at runtime. + +WARNING: The object mapper configured in your application determines how GraphQL responses are actually rendered in production. A customized object mapper (for example, with non-default date formats, property naming strategies, or custom serializers) can produce output that differs from the plain JSON parsed by `GraphQLSpec`. Assertions made through the trait may therefore not match the exact payload a client using your application's object mapper would receive. When these serialization details matter, assert against the raw response body and verify your object mapper configuration with a dedicated test. From cb750435a99a514991727976e1b5f26947f0ebef Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 28 May 2026 18:45:35 -0400 Subject: [PATCH 11/13] test(forge): Cover the grails-micronaut JDK 25 guard 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 --- .../micronaut/GrailsMicronautSpec.groovy | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/micronaut/GrailsMicronautSpec.groovy diff --git a/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/micronaut/GrailsMicronautSpec.groovy b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/micronaut/GrailsMicronautSpec.groovy new file mode 100644 index 00000000000..b58bf28318b --- /dev/null +++ b/grails-forge/grails-forge-core/src/test/groovy/org/grails/forge/feature/micronaut/GrailsMicronautSpec.groovy @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.grails.forge.feature.micronaut + +import org.grails.forge.BeanContextSpec +import org.grails.forge.BuildBuilder +import org.grails.forge.options.JdkVersion + +class GrailsMicronautSpec extends BeanContextSpec { + + void "test grails-micronaut adds the dependency when JDK 25 is selected"() { + when: + final String template = new BuildBuilder(beanContext) + .features(["grails-micronaut"]) + .jdkVersion(JdkVersion.JDK_25) + .render() + + then: + template.contains('implementation "org.apache.grails:grails-micronaut') + } + + void "test grails-micronaut is rejected when the selected JDK is below 25"() { + when: + // micronaut-core's ScopedValues references java.lang.ScopedValue.CallableOp + // (JEP 506, finalized in JDK 25), so the feature must refuse older JDKs. + new BuildBuilder(beanContext) + .features(["grails-micronaut"]) + .jdkVersion(JdkVersion.JDK_21) + .render() + + then: + IllegalArgumentException e = thrown() + e.message == 'grails-micronaut requires JDK 25 or later (selected: JDK 21).' + } +} From 0b7ce1bd064e3ad2bfdc415ffae2f60f092225f7 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 28 May 2026 21:16:36 -0400 Subject: [PATCH 12/13] build(deps): Auto-toggle the Grails-Micronaut island by build JDK 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 --- .github/workflows/gradle.yml | 55 ++++++++++------------ .github/workflows/release-publish-docs.yml | 10 ++-- .github/workflows/release.yml | 42 ++++++++--------- settings.gradle | 32 ++++++++++--- 4 files changed, 75 insertions(+), 64 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index dfc83c1cb15..c8d1e64885f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -125,16 +125,17 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "🔨 Build project" - # Micronaut 5 platform GA targets JVM 25 bytecode, so on the Java 21 jobs we - # skip the Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, - # the micronaut-tied test-examples). Java 25 jobs build the full graph. + # The Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, the + # micronaut-tied test-examples) is auto-managed by settings.gradle based on the + # build JDK: it is pruned on a sub-25 JDK (Micronaut 5 GA targets JVM 25 bytecode) + # and included automatically on JDK 25+. So the Java 21 entries build everything + # except the island and the Java 25 entries build the full graph - no flag needed. run: > ./gradlew build :grails-shell-cli:installDist groovydoc --continue --stacktrace -PonlyCoreTests -PskipCodeStyle - ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} buildRerunTasks: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: 'Build Grails-Core Rerunning all Tasks' @@ -162,8 +163,8 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "🔨 Build project" - # This job only runs on Java 21; skip the Micronaut island because the - # Micronaut 5 platform GA targets JVM 25 bytecode (see comment on `build`). + # This job only runs on Java 21, where settings.gradle auto-prunes the Micronaut + # island (Micronaut 5 GA targets JVM 25 bytecode). See comment on `build`. run: > ./gradlew build :grails-shell-cli:installDist groovydoc --continue @@ -171,7 +172,6 @@ jobs: --stacktrace -PonlyCoreTests -PskipCodeStyle - -PskipMicronautProjects buildForge: name: "Build Grails Forge (Java ${{ matrix.java }}, indy=${{ matrix.indy }})" strategy: @@ -203,9 +203,9 @@ jobs: if: ${{ contains(github.event.head_commit.message, '[skip tests]') }} working-directory: 'grails-forge' # The Forge composite build includes the root grails-core via - # `includeBuild('..')` in grails-forge/settings.gradle, so on Java 21 - # we must propagate -PskipMicronautProjects to keep the included - # grails-core build from trying to compile the JVM-25 Micronaut island. + # `includeBuild('..')` in grails-forge/settings.gradle. On Java 21 that + # included build auto-prunes the Micronaut island (settings.gradle), so the + # Forge build never tries to compile the JVM-25 island; Java 25 includes it. run: > ./gradlew build --continue @@ -213,11 +213,10 @@ jobs: -PgrailsIndy=${{ matrix.indy }} -PskipCodeStyle -PskipTests - ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} - name: "🔨 Build project with tests" if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} working-directory: 'grails-forge' - # See comment above on '-PskipMicronautProjects'. + # See comment above on the auto-managed Micronaut island. run: > ./gradlew build --continue @@ -225,7 +224,6 @@ jobs: --stacktrace -PgrailsIndy=${{ matrix.indy }} -PskipCodeStyle - ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} - name: "✅ Verify combined CLI" run: | cd grails-forge @@ -271,8 +269,8 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "🏃 Run Functional Tests" - # Micronaut 5 platform GA targets JVM 25 bytecode, so skip the Micronaut - # island on Java 21 jobs (see comment on `build`). + # The Micronaut island is auto-pruned on a sub-25 JDK (settings.gradle), so the + # Java 21 entries skip it and the Java 25 entries include it (see comment on `build`). run: > ./gradlew bootJar check --continue @@ -284,7 +282,6 @@ jobs: -PskipCodeStyle -PskipHibernate5Tests -PskipMongodbTests - ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} mongodbFunctional: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: "Mongodb Functional Tests (Java ${{ matrix.java }}, MongoDB ${{ matrix.mongodb-version }}, indy=${{ matrix.indy }})" @@ -316,8 +313,8 @@ jobs: - name: "🏃 Run Functional Tests" env: GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - # Micronaut 5 platform GA targets JVM 25 bytecode, so skip the Micronaut - # island on Java 21 jobs (see comment on `build`). + # The Micronaut island is auto-pruned on a sub-25 JDK (settings.gradle), so the + # Java 21 entries skip it and the Java 25 entries include it (see comment on `build`). run: > ./gradlew bootJar check --continue @@ -327,7 +324,6 @@ jobs: -PonlyMongodbTests -PmongodbContainerVersion=${{ matrix.mongodb-version }} -PskipCodeStyle - ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} hibernate5Functional: if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} name: "Hibernate5 Functional Tests (Java ${{ matrix.java }}, indy=${{ matrix.indy }})" @@ -357,8 +353,8 @@ jobs: - name: "🏃 Run Functional Tests" env: GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - # Micronaut 5 platform GA targets JVM 25 bytecode, so skip the Micronaut - # island on Java 21 jobs (see comment on `build`). + # The Micronaut island is auto-pruned on a sub-25 JDK (settings.gradle), so the + # Java 21 entries skip it and the Java 25 entries include it (see comment on `build`). run: > ./gradlew bootJar check --continue @@ -367,7 +363,6 @@ jobs: -PgrailsIndy=${{ matrix.indy }} -PonlyHibernate5Tests -PskipCodeStyle - ${{ matrix.java == 21 && '-PskipMicronautProjects' || '' }} publishGradle: if: github.repository_owner == 'apache' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') needs: [ buildGradle ] @@ -439,10 +434,10 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@v1 - name: "📤 Publish Grails-Core Snapshot Artifacts" - # -PskipMicronautProjects keeps this Java 21 publish from trying to compile - # the Micronaut 5 / JVM 25 island. The Micronaut artifacts (grails-micronaut, - # grails-micronaut-bom) are published by the parallel publishMicronaut job - # below, which runs on JDK 25. + # This Java 21 publish excludes the Micronaut 5 / JVM 25 island automatically - + # settings.gradle prunes it on a sub-25 JDK. The island artifacts (grails-micronaut, + # grails-micronaut-bom) are published by the parallel publishMicronaut job below, + # which runs on JDK 25. uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 env: GRAILS_PUBLISH_RELEASE: 'false' @@ -453,7 +448,7 @@ jobs: timeout_seconds: 1200 # normal range 14min if build is not cached (no tests running) max_attempts: 3 # Attempts to address: Could not write to resource 'https://repository.apache.org/content/repositories/snapshots/...' Read timed out retry_wait_seconds: 180 - command: ./gradlew publish aggregateChecksums aggregatePublishedArtifacts --no-build-cache --rerun-tasks -PskipMicronautProjects + command: ./gradlew publish aggregateChecksums aggregatePublishedArtifacts --no-build-cache --rerun-tasks - name: "📤 Upload grails-core checksums" uses: actions/upload-artifact@v7.0.1 with: @@ -479,9 +474,9 @@ jobs: publishMicronaut: # Micronaut 5.0.0 publishes JARs targeting JVM 25 bytecode, so the Micronaut # "island" (grails-micronaut, grails-micronaut-bom) must publish from a JDK 25 - # runner. The sibling `publish` job runs on JDK 21 with - # -PskipMicronautProjects to publish everything else; this job publishes the - # two Micronaut artifacts. The two test-example projects in the island are + # runner. The sibling `publish` job runs on JDK 21, where settings.gradle + # auto-prunes the island, so it publishes everything else; this job publishes + # the two Micronaut artifacts. The two test-example projects in the island are # not published. needs: [ publishGradle, build, functional, hibernate5Functional, mongodbFunctional ] if: >- diff --git a/.github/workflows/release-publish-docs.yml b/.github/workflows/release-publish-docs.yml index 47111fedb96..d71a75c8c95 100644 --- a/.github/workflows/release-publish-docs.yml +++ b/.github/workflows/release-publish-docs.yml @@ -67,11 +67,11 @@ jobs: with: develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: "📖 Generate Documentation" - # The Grails-Micronaut "island" requires JDK 25 (Micronaut 5 bytecode); - # this docs build runs on the JDK 21 reproducibility pin, so prune the - # island from the project graph to avoid Gradle resolving its JVM 25 - # variants. The docs themselves have no code dependency on Micronaut. - run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects + # The Grails-Micronaut "island" requires JDK 25 (Micronaut 5 bytecode); this docs + # build runs on the JDK 21 reproducibility pin, where settings.gradle auto-prunes + # the island from the project graph so Gradle does not resolve its JVM 25 variants. + # The docs themselves have no code dependency on Micronaut. + run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} - name: "🚀 Publish to GitHub Pages" uses: apache/grails-github-actions/deploy-github-pages@asf env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52c852bce1e..914c5604244 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,19 +93,18 @@ jobs: - name: "🔍 Validate dependency versions" run: ./gradlew validateDependencyVersions - name: "🧩 Run grails-core assemble" - # Pre-publish smoke check on JDK 21. The Micronaut island is built and - # smoke-checked implicitly by the JDK 25 publishToSonatype step below; - # no separate JDK-25 assemble step is needed here. - run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects + # Pre-publish smoke check on JDK 21, where settings.gradle auto-prunes the + # Micronaut island. The island is built and smoke-checked by the JDK 25 + # publishToSonatype step below; no separate JDK-25 assemble step is needed here. + run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} - name: "🧩 Run grails-forge assemble" working-directory: grails-forge - run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects + run: ./gradlew assemble -PgithubBranch=${TARGET_BRANCH} - name: "📦 Generate grails-core docs (to assert that is works, before proceeding)" - # grails-doc has no code dependency on the Micronaut island, but the - # configuration phase still evaluates all sibling projects in the - # multi-project build. -PskipMicronautProjects prunes the island from - # the project graph so Gradle does not try to resolve its JDK 25 deps. - run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects + # grails-doc has no code dependency on the Micronaut island, and on this JDK 21 + # runner settings.gradle auto-prunes the island from the project graph so Gradle + # does not try to resolve its JDK 25 deps during configuration. + run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} - name: "🔏 Sign grails-wrapper ZIP" run: > gpg @@ -181,8 +180,8 @@ jobs: aggregateChecksums aggregatePublishedArtifacts - name: "📤 Publish Grails Core to Staging Repository" - # -PskipMicronautProjects excludes the Grails-Micronaut "island" from - # this JDK 21 publish. The two island artifacts (grails-micronaut, + # On this JDK 21 runner settings.gradle auto-prunes the Grails-Micronaut + # "island" from the build. The two island artifacts (grails-micronaut, # grails-micronaut-bom) are signed and staged by the # `Publish Grails-Micronaut` step below, which switches to JDK 25. env: @@ -199,11 +198,10 @@ jobs: publishToSonatype aggregateChecksums aggregatePublishedArtifacts - -PskipMicronautProjects - name: "📤 Publish Grails Forge to Staging Repository" - # The Forge composite build does includeBuild('..') so it pulls in the - # root grails-core build. Without -PskipMicronautProjects the Micronaut - # island would be evaluated on JDK 21 and fail to resolve. + # The Forge composite build does includeBuild('..') so it pulls in the root + # grails-core build. On this JDK 21 runner settings.gradle auto-prunes the + # Micronaut island from that included build so it is not evaluated/resolved. env: NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} @@ -219,7 +217,6 @@ jobs: publishToSonatype aggregateChecksums aggregatePublishedArtifacts - -PskipMicronautProjects - name: "☕️ Switch to JDK 25 for Micronaut publish" # Micronaut 5 platform GA targets JVM 25 bytecode, so the island # artifacts (grails-micronaut, grails-micronaut-bom) must be built and @@ -271,8 +268,8 @@ jobs: cat build/grails-core-checksums.txt > "$combined_file" cat grails-gradle/build/grails-gradle-checksums.txt >> "$combined_file" cat grails-forge/build/grails-forge-checksums.txt >> "$combined_file" - # The grails-core aggregation above ran with -PskipMicronautProjects (JDK 21), - # so it excludes the Micronaut "island" (grails-micronaut, grails-micronaut-bom). + # The grails-core aggregation above ran on JDK 21, where settings.gradle auto-prunes + # the Micronaut "island" (grails-micronaut, grails-micronaut-bom), so it is excluded. # Those artifacts were built/signed on JDK 25 and their per-project checksums # were generated by publishedChecksums (finalizer of publishToSonatype). Append # them here rather than re-running aggregateChecksums on JDK 25, which would @@ -688,7 +685,8 @@ jobs: with: develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: "📖 Generate Documentation" - run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} -PskipMicronautProjects + # Runs on the JDK 21 pin, where settings.gradle auto-prunes the Micronaut island. + run: ./gradlew grails-doc:build -PgithubBranch=${TARGET_BRANCH} - name: "🚀 Publish to GitHub Pages" uses: apache/grails-github-actions/deploy-github-pages@asf env: @@ -728,14 +726,14 @@ jobs: - name: "🚀 Grails SDK Minor Release" if: contains(env.VERSION, 'M') || contains(env.VERSION, 'RC') working-directory: grails-forge - run: ./gradlew sdkMinorRelease -PskipMicronautProjects + run: ./gradlew sdkMinorRelease env: GVM_SDKVENDOR_KEY: ${{ secrets.GVM_SDKVENDOR_KEY }} GVM_SDKVENDOR_TOKEN: ${{ secrets.GVM_SDKVENDOR_TOKEN }} - name: "🚀 Grails SDK Major Release" if: startsWith(env.VERSION, '7.') && !contains(env.VERSION, 'M') && !contains(env.VERSION, 'RC') working-directory: grails-forge - run: ./gradlew sdkMajorRelease -PskipMicronautProjects + run: ./gradlew sdkMajorRelease env: GVM_SDKVENDOR_KEY: ${{ secrets.GVM_SDKVENDOR_KEY }} GVM_SDKVENDOR_TOKEN: ${{ secrets.GVM_SDKVENDOR_TOKEN }} diff --git a/settings.gradle b/settings.gradle index 969f1c7f65c..46d7fbe5cc9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -73,19 +73,37 @@ buildCache { rootProject.name = 'grails.core.ROOT' -// Presence-based toggle (matches project convention: skipFunctionalTests, skipCodeStyle, etc.). -// When -PskipMicronautProjects is passed (regardless of value), the Grails-Micronaut "island" -// is excluded from the build graph entirely: +// Grails-Micronaut "island" toggle. +// +// The island is excluded from the build graph entirely when skipped: // * grails-micronaut (Grails plugin that re-exports the Micronaut platform) // * grails-micronaut-bom (overrides Groovy/Spock to Groovy 5 / Spock 2.4-groovy-5.0) -// * the five grails-test-examples that consume grails-micronaut-bom +// * the grails-test-examples that consume grails-micronaut-bom +// +// The island builds against the Micronaut 5 platform, whose GA artifacts target JVM 25 +// bytecode and declare org.gradle.jvm.version=25. A build JDK older than 25 cannot resolve +// or compile them, so by default the island is auto-excluded on a sub-25 JDK and auto-included +// on JDK 25+. This lets a plain `./gradlew build` work on the Grails 8 baseline (JDK 21) and +// transparently pick up the island when run on JDK 25+. +// +// Two presence-based overrides (matching project convention: skipFunctionalTests, skipCodeStyle): +// -PskipMicronautProjects force-exclude the island on ANY JDK. Used by +// groovy-joint-workflow.yml, where the island's Groovy 5 / +// Spock 2.4-groovy-5.0 pin clashes with the Groovy 4.x snapshot +// that build swaps in - a reason independent of the JDK (#15613). +// -PincludeMicronautProjects force-include the island on a sub-25 JDK. Escape hatch for +// making the :grails-micronaut tasks addressable on an older JDK +// (e.g. to inspect the project graph). Note the island still +// cannot compile the JVM-25 Micronaut platform on a sub-25 JDK. +// -PskipMicronautProjects wins if both are present. // -// Used by .github/workflows/groovy-joint-workflow.yml so the joint Groovy 4 snapshot -// build does not try to compile Spock specs against a Groovy-5-only Spock artifact. // Consumers that reference :grails-micronaut-bom (e.g. grails-doc:generateBomDocumentation) // must guard those references with findProject(':grails-micronaut-bom') != null. // See https://github.com/apache/grails-core/issues/15613. -def skipMicronautProjects = providers.gradleProperty('skipMicronautProjects').isPresent() +def explicitlySkipMicronaut = providers.gradleProperty('skipMicronautProjects').isPresent() +def explicitlyIncludeMicronaut = providers.gradleProperty('includeMicronautProjects').isPresent() +def buildJdkSupportsMicronaut = Runtime.version().feature() >= 25 +def skipMicronautProjects = explicitlySkipMicronaut || (!buildJdkSupportsMicronaut && !explicitlyIncludeMicronaut) include( 'grails-bootstrap', From 62075a2a08329cb79c9992d5f5265d47b44907c9 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 28 May 2026 21:18:41 -0400 Subject: [PATCH 13/13] docs(deps): Sync gradle.properties island comment with JDK auto-detection 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 --- gradle.properties | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2dc0a0232be..0703f659575 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,9 +54,10 @@ gradleCycloneDxPluginVersion=3.0.0 micronautPlatformVersion=5.0.0 -# Pass -PskipMicronautProjects (presence-based, like skipFunctionalTests / skipCodeStyle) -# to drop the Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, and -# the micronaut-tied test-examples) from the build graph. See settings.gradle for the +# The Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, and the +# micronaut-tied test-examples) is auto-included on a JDK 25+ build (Micronaut 5 targets +# JVM 25 bytecode) and auto-excluded on a sub-25 JDK. Override with the presence-based +# -PskipMicronautProjects / -PincludeMicronautProjects flags. See settings.gradle for the # gating logic and grails-core#15613 for the rationale. # Libraries only specific to test apps, these should not be exposed