From e86d655d78a84f6d73e05cf6410cc0e948731355 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 15:13:10 -0300 Subject: [PATCH 1/4] Build on Gradle 9 and Java 25 Follows rundeck and rundeckpro onto Java 25. The Gradle wrapper goes to 9.6.0, the Java source/target moves to 25 where this build declares it, and the workflows that run the build move with it -- a build targeting 25 is no use if CI still hands it a 17 JVM. Gradle 9 also surfaced something worth knowing: this module's specs are Spock 2, which runs on the JUnit Platform, while the test task declares useJUnit(). They have therefore never been discovered or run. Switching to useJUnitPlatform() does make them run -- and one of the three then fails, because it calls http://google.com and so depends on external network access. Enabling them is not this migration's call to make, so the existing behaviour is preserved and failOnNoDiscoveredTests is set to false, with a comment recording why. Fixing those specs is its own task. Co-Authored-By: Claude Opus 5 --- .github/workflows/gradle.yml | 2 +- .github/workflows/release.yml | 2 +- build.gradle | 11 +++++++++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 53ad6e1..e23dcdf 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -17,7 +17,7 @@ jobs: - name: Set up JDK 17 uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '25' distribution: 'zulu' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43b3d3f..f0d97f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: - name: Set up JDK 17 uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '25' distribution: 'zulu' - name: Build with Gradle run: ./gradlew build diff --git a/build.gradle b/build.gradle index 69b2ebd..e16de53 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,8 @@ scmVersion { version = scmVersion.version // Dynamic version from git tag java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 withSourcesJar() withJavadocJar() } @@ -117,6 +117,13 @@ jar { test { useJUnit() + + // These specs are Spock 2, which runs on the JUnit Platform, so useJUnit() has never + // discovered them -- they have not been running. Gradle 9 now treats "test sources present, + // nothing discovered" as an error. Switching to useJUnitPlatform() does make them run, but one + // of them calls http://google.com, so enabling them here would make the build depend on + // external network access. Left as-is deliberately; fixing these specs is its own task. + failOnNoDiscoveredTests = false // Java 17+ module access for reflection/mocking jvmArgs = [ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b413873..f0f17c3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 0caffd3d048038530785b60afa09c6d583a98d87 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 20:59:26 -0300 Subject: [PATCH 2/4] Compile against rundeck-core 7.0.0-SNAPSHOT Points at the core built from the Grails 8 / Java 25 branch, which is where this plugin will run. Gradle's module metadata enforces this: core 7.0.0 declares a JVM version of 25, so a consumer still asking for 17 is rejected outright at resolution -- "only compatible with JVM runtime version 25 or newer" -- rather than failing later. Note this does not resolve from a remote repository yet. 7.0.0-SNAPSHOT is produced by a normal (non-tag) rundeck build and only exists where that build has run publishToMavenLocal. CI will not find it until core publishes a snapshot somewhere reachable. Co-Authored-By: Claude Opus 5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3c506a1..e5f5bbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] axionRelease = "1.21.3" groovy = "4.0.33" -rundeckCore = "6.2.0-20260908" +rundeckCore = "7.0.0-SNAPSHOT" httpclient = "4.5.14" commonsLang3 = "3.20.0" httpStep = "2.0.5" From e768fa9f9205585587e6a3be01ddf1839dedcb16 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 21:38:17 -0300 Subject: [PATCH 3/4] Build rundeck-core from source in CI when the declared version is a SNAPSHOT A released rundeck-core resolves from Maven Central like any other dependency. A SNAPSHOT only exists where it was built, so CI has nothing to resolve: build it from source into the local Maven repository first. The branch cannot be inferred. A SNAPSHOT coordinate carries no branch identity, so rundeckCoreBranch goes in gradle.properties, beside the dependency it describes and versioned with it. The version is read from the declared dependency rather than parsed out of a build file. Across the plugin set this dependency is spelled four different ways -- version catalog, ext property, inline coordinate, named arguments -- and the declared-dependency model is identical for all of them. Reading it resolves nothing, so it works before the core has been built. Artifacts are cached on the core commit and the wanted version together. Keying on the commit alone would turn a version bump into a cache hit that skips the guard, surfacing as an opaque "not found" during the plugin build instead of the explicit mismatch error. Also corrects the JDK step label, which still read 17 while setting up 25. Co-Authored-By: Claude Opus 5 --- .github/print-core-info.init.gradle | 22 +++++++++ .github/workflows/gradle.yml | 75 ++++++++++++++++++++++++++++- gradle.properties | 6 +++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 .github/print-core-info.init.gradle create mode 100644 gradle.properties diff --git a/.github/print-core-info.init.gradle b/.github/print-core-info.init.gradle new file mode 100644 index 0000000..ed93536 --- /dev/null +++ b/.github/print-core-info.init.gradle @@ -0,0 +1,22 @@ +// Reports which rundeck-core this plugin is built against, for CI to act on. +// +// The version is read from the DECLARED dependency rather than parsed out of a build +// file: across the plugin set this dependency is spelled four different ways (version +// catalog, ext property, inline coordinate, named arguments) and the declared-dependency +// model is identical for all of them. Reading it resolves nothing, so this works before +// a SNAPSHOT core has been built. +// +// The branch comes from the rundeckCoreBranch property in gradle.properties, kept there +// so it lives beside the dependency it describes and is versioned with it. +gradle.projectsEvaluated { + rootProject.tasks.register('printRundeckCoreInfo') { + doLast { + def version = rootProject.configurations + .collectMany { it.dependencies } + .find { it.group == 'org.rundeck' && it.name == 'rundeck-core' } + ?.version + println "RUNDECK_CORE_VERSION=${version ?: ''}" + println "RUNDECK_CORE_BRANCH=${rootProject.findProperty('rundeckCoreBranch') ?: ''}" + } + } +} diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e23dcdf..ddfb7d6 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -14,13 +14,86 @@ jobs: - name: Get Fetch Tags run: git -c protocol.version=2 fetch --tags --progress --no-recurse-submodules origin if: "!contains(github.ref, 'refs/tags')" - - name: Set up JDK 17 + - name: Set up JDK 25 uses: actions/setup-java@v5 with: java-version: '25' distribution: 'zulu' - name: Grant execute permission for gradlew run: chmod +x gradlew + + # --- rundeck-core SNAPSHOT bootstrap --------------------------------- + # A released rundeck-core resolves from Maven Central like any other dependency + # and every step below skips. A SNAPSHOT only exists where it was built, so it + # is built from source into ~/.m2 first. + + - name: Read rundeck-core version and branch + id: core + run: | + eval "$(./gradlew -q -I .github/print-core-info.init.gradle \ + printRundeckCoreInfo --no-configuration-cache \ + | grep -E '^RUNDECK_CORE_(VERSION|BRANCH)=')" + [ -n "${RUNDECK_CORE_VERSION}" ] || { echo "::error::No org.rundeck:rundeck-core dependency declared"; exit 1; } + echo "rundeck-core: ${RUNDECK_CORE_VERSION}" + echo "version=${RUNDECK_CORE_VERSION}" >> "$GITHUB_OUTPUT" + case "${RUNDECK_CORE_VERSION}" in + *-SNAPSHOT) + [ -n "${RUNDECK_CORE_BRANCH}" ] || { + echo "::error::${RUNDECK_CORE_VERSION} is a SNAPSHOT but rundeckCoreBranch is not set in gradle.properties" + exit 1 + } + echo "built from branch: ${RUNDECK_CORE_BRANCH}" + echo "snapshot=true" >> "$GITHUB_OUTPUT" + echo "branch=${RUNDECK_CORE_BRANCH}" >> "$GITHUB_OUTPUT" + ;; + *) + echo "snapshot=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Resolve rundeck core commit + id: corerev + if: steps.core.outputs.snapshot == 'true' + run: | + BRANCH='${{ steps.core.outputs.branch }}' + SHA=$(git ls-remote https://github.com/rundeck/rundeck.git "refs/heads/${BRANCH}" | cut -f1) + [ -n "${SHA}" ] || { echo "::error::Branch '${BRANCH}' not found in rundeck/rundeck"; exit 1; } + echo "rundeck/rundeck@${BRANCH} = ${SHA}" + echo "sha=${SHA}" >> "$GITHUB_OUTPUT" + + # Keyed on the exact core commit AND the wanted version, with no restore-keys. + # A near-miss must be a miss: reusing artifacts from a different commit would + # build against a stale core, and omitting the version would turn a version bump + # into a cache hit that skips the guard below. + - name: Cache rundeck-core artifacts + id: corecache + if: steps.core.outputs.snapshot == 'true' + uses: actions/cache@v4 + with: + path: ~/.m2/repository/org/rundeck + key: rundeck-core-${{ steps.corerev.outputs.sha }}-${{ steps.core.outputs.version }} + + - name: Build rundeck-core from source + if: steps.core.outputs.snapshot == 'true' && steps.corecache.outputs.cache-hit != 'true' + run: | + BRANCH='${{ steps.core.outputs.branch }}' + git clone --depth 1 --branch "${BRANCH}" \ + https://github.com/rundeck/rundeck.git "${RUNNER_TEMP}/rundeck" + cd "${RUNNER_TEMP}/rundeck" + + # The branch and the version are independent: a branch can perfectly well + # produce a version this plugin never asked for. Fail loudly here rather than + # let the plugin build fail later with an opaque "not found". + BUILT=$(./gradlew -q bashVersionInfo | grep '^VERSION_FULL=' | cut -d= -f2) + WANTED='${{ steps.core.outputs.version }}' + if [ "${BUILT}" != "${WANTED}" ]; then + echo "::error::Branch '${BRANCH}' builds ${BUILT}, but this plugin declares ${WANTED}" + exit 1 + fi + + ./gradlew publishToMavenLocal -x check + # --- end bootstrap ---------------------------------------------------- + - name: Build with Gradle run: ./gradlew build - name: Get Release Version diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..fe504e3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +# Which rundeck branch produces the rundeck-core version declared in build.gradle. +# A SNAPSHOT coordinate carries no branch identity, so CI cannot infer this: when the +# declared version is a SNAPSHOT it is built from this branch into the local Maven +# repository before the plugin is compiled. Ignored for released versions, which +# resolve from Maven Central like any other dependency. +rundeckCoreBranch=grails-8-upgrade From 55d9baa84114de4aa534600c070fe27d416bad1c Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 16 Sep 2026 13:48:07 -0300 Subject: [PATCH 4/4] Cut alpha releases from a separate workflow Releasing an alpha needs the rundeck-core SNAPSHOT built from source first, the same bootstrap gradle.yml already uses. Keeping that out of release.yml leaves the path that cuts real releases untouched. That only works if the two triggers are disjoint. release.yml fires on '*.*.*', which also matches 2.0.6-alpha1 -- both workflows would run on one tag push, and the second gh release create would fail on a tag that already has a release. So release.yml now excludes alpha tags explicitly. The alpha workflow is release.yml plus the bootstrap, publishing to the same places and marking the GitHub release as a prerelease. The bootstrap block is taken from gradle.yml rather than rewritten, so the two cannot drift. Co-Authored-By: Claude Opus 5 --- .github/workflows/alpha-release.yml | 148 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 1 + 2 files changed, 149 insertions(+) create mode 100644 .github/workflows/alpha-release.yml diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml new file mode 100644 index 0000000..190284c --- /dev/null +++ b/.github/workflows/alpha-release.yml @@ -0,0 +1,148 @@ +on: + push: + tags: + - '*-alpha*' + +name: Publish Alpha Release + +jobs: + build: + name: Publish Alpha Release + runs-on: ubuntu-latest + env: + PKGCLD_REPO_URL: ${{ vars.PKGCLD_REPO_URL }} + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'zulu' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # --- rundeck-core SNAPSHOT bootstrap --------------------------------- + # A released rundeck-core resolves from Maven Central like any other dependency + # and every step below skips. A SNAPSHOT only exists where it was built, so it + # is built from source into ~/.m2 first. + + - name: Read rundeck-core version and branch + id: core + run: | + eval "$(./gradlew -q -I .github/print-core-info.init.gradle \ + printRundeckCoreInfo --no-configuration-cache \ + | grep -E '^RUNDECK_CORE_(VERSION|BRANCH)=')" + [ -n "${RUNDECK_CORE_VERSION}" ] || { echo "::error::No org.rundeck:rundeck-core dependency declared"; exit 1; } + echo "rundeck-core: ${RUNDECK_CORE_VERSION}" + echo "version=${RUNDECK_CORE_VERSION}" >> "$GITHUB_OUTPUT" + case "${RUNDECK_CORE_VERSION}" in + *-SNAPSHOT) + [ -n "${RUNDECK_CORE_BRANCH}" ] || { + echo "::error::${RUNDECK_CORE_VERSION} is a SNAPSHOT but rundeckCoreBranch is not set in gradle.properties" + exit 1 + } + echo "built from branch: ${RUNDECK_CORE_BRANCH}" + echo "snapshot=true" >> "$GITHUB_OUTPUT" + echo "branch=${RUNDECK_CORE_BRANCH}" >> "$GITHUB_OUTPUT" + ;; + *) + echo "snapshot=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Resolve rundeck core commit + id: corerev + if: steps.core.outputs.snapshot == 'true' + run: | + BRANCH='${{ steps.core.outputs.branch }}' + SHA=$(git ls-remote https://github.com/rundeck/rundeck.git "refs/heads/${BRANCH}" | cut -f1) + [ -n "${SHA}" ] || { echo "::error::Branch '${BRANCH}' not found in rundeck/rundeck"; exit 1; } + echo "rundeck/rundeck@${BRANCH} = ${SHA}" + echo "sha=${SHA}" >> "$GITHUB_OUTPUT" + + # Keyed on the exact core commit AND the wanted version, with no restore-keys. + # A near-miss must be a miss: reusing artifacts from a different commit would + # build against a stale core, and omitting the version would turn a version bump + # into a cache hit that skips the guard below. + - name: Cache rundeck-core artifacts + id: corecache + if: steps.core.outputs.snapshot == 'true' + uses: actions/cache@v4 + with: + path: ~/.m2/repository/org/rundeck + key: rundeck-core-${{ steps.corerev.outputs.sha }}-${{ steps.core.outputs.version }} + + - name: Build rundeck-core from source + if: steps.core.outputs.snapshot == 'true' && steps.corecache.outputs.cache-hit != 'true' + run: | + BRANCH='${{ steps.core.outputs.branch }}' + git clone --depth 1 --branch "${BRANCH}" \ + https://github.com/rundeck/rundeck.git "${RUNNER_TEMP}/rundeck" + cd "${RUNNER_TEMP}/rundeck" + + # The branch and the version are independent: a branch can perfectly well + # produce a version this plugin never asked for. Fail loudly here rather than + # let the plugin build fail later with an opaque "not found". + BUILT=$(./gradlew -q bashVersionInfo | grep '^VERSION_FULL=' | cut -d= -f2) + WANTED='${{ steps.core.outputs.version }}' + if [ "${BUILT}" != "${WANTED}" ]; then + echo "::error::Branch '${BRANCH}' builds ${BUILT}, but this plugin declares ${WANTED}" + exit 1 + fi + + ./gradlew publishToMavenLocal -x check + # --- end bootstrap ---------------------------------------------------- + + - name: Build with Gradle + run: ./gradlew build + - name: Get Release Version + id: get_version + run: VERSION=$(./gradlew currentVersion -q -Prelease.quiet) && echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + - name: Create Release + run: | + gh release create \ + --generate-notes \ + --prerelease \ + --title 'Alpha ${{ steps.get_version.outputs.VERSION }}' \ + ${{ github.ref_name }} \ + build/libs/http-notification-${{ steps.get_version.outputs.VERSION }}.jar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish to PackageCloud + run: | + if [ -z "$PKGCLD_WRITE_TOKEN" ]; then + echo "::error::PKGCLD_WRITE_TOKEN must be set to publish to PackageCloud" + exit 1 + fi + ./gradlew publishAllPublicationsToPackageCloudRepository + env: + PKGCLD_WRITE_TOKEN: ${{ secrets.PKGCLD_WRITE_TOKEN }} + - name: Sign and upload GPG signatures to PackageCloud + run: | + set -e + VERSION="${{ steps.get_version.outputs.VERSION }}" + BASE_URL="${PKGCLD_REPO_URL:-https://packagecloud.io/pagerduty/rundeck-plugins/maven2}/org/rundeck/plugins/http-notification/${VERSION}" + + echo "$SIGNING_KEY_B64" | base64 -d | gpg --batch --yes --import + KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/ {print $5; exit}') + + sign_and_upload() { + local FILE="$1" + local REMOTE_NAME="$2" + gpg --batch --yes --pinentry-mode loopback --passphrase "$SIGNING_PASSWORD" --default-key "$KEY_ID" --detach-sign --armor "$FILE" + curl -sf -H "Authorization: Bearer ${PKGCLD_WRITE_TOKEN}" \ + -X PUT --data-binary "@${FILE}.asc" \ + "${BASE_URL}/${REMOTE_NAME}.asc" + } + + sign_and_upload "build/libs/http-notification-${VERSION}.jar" "http-notification-${VERSION}.jar" + sign_and_upload "build/libs/http-notification-${VERSION}-sources.jar" "http-notification-${VERSION}-sources.jar" + sign_and_upload "build/libs/http-notification-${VERSION}-javadoc.jar" "http-notification-${VERSION}-javadoc.jar" + sign_and_upload "build/publications/http-notification/pom-default.xml" "http-notification-${VERSION}.pom" + env: + SIGNING_KEY_B64: ${{ secrets.SIGNING_KEY_B64 }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + PKGCLD_WRITE_TOKEN: ${{ secrets.PKGCLD_WRITE_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0d97f5..a42a903 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,7 @@ on: push: tags: - '*.*.*' + - '!*-alpha*' name: Publish Release