-
-
Notifications
You must be signed in to change notification settings - Fork 973
Move legacy command compatibility tests into an end-to-end build #16059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| # 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. | ||
|
|
||
| # The end-to-end suite is its own Gradle build, so the core build never reaches it and | ||
| # `./gradlew build` at the root is unaffected. | ||
| # | ||
| # It resolves Grails from the artifacts the core build publishes rather than by project | ||
| # substitution - that is what makes the tests end-to-end. The repository is the same | ||
| # build/local-maven that grails-forge points its generated applications at, populated by | ||
| # publishAllPublicationsToTestCaseMavenRepoRepository, so the suite exercises real poms and | ||
| # module metadata including the CLI companion artifacts. | ||
| # | ||
| # It also needs two JDKs, which is the other reason it gets its own workflow: the Grails 7 | ||
| # fixture must be compiled on Java 17 (the minimum for a Grails 7 app, so the binary matches | ||
| # what a real Grails 7 plugin is built with), while the core build and the Grails 8 | ||
| # application consuming the fixture need 21. The Grails 8 side simply tracks the repository's | ||
| # root .sdkmanrc - it has to run on whatever the core build it consumes runs on - and only the | ||
| # fixture carries its own pin. The steps below read both out of those files rather than relying | ||
| # on Gradle toolchain auto-detection. | ||
| name: "End to End" | ||
| on: | ||
| push: | ||
| branches: | ||
| - '[0-9]+.[0-9]+.x' | ||
| pull_request: | ||
| workflow_dispatch: | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | ||
| jobs: | ||
| endToEnd: | ||
| name: "End to End Tests (end-to-end build only)" | ||
| if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} | ||
| runs-on: ubuntu-24.04 | ||
| steps: | ||
| - name: "📥 Checkout repository" | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - name: "☕️ Determine JDKs from .sdkmanrc" | ||
| # Read both pins out of the files that already declare them. Only the major is kept: | ||
| # `sdk env` gives a developer the exact Liberica patch, but the fixture and the suite are | ||
| # test builds outside the reproducible-build surface, so CI deliberately takes the | ||
| # runner's current release of each major rather than pinning patch versions here. | ||
| id: jdks | ||
| run: | | ||
| set -euo pipefail | ||
| fixture_java=$(grep -E '^java=' end-to-end/legacy-g7-command-plugin/.sdkmanrc | cut -d= -f2) | ||
| build_java=$(grep -E '^java=' .sdkmanrc | cut -d= -f2) | ||
| echo "fixture-java=${fixture_java%%.*}" >> "$GITHUB_OUTPUT" | ||
| echo "build-java=${build_java%%.*}" >> "$GITHUB_OUTPUT" | ||
| echo "Grails 7 fixture JDK: ${fixture_java}" | ||
| echo "end-to-end build JDK (from root .sdkmanrc): ${build_java}" | ||
| - name: "☕️ Setup JDKs" | ||
| # Both, in one step. The last version listed becomes the default JAVA_HOME (the Grails 8 | ||
| # side); the fixture step below switches to the matching JAVA_HOME_<major>_X64 for its | ||
| # single invocation. | ||
| uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 | ||
| with: | ||
| distribution: liberica | ||
| java-version: | | ||
| ${{ steps.jdks.outputs.fixture-java }} | ||
| ${{ steps.jdks.outputs.build-java }} | ||
| - name: "🗄️ Restore dependency jar cache" | ||
| uses: actions/cache@v4 | ||
| with: | ||
| # Cache only downloaded dependency jars and wrapper distributions, never Grails build outputs. | ||
| # Keyed by branch version so each release branch maintains its own warm cache. | ||
| path: | | ||
| ~/.gradle/caches/modules-2 | ||
| ~/.gradle/wrapper | ||
| key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }} | ||
| restore-keys: | | ||
| gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}- | ||
| - name: "🐘 Setup Gradle" | ||
| uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 | ||
| with: | ||
| cache-disabled: true # dependency jars are cached by the explicit branch-keyed step above | ||
| develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} | ||
| - name: "📦 Setup: publish grails-gradle to the local repository the tests resolve from" | ||
| # Both builds publish into the same build/local-maven, and both are needed: the Grails BOM | ||
| # constrains org.apache.grails.gradle artifacts. grails-forge depends on this same pair of | ||
| # publish tasks for the applications its tests generate. | ||
| working-directory: 'grails-gradle' | ||
| run: ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository -PskipTests --stacktrace | ||
| - name: "📦 Setup: publish Grails to the local repository the tests resolve from" | ||
| run: ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository -PskipTests --stacktrace | ||
| - name: "📦 Setup: build the precompiled Grails 7 / Groovy 4 fixture" | ||
| working-directory: 'end-to-end/legacy-g7-command-plugin' | ||
| env: | ||
| # Derived from the major the jdks step read out of the fixture's .sdkmanrc, so a fixture | ||
| # JDK bump changes this lookup with it instead of leaving a stale literal behind. | ||
| JAVA_HOME: ${{ env[format('JAVA_HOME_{0}_X64', steps.jdks.outputs.fixture-java)] }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [ -z "${JAVA_HOME:-}" ]; then | ||
| echo "setup-java did not provision JDK ${{ steps.jdks.outputs.fixture-java }}; refusing to build the fixture on the default JDK" >&2 | ||
| exit 1 | ||
| fi | ||
| ./gradlew jar --stacktrace | ||
| - name: "🔍 Verify the fixture really was built by the Grails 7 toolchain" | ||
| working-directory: 'end-to-end/legacy-g7-command-plugin' | ||
| # A fixture silently built by the wrong toolchain would still pass the suite while | ||
| # proving nothing, so fail loudly here instead. Grails-/Groovy-Compile-Version come from | ||
| # the resolved BOM and Build-Jdk-Spec from the JVM that ran Gradle (all stamped into the | ||
| # manifest at jar time); the class-file major version is the ground truth for what the | ||
| # bytecode actually targets, since the fixture deliberately sets no toolchain or release. | ||
| run: | | ||
| set -euo pipefail | ||
| fixture_java='${{ steps.jdks.outputs.fixture-java }}' | ||
| jar=$(ls build/libs/*.jar) | ||
| unzip -p "$jar" META-INF/MANIFEST.MF | tr -d '\r' > /tmp/fixture-manifest | ||
| cat /tmp/fixture-manifest | ||
| grep -q '^Grails-Compile-Version: 7\.' /tmp/fixture-manifest | ||
| grep -q '^Groovy-Compile-Version: 4\.' /tmp/fixture-manifest | ||
| grep -q "^Build-Jdk-Spec: ${fixture_java}\$" /tmp/fixture-manifest | ||
| expected_major=$((fixture_java + 44)) | ||
| actual_major=$(unzip -p "$jar" legacy/g7/commands/HelloG7PrecompiledCommand.class \ | ||
| | od -An -t u1 -j 6 -N 2 | awk 'NF { print $1 * 256 + $2; exit }') | ||
| echo "class-file major version: ${actual_major} (expected ${expected_major} for Java ${fixture_java})" | ||
| [ "$actual_major" -eq "$expected_major" ] | ||
| - name: "🔍 Setup TestLens" | ||
| uses: testlens-app/setup-testlens@d96a555133c275a00949d2cc77b70fe9a4242ebf # v1.9.2 | ||
| - name: "🧪 Run the end-to-end tests" | ||
| # Only the end-to-end build. grails-core's own unit and functional suites are the CI | ||
| # workflow's job; nothing here re-runs them. The publish steps above are setup, not tests. | ||
| working-directory: 'end-to-end' | ||
| run: ./gradlew check --continue --stacktrace | ||
|
Comment on lines
+134
to
+138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is a real regression from the move rather than a pre-existing gap: before this PR these projects were in that graph. Either apply the style/analysis convention plugins in |
||
| - name: "📤 Upload test reports" | ||
| if: failure() | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: end-to-end-test-reports | ||
| path: end-to-end/**/build/reports/tests/** | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| <!-- | ||
| SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| Licensed 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. | ||
| --> | ||
|
|
||
| # End-to-end tests | ||
|
|
||
| Tests that exercise Grails from the outside, where doing so needs something the core build | ||
| cannot provide — a different JDK, a different Grails major, or a real published artifact. | ||
|
|
||
| This is its own Gradle build, so the core build never reaches these projects and `./gradlew build` | ||
| at the repository root is unaffected by anything here. | ||
|
|
||
| It resolves Grails from the artifacts the core build **publishes**, not by project substitution. | ||
| That is what makes these tests end-to-end: they consume grails-core the way an application does, | ||
| through real poms and Gradle module metadata, including the CLI companion artifacts. The repository | ||
| is `<repository root>/build/local-maven` — the same one `grails-forge` points its generated | ||
| applications at via `GRAILS_REPO_URL`. | ||
|
|
||
| ## Projects | ||
|
|
||
| | Project | What it is | | ||
| |---|---| | ||
| | `legacy-g7-command-plugin` | A **standalone build**, not part of this one. Compiles against published Grails 7 / Groovy 4 to produce a genuine precompiled `grails.dev.commands.ApplicationCommand` binary. | | ||
| | `legacy-commands-plugin` | A Grails 8 plugin whose legacy commands are recompiled under Groovy 5. | | ||
| | `legacy-commands` | A Grails 8 application that consumes both and runs their commands through the registry. | | ||
|
|
||
| `legacy-g7-command-plugin` is deliberately excluded from `settings.gradle`. An included build would | ||
| substitute `org.apache.grails:grails-core` for this repository's Groovy 5 project, which is exactly | ||
| the substitution the fixture exists to avoid — it must be compiled by a real Grails 7 toolchain for | ||
| its trait-woven bytecode to prove anything. | ||
|
|
||
| ## JDKs | ||
|
|
||
| The Grails 7 half declares the JDK it needs in a `.sdkmanrc`, rather than a Gradle toolchain, so that | ||
| neither the core build nor a contributor's default environment inherits a second JDK requirement: | ||
|
|
||
| | Where | JDK | Why | | ||
| |---|---|---| | ||
| | `legacy-g7-command-plugin/.sdkmanrc` | 17, Gradle 8.14.5 | What Grails 7 pins, so the fixture is built the way a Grails 7 plugin actually was. `gradle-bootstrap` generates this wrapper from that file via its `legacyG7Wrapper` task, rather than copying the shared one. | | ||
| | the repository's root `.sdkmanrc` | 21 | The Grails 8 baseline. This build consumes artifacts from the core build, so it runs on whatever the core build runs on — it deliberately does not re-pin that. | | ||
|
|
||
| ## Running locally | ||
|
|
||
| Three steps, in order. Each fails with an actionable message if a prior one was skipped. | ||
|
|
||
| Publish Grails to the repository this build resolves from. Both builds publish into the same | ||
| directory and both are needed — the BOM constrains `org.apache.grails.gradle` artifacts too: | ||
|
|
||
| ```shell | ||
| (cd grails-gradle && ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository) | ||
| ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository | ||
| ``` | ||
|
|
||
| Build the Grails 7 fixture — it is consumed as a prebuilt jar: | ||
|
|
||
| ```shell | ||
| cd end-to-end/legacy-g7-command-plugin | ||
| sdk env | ||
| ./gradlew jar | ||
| ``` | ||
|
|
||
| Run the suite, on the root JDK: | ||
|
|
||
| ```shell | ||
| sdk env # from the repository root | ||
| cd end-to-end | ||
| ./gradlew check | ||
| ``` | ||
|
|
||
| Re-run the publish whenever you change something in the core build that these tests exercise; | ||
| nothing here can detect that for you, because the whole point is that the build boundary is real. | ||
|
|
||
| CI does the same three steps, reading both JDK majors out of the `.sdkmanrc` files. Only the major | ||
| is honoured there: `sdk env` gives a developer the exact Liberica patch, but these are test builds | ||
| outside the reproducible-build surface, so CI deliberately takes the runner's current release of | ||
| each major. See `.github/workflows/end-to-end.yml`. | ||
|
|
||
| ## Style and analysis checks | ||
|
|
||
| These projects are deliberately outside the violation gate — no `grails-code-style` or | ||
| `grails-code-analysis` plugin is applied here, matching every other test-example application in the | ||
| repository (none of the `grails-test-examples/*` projects apply them either, and the root | ||
| `aggregateViolations` reports only collect from projects that do). They are fixtures: the | ||
| application and plugin sources exist to exercise the command registry, and the Grails 7 fixture | ||
| must stay compilable by a real Grails 7 toolchain, which the current convention plugins do not | ||
| target. | ||
|
|
||
| ## Why not `includeBuild('..')` | ||
|
|
||
| Composite substitution would win over the local repository and put us back to resolving projects | ||
| instead of artifacts, which is the thing these tests exist not to do. It also cannot express the CLI | ||
| companions: `grails-core-cli` is a secondary capability of `:grails-core` rather than a project, so | ||
| substituting it hits a capability self-conflict. | ||
|
|
||
| Published metadata has that solved already — the companion is a first-class module with its own | ||
| publication, and `CliPublishingSupport` rewrites capability requests out of what gets published so | ||
| that external consumers resolve the plain coordinate. Resolving from the repository gets that for | ||
| free; `settings.gradle` uses `exclusiveContent` so every `org.apache.grails` artifact must come from | ||
| the local build and a remote snapshot cannot quietly satisfy the request instead. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| def localMavenRepo = rootProject.layout.projectDirectory.dir('../build/local-maven') | ||
|
|
||
| subprojects { | ||
| configurations.configureEach { | ||
| // Snapshots in the local repository are timestamped and rewritten on every core publish, | ||
| // so cached metadata would serve a stale one. | ||
| resolutionStrategy { | ||
| cacheChangingModulesFor(0, 'seconds') | ||
| cacheDynamicVersionsFor(0, 'seconds') | ||
| } | ||
| } | ||
|
|
||
| tasks.withType(AbstractCompile).configureEach { | ||
| doFirst { | ||
| if (!localMavenRepo.asFile.directory) { | ||
| throw new GradleException('Grails has not been published to the repository this ' + | ||
| "build resolves from (${localMavenRepo.asFile}). Run this first, from the " + | ||
| 'repository root: ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository') | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| projectVersion=8.0.0-SNAPSHOT | ||
|
|
||
| org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx5G | ||
| org.gradle.configuration-cache=false | ||
| org.gradle.caching=true | ||
| org.gradle.parallel=true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| distributionBase=GRADLE_USER_HOME | ||
| distributionPath=wrapper/dists | ||
| distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip | ||
| networkTimeout=10000 | ||
| retries=0 | ||
| retryBackOffMs=500 | ||
| validateDistributionUrl=true | ||
| zipStoreBase=GRADLE_USER_HOME | ||
| zipStorePath=wrapper/dists |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two problems that compound, and together they defeat the stated purpose of the verify step.
JAVA_HOME_17_X64hardcodes the very version the step above just derived. The17in that variable name is a literal. Iflegacy-g7-command-plugin/.sdkmanrcever moves off 17,steps.jdks.outputs.fixture-javafollows it andsetup-javaprovisions the new major - butenv.JAVA_HOME_17_X64is then unset,JAVA_HOMEresolves to empty, and the fixture builds on the default JDK (21) instead. Nothing fails. Deriving the variable name would fix it:And the verify step cannot catch that. Its comment says "a fixture silently built by the wrong toolchain would still pass the suite while proving nothing, so fail loudly here instead" - but it only greps
Grails-Compile-VersionandGroovy-Compile-Version, which come from the resolved BOM, not from the JDK. A fixture compiled on 21 against Grails 7.0.14 / Groovy 4.0.32 still prints7.and4.and sails through.That matters more than usual here because
legacy-g7-command-plugin/build.gradlesets norelease,sourceCompatibilityor toolchain (deliberately - that was the point of the move), so the class file version is determined entirely byJAVA_HOME. The check that would actually be loud is the bytecode major:Stamp the JDK into the manifest at jar time the same way Grails/Groovy are, and assert it here.