Fix the concurrency group, bump tooling, unpin CodeQL and gradle actions #251
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2014-2026 Bernard Ladenthin <bernard.ladenthin@gmail.com> | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: Publish | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| publish_to_central: | |
| description: "Deploy to Maven Central (snapshot if -SNAPSHOT, release if a vX.Y.Z tag)" | |
| type: boolean | |
| default: false | |
| # Supersede an in-flight run when a PR branch is pushed again. | |
| # | |
| # Without this every push starts a full parallel pipeline and the older ones keep | |
| # draining -- four were live at once during one session, which makes "what is CI | |
| # saying right now" genuinely ambiguous and wastes a lot of runner time on results | |
| # nobody will read. | |
| # | |
| # cancel-in-progress is deliberately scoped to pull_request ONLY. A push to main or | |
| # to a v* tag is a release path: cancelling one midway could leave a partially | |
| # published set of artifacts. | |
| # | |
| # cancel-in-progress: false is NOT sufficient on its own to protect a release run. | |
| # GitHub cancels a *pending* run whenever a newer run joins the same group behind an | |
| # in-progress one -- that rule is independent of cancel-in-progress. So with a plain | |
| # `workflow-ref` group, a queued `publish_to_central` dispatch on main could be | |
| # silently dropped by a later push to main, both sharing `Publish-refs/heads/main`. | |
| # Giving every non-PR run its own group (via the unique run_id) means such a run is | |
| # never queued behind a sibling and therefore can never be cancelled, while PR runs | |
| # still share a group per ref and supersede each other as intended. | |
| # | |
| # One-time effect when this expression changes: GitHub reads `concurrency` from the | |
| # workflow file at each run's own ref, so a run started before the change sits in the | |
| # old group and a run started after it sits in the new one. They are different groups, | |
| # so the new push does NOT supersede the in-flight old run -- exactly once, on the | |
| # commit that lands this. It self-heals from the next push on. Expect the same overlap | |
| # when porting this to a sibling repo; it is not a sign the expression is wrong. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'pull_request' && 'pr' || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Start gate — single cancellable abort window before the pipeline starts. | |
| # The wait duration lives in the `startgate` GitHub Environment (Settings → | |
| # Environments → startgate → Wait timer). | |
| # --------------------------------------------------------------------------- | |
| startgate: | |
| name: Start gate (abort window) | |
| runs-on: ubuntu-latest | |
| environment: startgate | |
| steps: | |
| - run: echo "Start gate elapsed — proceeding with pipeline." | |
| # --------------------------------------------------------------------------- | |
| # GPG signing-key preflight (standalone, no `needs:` — runs in parallel at the | |
| # very start on every trigger). Reproduces what maven-gpg-plugin does at deploy | |
| # time so a bad/expired key or wrong passphrase is caught in ~20s instead of | |
| # failing the publish stage. Declares `environment: maven-central` so it reads | |
| # the SAME GPG_PRIVATE_KEY / GPG_PASSPHRASE secret the publish jobs use. | |
| # | |
| # It is EXPECTED to go RED on refs where the secret is not delivered — fork PRs | |
| # and other contributors' branches (secrets are withheld there). That red is | |
| # the intended signal: "this ref cannot sign a release", not a regression. | |
| # | |
| # SECURITY: this job NEVER prints secret material. It imports the key into an | |
| # ephemeral keyring, prints only PUBLIC key metadata (key id, fingerprint, | |
| # owner UID, algorithm, created/expiry — all of which live on public | |
| # keyservers), and validates the passphrase by producing + verifying a | |
| # throwaway signature. The passphrase is passed on fd 3 (never argv, never a | |
| # log line), `set -x` is deliberately never enabled, and the passphrase is | |
| # additionally `::add-mask::`ed. | |
| # --------------------------------------------------------------------------- | |
| verify-signing-key: | |
| name: Verify GPG signing key (no secrets printed) | |
| runs-on: ubuntu-latest | |
| environment: maven-central | |
| steps: | |
| - name: Import key + run sign/verify self-test (prints only PUBLIC metadata) | |
| shell: bash | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| run: | | |
| set -euo pipefail # NOTE: deliberately NO `set -x` — it would echo the passphrase. | |
| if [ -z "${GPG_PRIVATE_KEY:-}" ]; then | |
| echo "::error::GPG_PRIVATE_KEY is empty for this run. Either the secret is not set, or it is scoped to a different environment/branch than 'maven-central' on this ref. Nothing to verify." | |
| exit 1 | |
| fi | |
| # Defensive: even though we never print it, register the passphrase as a | |
| # masked value so any accidental echo downstream is redacted. | |
| if [ -n "${GPG_PASSPHRASE:-}" ]; then echo "::add-mask::${GPG_PASSPHRASE}"; fi | |
| echo "gpg: $(gpg --version | head -n1)" | |
| # Ephemeral, private keyring; removed on exit. | |
| export GNUPGHOME="$(mktemp -d)" | |
| chmod 700 "$GNUPGHOME" | |
| cleanup() { gpgconf --kill gpg-agent >/dev/null 2>&1 || true; rm -rf "$GNUPGHOME"; } | |
| trap cleanup EXIT | |
| echo "== Import private key into an ephemeral keyring (key via stdin, never argv) ==" | |
| printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| COLONS="$(gpg --list-secret-keys --with-colons --fixed-list-mode)" | |
| SECCOUNT="$(printf '%s\n' "$COLONS" | awk -F: '$1=="sec"{n++} END{print n+0}')" | |
| echo "Secret keys imported: $SECCOUNT" | |
| if [ "$SECCOUNT" -lt 1 ]; then | |
| echo "::error::No secret key was imported — GPG_PRIVATE_KEY is not a valid armored secret key (check that the secret contains the full -----BEGIN PGP PRIVATE KEY BLOCK----- with intact newlines)." | |
| exit 1 | |
| fi | |
| KEYID="$(printf '%s\n' "$COLONS" | awk -F: '$1=="sec"{print $5; exit}')" | |
| ALGO="$(printf '%s\n' "$COLONS" | awk -F: '$1=="sec"{print $4; exit}')" | |
| CREATED="$(printf '%s\n' "$COLONS"| awk -F: '$1=="sec"{print $6; exit}')" | |
| EXPIRES="$(printf '%s\n' "$COLONS"| awk -F: '$1=="sec"{print $7; exit}')" | |
| FPR="$(printf '%s\n' "$COLONS" | awk -F: '$1=="fpr"{print $10; exit}')" | |
| echo "== PUBLIC key metadata ==" | |
| echo " Key ID (long): $KEYID" | |
| echo " Fingerprint: $FPR" | |
| echo " Pubkey algo id: $ALGO" | |
| echo " Created (UTC): $(date -u -d "@$CREATED" 2>/dev/null || echo "$CREATED")" | |
| echo " Owner UID(s):" | |
| printf '%s\n' "$COLONS" | awk -F: '$1=="uid"{print " - " $10}' | |
| # --- Expiration gate --- | |
| NOW="$(date -u +%s)" | |
| if [ -n "$EXPIRES" ]; then | |
| echo " Expires (UTC): $(date -u -d "@$EXPIRES" 2>/dev/null || echo "$EXPIRES")" | |
| if [ "$EXPIRES" -le "$NOW" ]; then | |
| echo "::error::Signing key is EXPIRED — Maven Central will reject its signatures. Extend the key's expiry and update the GPG_PRIVATE_KEY secret." | |
| exit 1 | |
| fi | |
| echo " Days to expiry: $(( (EXPIRES - NOW) / 86400 ))" | |
| if [ "$(( (EXPIRES - NOW) / 86400 ))" -lt 30 ]; then | |
| echo "::warning::Signing key expires in under 30 days — plan to rotate it." | |
| fi | |
| else | |
| echo " Expires (UTC): never" | |
| fi | |
| # --- Signing-capability gate --- | |
| if printf '%s\n' "$COLONS" | awk -F: '($1=="sec"||$1=="ssb"){print $12}' | grep -q 's'; then | |
| echo " Signing capability: present" | |
| else | |
| echo "::error::No signing-capable (sub)key found — this key cannot produce release signatures." | |
| exit 1 | |
| fi | |
| # --- Passphrase unlock + sign + verify roundtrip (the exact failure mode) --- | |
| # Passphrase on fd 3 only. Payload is a throwaway nonce; only the signature's | |
| # validity (exit codes) matters — no secret is ever emitted. | |
| echo "== Passphrase unlock + detached-sign + verify self-test ==" | |
| WORK="$(mktemp -d)" | |
| printf '%s' "ai-index signing-selftest" > "$WORK/payload.txt" | |
| gpg --batch --yes --pinentry-mode loopback --passphrase-fd 3 \ | |
| --local-user "$KEYID" \ | |
| --detach-sign --armor --output "$WORK/payload.txt.asc" "$WORK/payload.txt" \ | |
| 3<<<"${GPG_PASSPHRASE:-}" | |
| echo " Signature produced: $(wc -c < "$WORK/payload.txt.asc") armored bytes" | |
| gpg --batch --verify "$WORK/payload.txt.asc" "$WORK/payload.txt" | |
| rm -rf "$WORK" | |
| echo "RESULT: OK — key imports, is not expired, is signing-capable, and the passphrase successfully unlocked it to produce a VALID signature. maven-gpg-plugin will be able to sign with this key/passphrase." | |
| # --------------------------------------------------------------------------- | |
| # GPG signing-key preflight — GRADLE / BouncyCastle path. | |
| # Companion to the `verify-signing-key` (gpg) job above: that one mirrors | |
| # maven-gpg-plugin (how the Maven artifacts are signed); this one drives | |
| # Gradle's `signing` plugin + `useInMemoryPgpKeys` (BouncyCastle) — the path any | |
| # Gradle-based publish (e.g. an Android AAR) uses to sign. BouncyCastle is a | |
| # STRICTER parser of the armored key than gpg, so it catches key/format problems | |
| # gpg tolerates (e.g. the primary-vs-signing-subkey null-PGPPrivateKey issue). | |
| # It signs a throwaway project (.github/signing-selftest/) — no repo build is | |
| # involved — so this job is IDENTICAL across the sibling repos and validates the | |
| # release key via the Gradle path even in repos that do not publish via Gradle | |
| # yet ("prepared for Gradle"). Standalone (no `needs:`), parallel at pipeline | |
| # start, `environment: maven-central` so it reads the same secret the publish | |
| # uses. Red-by-design where the secret is not delivered (see the gpg job's note). | |
| # | |
| # SECURITY: prints no secret material. Key/passphrase reach Gradle only via env | |
| # (read by System.getenv at runtime); `set -x` is never enabled; the passphrase | |
| # is `::add-mask::`ed; Gradle runs with `--stacktrace` only. Only the produced | |
| # `.asc` (exit code) is asserted. Uses Gradle 9.6.1. | |
| # --------------------------------------------------------------------------- | |
| verify-signing-key-gradle: | |
| name: Verify GPG signing key — Gradle/BouncyCastle path (no secrets printed) | |
| runs-on: ubuntu-latest | |
| environment: maven-central | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: | |
| java-version: '21' | |
| distribution: temurin | |
| - uses: gradle/actions/setup-gradle@v6 | |
| with: | |
| gradle-version: "9.6.1" | |
| - name: Sign a throwaway artifact via useInMemoryPgpKeys (BouncyCastle) | |
| shell: bash | |
| env: | |
| MAVEN_GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| MAVEN_GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} | |
| run: | | |
| set -euo pipefail # NOTE: deliberately NO `set -x` — it would echo the passphrase. | |
| if [ -z "${MAVEN_GPG_PRIVATE_KEY:-}" ]; then | |
| echo "::error::MAVEN_GPG_PRIVATE_KEY is empty for this run. The maven-central environment did not deliver the secret to this ref (fork PR / other branch). Nothing to verify." | |
| exit 1 | |
| fi | |
| if [ -n "${MAVEN_GPG_PASSPHRASE:-}" ]; then echo "::add-mask::${MAVEN_GPG_PASSPHRASE}"; fi | |
| PROJ=".github/signing-selftest" | |
| echo "== Sign a throwaway artifact through Gradle's useInMemoryPgpKeys (BouncyCastle) ==" | |
| gradle --no-daemon -p "$PROJ" signMakeArtifact --stacktrace | |
| ASC="$PROJ/build/signing-selftest.zip.asc" | |
| if [ -f "$ASC" ]; then | |
| echo " Detached signature produced: $(wc -c < "$ASC") armored bytes" | |
| echo "RESULT: OK — Gradle's useInMemoryPgpKeys accepted the armored key + passphrase and produced a signature." | |
| else | |
| echo "::error::Gradle signing produced no .asc — useInMemoryPgpKeys could not build a usable signatory from MAVEN_GPG_PRIVATE_KEY / MAVEN_GPG_PASSPHRASE." | |
| exit 1 | |
| fi | |
| code-style: | |
| name: Code style (spotless) + package graph | |
| needs: startgate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: | |
| java-version: '21' | |
| distribution: temurin | |
| - name: Spotless check (fail fast on format violations) | |
| run: mvn -B --no-transfer-progress spotless:check | |
| - name: SpotBugs check (fail fast on static-analysis findings) | |
| run: mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true compile spotbugs:check | |
| - name: Print internal package dependency graph (jdeps, informational) | |
| continue-on-error: true | |
| run: | | |
| mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true compile | |
| echo "=== internal package dependency graph (jdeps, bytecode) ===" | |
| jdeps -verbose:package target/classes | grep 'net.ladenthin.streambuffer' || true | |
| build: | |
| name: Build | |
| needs: startgate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: | |
| java-version: '21' | |
| distribution: temurin | |
| cache: maven | |
| - name: Build | |
| run: mvn --batch-mode --no-transfer-progress -DskipTests package | |
| - uses: actions/upload-artifact@v7 | |
| with: { name: jars, path: target/*.jar } | |
| test: | |
| name: Test (JDK ${{ matrix.java-version }}) | |
| needs: [build] | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| java-version: ['21'] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: | |
| java-version: ${{ matrix.java-version }} | |
| distribution: temurin | |
| cache: maven | |
| - name: Memory before tests | |
| run: free -h | |
| - name: Test | |
| run: mvn -e --batch-mode --no-transfer-progress -P jcstress verify | |
| - uses: actions/upload-artifact@v7 | |
| if: matrix.java-version == '21' | |
| with: | |
| name: jacoco-report | |
| path: target/site/jacoco/jacoco.xml | |
| if-no-files-found: ignore | |
| - name: Memory after tests | |
| if: always() | |
| run: free -h | |
| # A forked test JVM that aborts leaves an hs_err_pid log and a surefire | |
| # dumpstream -- both otherwise ONLY inside the artifact uploaded below, | |
| # which is unreachable from anywhere that cannot fetch from Azure Blob | |
| # (a phone, a restricted network, an agent sandbox). Echo them here so the | |
| # aborting frame is readable from the run page itself. See | |
| # ../workspace/policies/ci-test-diagnostics.md section 3.1. | |
| - name: Print crash logs (on failure) | |
| if: failure() | |
| shell: bash | |
| run: | | |
| shopt -s nullglob | |
| found=0 | |
| for f in hs_err_pid*.log; do | |
| found=1 | |
| echo "===== $f (first 200 lines; full file in the uploaded artifact) =====" | |
| sed -n '1,200p' "$f" | |
| done | |
| for f in target/surefire-reports/*.dumpstream target/surefire-reports/*.dump; do | |
| found=1 | |
| echo "===== $f =====" | |
| cat "$f" | |
| done | |
| if [ "$found" = 0 ]; then | |
| echo "No hs_err_pid*.log and no surefire dump/dumpstream was written." | |
| echo | |
| echo "For an ordinary test failure that is EXPECTED, not a finding: this step runs on" | |
| echo "any job failure, and an assertion failure, a timeout or a compile error writes no" | |
| echo "crash log. Read the surefire output above for the real cause." | |
| echo | |
| echo "It points at a JVM-level abort only if the log ALSO shows a fork ending abnormally" | |
| echo "-- 'The forked VM terminated without properly saying goodbye', or an exit with no" | |
| echo "test results. In that case the abort bypassed the JVM error handler (a native" | |
| echo "exit()/terminate() rather than a raised signal), which is why no file was written." | |
| fi | |
| - name: Upload crash & surefire dumps | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: crash-dumps-jdk${{ matrix.java-version }} | |
| path: | | |
| ${{ github.workspace }}/hs_err_pid*.log | |
| ${{ github.workspace }}/*.hprof | |
| ${{ github.workspace }}/target/surefire-reports/*.dump | |
| ${{ github.workspace }}/target/surefire-reports/*.dumpstream | |
| ${{ github.workspace }}/target/surefire-reports/*.txt | |
| ${{ github.workspace }}/target/surefire-reports/TEST-*.xml | |
| if-no-files-found: ignore | |
| vmlens: | |
| name: Test (vmlens interleavings) | |
| needs: [build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: { java-version: '21', distribution: temurin, cache: maven } | |
| - name: Test under vmlens | |
| run: mvn --batch-mode --no-transfer-progress -Pvmlens test | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: vmlens-report | |
| path: target/vmlens-report/ | |
| if-no-files-found: ignore | |
| report: | |
| name: Report | |
| needs: [test] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: { java-version: '21', distribution: temurin, cache: maven } | |
| - uses: actions/download-artifact@v8 | |
| with: { name: jacoco-report, path: target/site/jacoco/ } | |
| continue-on-error: true | |
| - uses: advanced-security/maven-dependency-submission-action@v5 | |
| - name: Coveralls | |
| uses: coverallsapp/github-action@v2 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| file: target/site/jacoco/jacoco.xml | |
| format: jacoco | |
| continue-on-error: true | |
| - name: Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: target/site/jacoco/jacoco.xml | |
| continue-on-error: true | |
| - name: Run PIT mutation tests | |
| run: mvn --batch-mode --no-transfer-progress test-compile org.pitest:pitest-maven:mutationCoverage | |
| - name: Extract PIT survivors | |
| if: always() | |
| run: | | |
| echo "=== PIT Survived Mutations ===" | |
| for html_file in $(find target/pit-reports -name "*.html" -type f | sort); do | |
| if grep -q "SURVIVED" "$html_file"; then | |
| echo "Found survivors in $html_file:" | |
| grep -B 2 -A 3 "SURVIVED" "$html_file" | |
| echo "" | |
| fi | |
| done | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: { name: pit-reports, path: target/pit-reports/ } | |
| - name: Run JMH benchmarks | |
| run: > | |
| mvn --batch-mode --no-transfer-progress exec:java | |
| -Dexec.mainClass=org.openjdk.jmh.Main | |
| -Dexec.classpathScope=test | |
| -Dexec.args="StreamBufferThroughputBenchmark -wi 2 -i 3 -f 0 -rf json -rff target/jmh-results.json" | |
| continue-on-error: true | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: { name: jmh-results, path: target/jmh-results.json } | |
| check-snapshot: | |
| name: "Check: main branch / SNAPSHOT" | |
| needs: [report] | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main') || | |
| (github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/v')) | |
| steps: | |
| - name: Confirm snapshot ref | |
| run: echo "Confirmed on snapshot ref ${{ github.ref }}" | |
| check-tag: | |
| name: "Check: v* tag" | |
| needs: [report] | |
| runs-on: ubuntu-latest | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| steps: | |
| - name: Confirm tag ref | |
| run: echo "Confirmed on tag ${{ github.ref }}" | |
| publish-snapshot: | |
| name: Publish Snapshot to Central | |
| needs: [check-snapshot, code-style] | |
| if: needs.check-snapshot.result == 'success' && inputs.publish_to_central | |
| runs-on: ubuntu-latest | |
| environment: maven-central | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: | |
| java-version: '21' | |
| distribution: temurin | |
| cache: maven | |
| server-id: central | |
| server-username: MAVEN_USERNAME | |
| server-password: MAVEN_PASSWORD | |
| gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} | |
| gpg-passphrase: MAVEN_GPG_PASSPHRASE | |
| - name: Guard - require a -SNAPSHOT version | |
| shell: bash | |
| run: | | |
| VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version | tail -n1) | |
| echo "Resolved project version: $VERSION" | |
| case "$VERSION" in | |
| *-SNAPSHOT) echo "OK: -SNAPSHOT version, continuing snapshot deploy." ;; | |
| *) echo "::error::Refusing to publish non-SNAPSHOT version '$VERSION' from the snapshot job. Snapshot publishing requires a -SNAPSHOT version; releases go through the v* tag path."; exit 1 ;; | |
| esac | |
| # Informational only (nothing depends on it): logs the effective POM with the same | |
| # profile as the deploy below, so the resolved central-publishing configuration | |
| # (waitUntil/waitMaxTime etc.) is visible for debugging. | |
| - name: Show effective POM (debug) | |
| run: mvn --batch-mode --no-transfer-progress -P release help:effective-pom | |
| - name: Deploy snapshot | |
| run: mvn --batch-mode --no-transfer-progress -P release deploy -DskipTests | |
| env: | |
| MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} | |
| MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| # Runs even when the deploy step failed: a Central publish-poll timeout reds the | |
| # job *after* the bundle was uploaded (and typically published server-side), while | |
| # the signed jars + .asc files already exist in target/ (signing happens at | |
| # verify). Collecting on failure lets the github-snapshot job still attach them. | |
| - name: Collect signed artifacts | |
| if: ${{ !cancelled() }} | |
| run: | | |
| mkdir -p signed-snapshot-assets | |
| cp target/*.jar signed-snapshot-assets/ 2>/dev/null || true | |
| cp target/*.jar.asc signed-snapshot-assets/ 2>/dev/null || true | |
| - uses: actions/upload-artifact@v7 | |
| if: ${{ !cancelled() }} | |
| with: | |
| name: signed-snapshot-assets | |
| path: signed-snapshot-assets/ | |
| github-snapshot: | |
| name: Update Snapshot Pre-release on GitHub | |
| needs: [publish-snapshot] | |
| # Also runs when publish-snapshot FAILED (not when skipped/cancelled): a Central | |
| # publish-poll timeout reds that job after the artifacts were already uploaded — | |
| # the GitHub pre-release assets must not be lost in that case. | |
| if: ${{ !cancelled() && (needs.publish-snapshot.result == 'success' || needs.publish-snapshot.result == 'failure') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: signed-snapshot-assets | |
| path: snapshot-assets/ | |
| - name: Update snapshot pre-release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release view snapshot --repo ${{ github.repository }} 2>/dev/null \ | |
| || gh release create snapshot \ | |
| --repo ${{ github.repository }} \ | |
| --prerelease \ | |
| --title "Snapshot (latest)" \ | |
| --notes "Latest snapshot build from the main branch." | |
| gh release upload snapshot snapshot-assets/* \ | |
| --repo ${{ github.repository }} \ | |
| --clobber | |
| publish-release: | |
| name: Publish Release to Central | |
| needs: [check-tag, code-style] | |
| if: needs.check-tag.result == 'success' && inputs.publish_to_central | |
| runs-on: ubuntu-latest | |
| environment: maven-central | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: | |
| java-version: '21' | |
| distribution: temurin | |
| cache: maven | |
| server-id: central | |
| server-username: MAVEN_USERNAME | |
| server-password: MAVEN_PASSWORD | |
| gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} | |
| gpg-passphrase: MAVEN_GPG_PASSPHRASE | |
| # Informational only (nothing depends on it): logs the effective POM with the same | |
| # profile as the deploy below, so the resolved central-publishing configuration | |
| # (waitUntil/waitMaxTime etc.) is visible for debugging. | |
| - name: Show effective POM (debug) | |
| run: mvn --batch-mode --no-transfer-progress -P release help:effective-pom | |
| - name: Deploy release | |
| run: mvn --batch-mode --no-transfer-progress -P release deploy -DskipTests | |
| env: | |
| MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} | |
| MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| # Runs even when the deploy step failed: a Central publish-poll timeout reds the | |
| # job *after* the bundle was uploaded (and typically published server-side), while | |
| # the signed jars + .asc files already exist in target/ (signing happens at | |
| # verify). Collecting on failure lets the github-release job still attach them. | |
| - name: Collect signed artifacts | |
| if: ${{ !cancelled() }} | |
| run: | | |
| mkdir -p signed-release-assets | |
| cp target/*.jar signed-release-assets/ 2>/dev/null || true | |
| cp target/*.jar.asc signed-release-assets/ 2>/dev/null || true | |
| - uses: actions/upload-artifact@v7 | |
| if: ${{ !cancelled() }} | |
| with: | |
| name: signed-release-assets | |
| path: signed-release-assets/ | |
| github-release: | |
| name: Attach Binaries to GitHub Release | |
| needs: [publish-release] | |
| # Also runs when publish-release FAILED (not when skipped/cancelled): a Central | |
| # publish-poll timeout reds that job after the artifacts were already uploaded — | |
| # the GitHub release assets must not be lost in that case. | |
| if: ${{ !cancelled() && (needs.publish-release.result == 'success' || needs.publish-release.result == 'failure') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: signed-release-assets | |
| path: release-assets/ | |
| - name: Upload release assets | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| files: release-assets/* |