Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,119 @@ jobs:
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."

code-style:
name: Code style (spotless) + package graph
needs: startgate
Expand Down
Loading