From f3c46e0e7d69fe9e8b1705cd009c810a583aefd4 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 15:15:15 -0300 Subject: [PATCH 1/5] 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 +- gradle/java.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 7e1f42c..1f8c571 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 939c187..fa747e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,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/gradle/java.gradle b/gradle/java.gradle index 1117484..2ed708a 100644 --- a/gradle/java.gradle +++ b/gradle/java.gradle @@ -1,6 +1,6 @@ java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 withJavadocJar() withSourcesJar() } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4f5eb9d..7e7d24f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ 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 networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 8cb408b112c98e0ebbfb68ce0dc984d67297f664 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 21:00:12 -0300 Subject: [PATCH 2/5] 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 d9024b4..907f3ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bcprovVersion = "1.85.2" bcpkixVersion = "1.85" expectit = "0.9.0" commonsIo = "2.22.0" -rundeckCore = "6.1.0-20260803" +rundeckCore = "7.0.0-SNAPSHOT" slf4j = "2.0.19" junit = "4.13.2" groovy = "4.0.33" From 256a4f9f565e65a0758f9541a28af536da1060a4 Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Tue, 15 Sep 2026 21:38:56 -0300 Subject: [PATCH 3/5] 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 1f8c571..ab92d5f 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 c84fe7f0b40fb3d6d64de280cf83f72d85cccfdb Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 16 Sep 2026 13:22:57 -0300 Subject: [PATCH 4/5] Mock classes with byte-buddy instead of cglib Spock could not mock com.dtolabs.rundeck.core.common.Framework, and said exactly why: java-proxy: Cannot mock classes. byte-buddy: The byte-buddy library is missing on the class path. cglib: Mocking with cglib is not supported on Java 21 or newer. mockito: The mockito-core library >= 4.11 is missing on the class path. Framework is a class, not an interface, so mocking it needs a maker that can subclass. cglib was that maker, added for Java 17; Spock refuses it from Java 21 onward, which left no usable maker at all on Java 25. byte-buddy is Spock's supported replacement. objenesis stays -- it is what instantiates the generated subclass without calling a constructor -- and the --add-opens flags stay for the same reason they were added, only the comment changed to name the right library. Co-Authored-By: Claude Opus 5 --- build.gradle | 2 +- gradle/libs.versions.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 7e251c3..b38e056 100644 --- a/build.gradle +++ b/build.gradle @@ -118,7 +118,7 @@ jar { test { useJUnitPlatform() - // Java 17+ module access for cglib/Spock mocking + // Spock's byte-buddy mock maker needs these opens to mock classes jvmArgs = [ '--add-opens=java.base/java.lang=ALL-UNNAMED', '--add-opens=java.base/java.util=ALL-UNNAMED', diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 907f3ec..b45cb46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ slf4j = "2.0.19" junit = "4.13.2" groovy = "4.0.33" spock = "2.4-groovy-4.0" -cglib = "3.3.0" +byteBuddy = "1.18.14" objenesis = "3.6" axionRelease = "1.21.3" # Security overrides for transitive dependencies @@ -33,13 +33,13 @@ slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } junit = { group = "junit", name = "junit", version.ref = "junit" } groovyAll = { group = "org.apache.groovy", name = "groovy-all", version.ref = "groovy" } spockCore = { group = "org.spockframework", name = "spock-core", version.ref = "spock" } -cglibNodep = { group = "cglib", name = "cglib-nodep", version.ref = "cglib" } +byteBuddy = { group = "net.bytebuddy", name = "byte-buddy", version.ref = "byteBuddy" } objenesis = { group = "org.objenesis", name = "objenesis", version.ref = "objenesis" } commonsLang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" } [bundles] bouncycastle = ["bcpkix", "bcprov"] -testLibs = ["junit", "groovyAll", "spockCore", "cglibNodep", "objenesis"] +testLibs = ["junit", "groovyAll", "spockCore", "byteBuddy", "objenesis"] [plugins] axionRelease = { id = "pl.allegro.tech.build.axion-release", version.ref = "axionRelease" } From 5ad6716df2c00d09c319cc6f2488708552e4c70f Mon Sep 17 00:00:00 2001 From: Luis Toledo Date: Wed, 16 Sep 2026 13:48:51 -0300 Subject: [PATCH 5/5] 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 | 150 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 1 + 2 files changed, 151 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..469a158 --- /dev/null +++ b/.github/workflows/alpha-release.yml @@ -0,0 +1,150 @@ +on: + push: + # Sequence of patterns matched against refs/tags + 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 + id: create_release + run: | + gh release create \ + --generate-notes \ + --prerelease \ + --title 'Alpha ${{ steps.get_version.outputs.VERSION }}' \ + ${{ github.ref_name }} \ + build/libs/sshj-plugin-${{ 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/sshj-plugin/${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/sshj-plugin-${VERSION}.jar" "sshj-plugin-${VERSION}.jar" + sign_and_upload "build/libs/sshj-plugin-${VERSION}-sources.jar" "sshj-plugin-${VERSION}-sources.jar" + sign_and_upload "build/libs/sshj-plugin-${VERSION}-javadoc.jar" "sshj-plugin-${VERSION}-javadoc.jar" + sign_and_upload "build/publications/sshj-plugin/pom-default.xml" "sshj-plugin-${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 fa747e4..834ad58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ on: # Sequence of patterns matched against refs/tags tags: - '*.*.*' # Push events to matching semver tags (no v prefix) + - '!*-alpha*' name: Publish Release