diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b5e85454e5b..953da53216e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -128,6 +128,11 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2 - name: "🔨 Build project" + # 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 @@ -162,6 +167,8 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2 - name: "🔨 Build project" + # 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 @@ -200,6 +207,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. 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 @@ -210,6 +221,7 @@ jobs: - name: "🔨 Build project with tests" if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} working-directory: 'grails-forge' + # See comment above on the auto-managed Micronaut island. run: > ./gradlew build --continue @@ -263,6 +275,8 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2 - name: "🏃 Run Functional Tests" + # 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 @@ -306,6 +320,8 @@ jobs: - name: "🏃 Run Functional Tests" env: GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + # 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 @@ -345,6 +361,8 @@ jobs: - name: "🏃 Run Functional Tests" env: GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + # 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 @@ -426,6 +444,10 @@ jobs: - name: "🔍 Setup TestLens" uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2 - name: "📤 Publish Grails-Core Snapshot Artifacts" + # 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@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 env: GRAILS_PUBLISH_RELEASE: 'false' @@ -459,6 +481,54 @@ 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, 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: >- + ${{ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: "☕️ Setup JDK" + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: liberica + java-version: 25 + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + - name: "🔍 Setup TestLens" + uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2 + - name: "📤 Publish Grails-Micronaut Snapshot Artifacts" + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 + 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 ] diff --git a/.github/workflows/release-publish-docs.yml b/.github/workflows/release-publish-docs.yml index ce4e6112ecd..b4820647804 100644 --- a/.github/workflows/release-publish-docs.yml +++ b/.github/workflows/release-publish-docs.yml @@ -68,6 +68,10 @@ jobs: cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) 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, 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c210d48136..728e5acd1ba 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 @@ -93,11 +94,17 @@ jobs: - name: "🔍 Validate dependency versions" run: ./gradlew validateDependencyVersions - name: "🧩 Run grails-core assemble" + # 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} - name: "📦 Generate grails-core docs (to assert that is works, before proceeding)" + # 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: > @@ -174,6 +181,10 @@ jobs: aggregateChecksums aggregatePublishedArtifacts - name: "📤 Publish Grails Core to Staging Repository" + # 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: NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} @@ -189,6 +200,9 @@ jobs: aggregateChecksums aggregatePublishedArtifacts - name: "📤 Publish Grails Forge to Staging Repository" + # 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 }} @@ -204,6 +218,39 @@ jobs: publishToSonatype aggregateChecksums aggregatePublishedArtifacts + - 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 }} @@ -222,12 +269,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 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 + # 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" @@ -612,6 +687,7 @@ jobs: cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: "📖 Generate Documentation" + # 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 diff --git a/.sdkmanrc b/.sdkmanrc index 13522273716..1c13da4379b 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. 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 diff --git a/dependencies.gradle b/dependencies.gradle index a3fdc6dd62b..96cb2faab53 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -222,7 +222,7 @@ ext { 'liquibase-hibernate.version': '4.27.0', 'liquibase.version' : '4.27.0', 'hibernate.version' : '5.6.15.Final', - '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/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..3f4d053568b 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 --no-daemon + cd .. + ./gradlew build --rerun-tasks -PskipTests --no-build-cache --no-daemon -PskipMicronautProjects + cd grails-forge + ./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 --no-daemon + 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..e35263ff254 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 +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache --no-daemon cd .. -./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache --no-daemon -PskipMicronautProjects cd grails-forge -./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache +./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache --no-daemon -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 --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" if [ -e "${DOWNLOAD_LOCATION}/grails/etc/bin/results/second.txt" ] && [ ! -s "${DOWNLOAD_LOCATION}/grails/etc/bin/results/second.txt" ]; then diff --git a/gradle.properties b/gradle.properties index 6d15317671a..4db1119fb9a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -52,14 +52,12 @@ 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-M2 -micronautRxjava2Version=2.9.0 -micronautSerdeJacksonVersion=2.11.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 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' 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..beed6592eb6 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,11 @@ 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 using Spring's `org.springframework.web.client.RestClient`. JSON request bodies are serialized, and responses deserialized, by the `RestClient` Jackson message converter - the same JSON mechanism Spring uses elsewhere - rather than by hand. + +NOTE: `RestClient` only registers a JSON message converter when a JSON library is on the classpath. An application that renders JSON solely through JSON views, without otherwise depending on Jackson, must add `tools.jackson.core:jackson-databind` to its `integrationTestRuntimeOnly` configuration for the trait to serialize requests and parse responses. + +NOTE: Your application renders GraphQL responses with its configured object mapper, so customizing that object mapper (for example, with non-default date formats, property naming strategies, or custom serializers) changes the payloads your tests observe through the trait. When these serialization details matter, assert against the specific fields you care about and verify your object mapper configuration with a dedicated test. diff --git a/grails-data-graphql/plugin/build.gradle b/grails-data-graphql/plugin/build.gradle index c32901e795b..e55d21cd375 100644 --- a/grails-data-graphql/plugin/build.gradle +++ b/grails-data-graphql/plugin/build.gradle @@ -72,20 +72,22 @@ 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' testImplementation 'org.spockframework:spock-core' + + testImplementation 'org.springframework:spring-web' + testImplementation "io.github.cjstehno.ersatz:ersatz:$ersatzVersion" + testImplementation "io.github.cjstehno.ersatz:ersatz-groovy:$ersatzVersion" + testRuntimeOnly 'tools.jackson.core:jackson-databind' } compileGsonViews { 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..c825f31c5b7 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,12 @@ package org.grails.gorm.graphql.plugin.testing -import groovy.json.StreamingJsonBuilder +import groovy.json.JsonOutput 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.web.client.RestClient trait GraphQLSpec { @@ -37,7 +36,9 @@ trait GraphQLSpec { GraphQLRequestHelper getGraphQL() { if (_graphql == null) { - _graphql = new GraphQLRequestHelper(rest: RxHttpClient.create(new URL(getServerUrl()))) + _graphql = new GraphQLRequestHelper(rest: RestClient.builder() + .baseUrl(getServerUrl()) + .build()) } _graphql } @@ -56,59 +57,81 @@ trait GraphQLSpec { @TupleConstructor static class GraphQLRequestHelper { - RxHttpClient rest + private static final MediaType APPLICATION_GRAPHQL = MediaType.parseMediaType('application/graphql') - HttpResponse graphql(String requestBody) { - rest.exchange(HttpRequest.POST('/graphql', requestBody).contentType('application/graphql'), Map) - .firstOrError().blockingGet() - } + RestClient rest - def HttpResponse graphql(String requestBody, Class bodyType) { - rest.exchange(HttpRequest.POST('/graphql', requestBody).contentType('application/graphql'), bodyType) - .firstOrError().blockingGet() + ResponseEntity graphql(String requestBody) { + graphql(requestBody, Map) } - private HttpResponse buildJsonRequest(Map data) { - rest.exchange(HttpRequest.POST('/graphql', data), Map).firstOrError().blockingGet() + def ResponseEntity graphql(String requestBody, Class bodyType) { + rest.post() + .uri('/graphql') + .contentType(APPLICATION_GRAPHQL) + .accept(MediaType.APPLICATION_JSON) + .body(requestBody) + .retrieve() + .toEntity(bodyType) } - private HttpResponse buildGetRequest(Map data) { - if (data.containsKey('variables')) { - StringWriter sw = new StringWriter() - new StreamingJsonBuilder(sw).call(data.variables) - data.put('variables', sw.toString()) - } - UriBuilder uriBuilder = UriBuilder.of('/') - data.forEach({ key, value -> - uriBuilder.queryParam(key, value) - }) + private ResponseEntity buildJsonRequest(Map data) { + rest.post() + .uri('/graphql') + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(data) + .retrieve() + .toEntity(Map) + } - rest.exchange(HttpRequest.GET(uriBuilder.build()), Map).firstOrError().blockingGet() + private ResponseEntity buildGetRequest(Map data) { + // The GraphQL-over-HTTP GET protocol requires `variables` to be a + // URL-encoded JSON string query parameter, not an HTTP body, so it is + // JSON-encoded here rather than negotiated by a message converter. + Map queryParams = new LinkedHashMap<>(data) + if (queryParams.containsKey('variables')) { + queryParams.put('variables', JsonOutput.toJson(queryParams.get('variables'))) + } + rest.get() + .uri('/graphql', { uriBuilder -> + // Bind values as URI variables so the GraphQL query braces are encoded, not parsed as URI templates. + Map uriVariables = [:] + queryParams.eachWithIndex { entry, index -> + String name = "value${index}" + uriBuilder.queryParam(entry.key, '{' + name + '}') + uriVariables[name] = entry.value + } + uriBuilder.build(uriVariables) + }) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(Map) } - 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-data-graphql/plugin/src/test/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpecSpec.groovy b/grails-data-graphql/plugin/src/test/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpecSpec.groovy new file mode 100644 index 00000000000..653ed3f9d32 --- /dev/null +++ b/grails-data-graphql/plugin/src/test/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpecSpec.groovy @@ -0,0 +1,162 @@ +/* + * 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.gorm.graphql.plugin.testing + +import io.github.cjstehno.ersatz.GroovyErsatzServer +import io.github.cjstehno.ersatz.encdec.Decoders +import io.github.cjstehno.ersatz.encdec.JsonDecoder +import org.springframework.http.ResponseEntity +import org.springframework.web.client.RestClient +import spock.lang.AutoCleanup +import spock.lang.Specification + +/** + * Mock-HTTP-server coverage for {@code GraphQLSpec.GraphQLRequestHelper}. + * + * The trait exchanges JSON with the running application using Spring's + * {@link RestClient}. These tests stand up a local HTTP server and assert that + * the request bodies are serialized, and the responses deserialized, by the + * RestClient Jackson message converter - not by hand - and that the correct + * content types are sent for both the raw {@code application/graphql} and the + * structured {@code application/json} flavours. + */ +class GraphQLSpecSpec extends Specification { + + @AutoCleanup + GroovyErsatzServer server = new GroovyErsatzServer({}) + + private static GraphQLSpec.GraphQLRequestHelper helperFor(String baseUrl) { + new GraphQLSpec.GraphQLRequestHelper(rest: RestClient.builder().baseUrl(baseUrl).build()) + } + + void 'graphql(String) posts an application/graphql body and parses the JSON response into a Map'() { + given: + server.expectations { + POST('/graphql') { + called(1) + decoder('application/graphql', Decoders.utf8String) + body('{ bookList { id } }', 'application/graphql') + responder { + code(200) + body('{"data":{"bookList":[{"id":"1"}]}}', 'application/json') + } + } + } + + and: + def helper = helperFor(server.httpUrl) + + when: + ResponseEntity response = helper.graphql('{ bookList { id } }') + + then: 'the response was deserialized by the Jackson message converter into a Map' + response.statusCode.value() == 200 + response.body instanceof Map + response.body.data.bookList[0].id == '1' + + and: + server.verify() + } + + void 'json(...) serializes the request body with the RestClient Jackson converter'() { + given: + server.expectations { + POST('/graphql') { + called(1) + decoder('application/json', new JsonDecoder()) + body([query: 'query Q { x }', operationName: 'Q', variables: [a: 'one', b: 'two']], 'application/json') + responder { + code(200) + body('{"data":{"x":true}}', 'application/json') + } + } + } + + and: + def helper = helperFor(server.httpUrl) + + when: 'a Map payload is handed to RestClient, which encodes it as JSON' + ResponseEntity response = helper.json('query Q { x }', [a: 'one', b: 'two'], 'Q') + + then: 'the server received the Jackson-encoded body and the parsed response comes back as a Map' + response.statusCode.value() == 200 + response.body.data.x == true + + and: + server.verify() + } + + void 'get(...) issues a GET to /graphql with the variables JSON-encoded as a query parameter'() { + given: + server.expectations { + GET('/graphql') { + called(1) + query('query', 'query Q { x }') + query('operationName', 'Q') + query('variables', '{"a":"one","b":"two"}') + responder { + code(200) + body('{"data":{"x":true}}', 'application/json') + } + } + } + + and: + def helper = helperFor(server.httpUrl) + + when: + ResponseEntity response = helper.get('query Q { x }', [a: 'one', b: 'two'], 'Q') + + then: 'the request hits the graphql endpoint and the response is parsed into a Map' + response.statusCode.value() == 200 + response.body.data.x == true + + and: + server.verify() + } + + void 'graphql(String, Class) returns the unparsed String body'() { + given: + server.expectations { + POST('/graphql') { + called(1) + decoder('application/graphql', Decoders.utf8String) + body('{ ping }', 'application/graphql') + responder { + code(200) + body('{"data":{"ping":"pong"}}', 'application/json') + } + } + } + + and: + def helper = helperFor(server.httpUrl) + + when: + ResponseEntity response = helper.graphql('{ ping }', String) + + then: + response.statusCode.value() == 200 + response.body == '{"data":{"ping":"pong"}}' + + and: + server.verify() + } +} 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).' + } +} diff --git a/grails-test-examples/graphql/grails-docs-app/build.gradle b/grails-test-examples/graphql/grails-docs-app/build.gradle index b9cfdb411ef..2663d5969ae 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' @@ -66,6 +63,8 @@ dependencies { testImplementation 'org.apache.grails:grails-testing-support-datamapping' testImplementation 'org.apache.grails:grails-testing-support-web' + + integrationTestRuntimeOnly 'tools.jackson.core:jackson-databind' } apply { 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..f0dc4e5addc 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' @@ -71,6 +68,8 @@ dependencies { testImplementation 'org.apache.grails:grails-testing-support-datamapping' testImplementation 'org.apache.grails:grails-testing-support-web' testImplementation 'org.apache.grails.testing:grails-testing-support-mongodb' + + integrationTestRuntimeOnly 'tools.jackson.core:jackson-databind' } apply { 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..2663d5969ae 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' @@ -66,6 +63,8 @@ dependencies { testImplementation 'org.apache.grails:grails-testing-support-datamapping' testImplementation 'org.apache.grails:grails-testing-support-web' + + integrationTestRuntimeOnly 'tools.jackson.core:jackson-databind' } apply { 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..f89a0c2405b 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' @@ -67,6 +64,8 @@ dependencies { testImplementation 'org.apache.grails:grails-testing-support-datamapping' testImplementation 'org.apache.grails:grails-testing-support-web' + + integrationTestRuntimeOnly 'tools.jackson.core:jackson-databind' } apply { 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 diff --git a/settings.gradle b/settings.gradle index 9414e2c1c43..8b5008cbeea 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',