From 3465c41ac4ac6e2a0a3a906826cfcdfe6f1fd63a Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 15:13:19 -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. Co-Authored-By: Claude Opus 5 --- .github/workflows/gradle.yml | 2 +- .github/workflows/release.yml | 2 +- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 7cb4df2..226380c 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 fbfb6e7..1bbaa2e 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 584115b..c7b31c1 100644 --- a/build.gradle +++ b/build.gradle @@ -4,8 +4,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 withSourcesJar() withJavadocJar() } 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 c5f8501856afa310ca0e9857f1f2171273c7a8ea Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 20:59:31 -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 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c7b31c1..55744e3 100644 --- a/build.gradle +++ b/build.gradle @@ -63,8 +63,8 @@ configurations{ } dependencies { - compileOnly 'org.rundeck:rundeck-core:6.2.0-20260908' - testImplementation 'org.rundeck:rundeck-core:6.2.0-20260908' + compileOnly 'org.rundeck:rundeck-core:7.0.0-SNAPSHOT' + testImplementation 'org.rundeck:rundeck-core:7.0.0-SNAPSHOT' // Add secure commons-lang3 to provide alternative to vulnerable commons-lang 2.6 // Version 3.20.0 fixes CVE-2025-48924 (StackOverflowError in ClassUtils) From ad3677d77d347b43cecddc38708c4f39aa9b5d67 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 21:31:35 -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. Co-Authored-By: Claude Opus 5 --- .github/print-core-info.init.gradle | 22 +++++++++ .github/workflows/gradle.yml | 75 ++++++++++++++++++++++++++++- gradle.properties | 7 +++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 .github/print-core-info.init.gradle 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 226380c..404040e 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 index aaaa66c..4bea18d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,9 @@ httpclientVersion=4.5.14 commonsLoggingVersion=1.4.0 + +# 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 29d657e080e72e11b0bd4a85fa13e6b74b1a14ce Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 16 Sep 2026 13:48:12 -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..ec71e37 --- /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-step-${{ 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-step/${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-step-${VERSION}.jar" "http-step-${VERSION}.jar" + sign_and_upload "build/libs/http-step-${VERSION}-sources.jar" "http-step-${VERSION}-sources.jar" + sign_and_upload "build/libs/http-step-${VERSION}-javadoc.jar" "http-step-${VERSION}-javadoc.jar" + sign_and_upload "build/publications/http-step/pom-default.xml" "http-step-${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 1bbaa2e..eeba393 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,7 @@ on: push: tags: - '*.*.*' + - '!*-alpha*' name: Publish Release