Skip to content

Fix the concurrency group, bump tooling, unpin CodeQL and gradle actions #251

Fix the concurrency group, bump tooling, unpin CodeQL and gradle actions

Fix the concurrency group, bump tooling, unpin CodeQL and gradle actions #251

Workflow file for this run

# 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/*