From 9fdc0ff21127708a3cbf7ca1e04dea72c3f755f2 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:16:59 -0600 Subject: [PATCH 01/15] create variant of the gpg-ephemeral-key action with outputs that simplify downstream signing Signed-off-by: Sean Tronsen --- actions/gpg-configure-release-keys/README.md | 31 +++ actions/gpg-configure-release-keys/action.yml | 215 ++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 actions/gpg-configure-release-keys/README.md create mode 100644 actions/gpg-configure-release-keys/action.yml diff --git a/actions/gpg-configure-release-keys/README.md b/actions/gpg-configure-release-keys/README.md new file mode 100644 index 0000000..c35f50d --- /dev/null +++ b/actions/gpg-configure-release-keys/README.md @@ -0,0 +1,31 @@ + + +# gpg-configure-release-keys + +Creates a per-run ephemeral GPG key and certifies it with a repo-scoped +certification key, for use by downstream signing steps. + +## Key Lifecycle + +- **Master key**: public-only here — only `master-public-key-asc`/`master-fpr` + are consumed, to pass through for trust-chain export. Never imported as a + secret in this action. +- **Repo cert key**: imported from `repo-cert-key-armored-b64` (secret), used + only to certify the ephemeral key, then deleted with + `--delete-secret-keys` before this action returns. +- **Ephemeral key**: intentionally left in the exported `GNUPGHOME` — the + caller's next step (e.g. `gpg-sign-rpm`) needs the secret key. The calling + workflow, not this action, is responsible for shredding that `GNUPGHOME` + once signing is done. + +See [gpg-signing-manager](https://github.com/OpenCHAMI/gpg-signing-manager) +for key generation/rotation details. + +## Secrets vs. variables + +`MASTER_PUBLIC_ASC`/`MASTER_FPR` are public (a public key + fingerprint) — +stored as secrets here for consistency with the other key inputs, not +because they need confidentiality. Fine to move to repo/org variables. diff --git a/actions/gpg-configure-release-keys/action.yml b/actions/gpg-configure-release-keys/action.yml new file mode 100644 index 0000000..51b1f02 --- /dev/null +++ b/actions/gpg-configure-release-keys/action.yml @@ -0,0 +1,215 @@ +# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: 'configure gpg release keys for downstream tasks' +author: 'OpenCHAMI' +branding: + icon: 'lock' + color: 'purple' +description: 'Creates an ephemeral GPG key per build and certifies it with a repo-scoped certification key' + +inputs: + repo-cert-key-armored-b64: + required: true + description: 'Base64-encoded ASCII-armored repo certification secret key used to certify the ephemeral key' + master-public-key-asc: + required: true + description: 'ASCII-armored public key for the offline master key that certified the repo certification key' + master-fpr: + required: true + description: 'Full fingerprint of the offline master key' + name: + description: 'Name (real) for the ephemeral key' + default: 'Ephemeral Key' + comment: + description: 'Additional comment / metadata (will have a random suffix appended)' + default: '' + email: + description: 'Email for the ephemeral key' + default: 'ci@build.local' + key-length: + description: 'RSA key length' + default: '4096' + expire-days: + description: 'Expiration in days for the ephemeral key' + default: '1' + +outputs: + # keep private cryptographical info on disk; avoid leaking it out to logs + gnupg-home: + description: 'Path to isolated GNUPGHOME for subsequent actions' + value: ${{ steps.setup.outputs.gnupghome }} + + # directly export public key cryptography data to simplify downstream ops + ephemeral-fingerprint: + description: 'Fingerprint of the generated ephemeral key' + value: ${{ steps.export.outputs.ephemeral-fingerprint }} + ephemeral-public-key-b64: + description: 'Base64 of ASCII-armored ephemeral public key' + value: ${{ steps.export.outputs.ephemeral-public-b64 }} + ephemeral-public-key-file: + description: 'Path to ASCII-armored ephemeral public key file' + value: ${{ steps.export.outputs.ephemeral-public-file }} + repo-cert-fingerprint: + description: 'Fingerprint of the repo-cert key' + value: ${{ steps.export.outputs.repo-cert-fingerprint }} + repo-cert-public-key-b64: + description: 'Base64 of ASCII-armored repo certification public key (primary only)' + value: ${{ steps.export.outputs.repo-cert-public-b64 }} + repo-cert-public-key-file: + description: 'Path to ASCII-armored repo-cert public key file' + value: ${{ steps.export.outputs.repo-cert-public-file }} + master-fingerprint: + description: 'Fingerprint of the master key' + value: ${{ steps.export.outputs.master-fingerprint }} + master-public-key-b64: + description: 'Base64 of ASCII-armored master public key (primary only)' + value: ${{ steps.export.outputs.master-public-b64 }} + master-public-key-file: + description: 'Path to ASCII-armored master public key file' + value: ${{ steps.export.outputs.master-public-file }} + +runs: + using: "composite" + steps: + - id: setup + shell: bash + run: | + set -euo pipefail + + GNUPGHOME="$(mktemp -d)" + chmod 700 "$GNUPGHOME" + echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" + echo "gnupghome=$GNUPGHOME" >> "$GITHUB_OUTPUT" + + command -v gpg >/dev/null 2>&1 && exit 0 + SUDO=''; command -v sudo >/dev/null 2>&1 && SUDO=sudo + if command -v apt-get >/dev/null 2>&1; then + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq --no-install-recommends gnupg + elif command -v dnf >/dev/null 2>&1; then + $SUDO dnf install -y -q gnupg2 + else + echo '::error::Unsupported package manager: need apt-get or dnf' + exit 1 + fi + + - id: import + shell: bash + run: | + set -euo pipefail + echo "Importing repo certification key..." + + # Decode repo certification key + decoded=$(echo "${{ inputs.repo-cert-key-armored-b64 }}" | base64 -d 2>/dev/null || true) + if [[ -z "$decoded" ]]; then + echo "ERROR: repo-cert-key-armored-b64 is invalid or empty!" >&2 + exit 1 + fi + + # Import key + gpg --batch --import <(echo "$decoded") || { + echo "ERROR: Failed to import repo certification key" >&2 + exit 1 + } + + # List keys for debug + echo "::group::GPG secret keys" + gpg --list-secret-keys + echo "::endgroup::" + + # Get repo_cert fingerprint + repo_cert_fpr=$(gpg --batch --with-colons --list-secret-keys | awk -F: '/^fpr:/ {print $10; exit}') + if [[ -z "$repo_cert_fpr" ]]; then + echo "ERROR: No secret key found after import" >&2 + exit 1 + fi + + echo "Using REPO_CERT_FPR=$repo_cert_fpr" + echo "REPO_CERT_FPR=$repo_cert_fpr" >> "$GITHUB_ENV" + + - id: generate + shell: bash + run: | + set -euo pipefail + # Sanitize user inputs to avoid config injection + safe_name=$(printf '%s' "${{ inputs.name }}" | tr -cd '[:alnum:] ._@-') + safe_comment=$(printf '%s' "${{ inputs.comment }}" | tr -cd '[:alnum:] ._@:-') + safe_email=$(printf '%s' "${{ inputs.email }}" | tr -cd '[:alnum:]@._-') + marker=$(openssl rand -hex 6 2>/dev/null || date +%s) + safe_comment="$safe_comment build-${GITHUB_RUN_ID:-0}-$marker" + echo "MARKER=$marker" >> "$GITHUB_ENV" + expire_days="${{ inputs.expire-days }}" + key_length="${{ inputs.key-length }}" + + # No passphrase: key is ephemeral (short expiry, per expire-days + # input), lives only in this run's isolated GNUPGHOME, and is + # shredded on teardown. + printf "%%no-protection\n" > keygen.conf + printf "Key-Type: RSA\n" >> keygen.conf + printf "Key-Length: %s\n" "$key_length" >> keygen.conf + printf "Name-Real: %s\n" "$safe_name" >> keygen.conf + printf "Name-Comment: %s\n" "$safe_comment" >> keygen.conf + printf "Name-Email: %s\n" "$safe_email" >> keygen.conf + printf "Expire-Date: %sd\n" "$expire_days" >> keygen.conf + printf "Key-Usage: sign\n" >> keygen.conf + printf "%%commit\n" >> keygen.conf + + gpg --batch --gen-key keygen.conf + shred -u keygen.conf || rm -f keygen.conf + + - id: fpr + shell: bash + run: | + set -euo pipefail + echo "Available keys for debug:" + gpg --list-keys + + ephemeral_fpr=$(gpg --with-colons --list-keys | awk -F: -v m="${MARKER:-}" ' + /^fpr:/ { fpr=$10 } + /^uid:/ { + if (index($10, m) > 0) { + print fpr + exit + } + }') + + if [ -z "$ephemeral_fpr" ]; then + echo "Failed to locate ephemeral key fingerprint" >&2 + exit 1 + fi + echo "EPHEMERAL_FPR=$ephemeral_fpr" >> "$GITHUB_ENV" + + - id: sign-ephemeral + shell: bash + run: | + set -euo pipefail + gpg --batch --yes --quick-sign-key --local-user "$REPO_CERT_FPR" "$EPHEMERAL_FPR" + gpg --batch --yes --delete-secret-keys "$REPO_CERT_FPR" + + - id: export + shell: bash + env: + MASTER_PUBLIC_KEY_ASC: ${{ inputs.master-public-key-asc }} + run: | + set -euo pipefail + + gpg --armor --export "$EPHEMERAL_FPR" > "ephemeral.pub.asc" + gpg --armor --export "${REPO_CERT_FPR}!" > "repo-cert.pub.asc" + printf '%s\n' "$MASTER_PUBLIC_KEY_ASC" > "master.pub.asc" + + for k in ephemeral repo-cert master; do + test -s "$k.pub.asc" || { echo "ERROR: empty export for $k" >&2; exit 1; } + done + + { + echo "ephemeral-fingerprint=${EPHEMERAL_FPR}" + echo "ephemeral-public-b64=$(base64 -w0 < "ephemeral.pub.asc")" + echo "ephemeral-public-file=ephemeral.pub.asc" + echo "repo-cert-fingerprint=${REPO_CERT_FPR}" + echo "repo-cert-public-b64=$(base64 -w0 < "repo-cert.pub.asc")" + echo "repo-cert-public-file=repo-cert.pub.asc" + echo "master-fingerprint=${{ inputs.master-fpr }}" + echo "master-public-b64=$(base64 -w0 < "master.pub.asc")" + echo "master-public-file=master.pub.asc" + } >> "$GITHUB_OUTPUT" From 0e15bd4917ab5ab7aa83a69e702914ef79ff5cca Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:17:34 -0600 Subject: [PATCH 02/15] deprecate the previous gpg-ephemeral-key action Signed-off-by: Sean Tronsen --- actions/gpg-ephemeral-key/README.md | 4 ++++ actions/gpg-ephemeral-key/action.yml | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/actions/gpg-ephemeral-key/README.md b/actions/gpg-ephemeral-key/README.md index 69dc2a5..aa4e146 100644 --- a/actions/gpg-ephemeral-key/README.md +++ b/actions/gpg-ephemeral-key/README.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: MIT # 🛡️ GPG Ephemeral Key Generator +> [!WARNING] +> +> Deprecated: use [`gpg-configure-release-keys`](../gpg-configure-release-keys) instead. + This GitHub composite action generates a new ephemeral GPG key on every build, signs it using a repo‑scoped subkey, and exports the fingerprint and public key. It’s designed for use in CI pipelines where artifacts need secure signing without long‑lived keys in GitHub Actions. --- diff --git a/actions/gpg-ephemeral-key/action.yml b/actions/gpg-ephemeral-key/action.yml index 87b3507..3d6ce89 100644 --- a/actions/gpg-ephemeral-key/action.yml +++ b/actions/gpg-ephemeral-key/action.yml @@ -1,12 +1,12 @@ # SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT -name: 'Generate and Sign Ephemeral GPG Key' +name: '[DEPRECATED] Generate and Sign Ephemeral GPG Key' author: 'OpenCHAMI' branding: icon: 'lock' color: 'purple' -description: 'Creates an ephemeral GPG key per build and signs it with a repo-scoped subkey' +description: 'Deprecated: use gpg-configure-release-keys instead. Creates an ephemeral GPG key per build and signs it with a repo-scoped subkey' inputs: subkey-armored: @@ -45,6 +45,11 @@ outputs: runs: using: "composite" steps: + - id: deprecation-warning + shell: bash + run: | + echo "::warning::gpg-ephemeral-key is deprecated; migrate to gpg-configure-release-keys." + - id: setup shell: bash run: | From 6f057b61da0f11a595b2d5283081385a8ab48f67 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:18:39 -0600 Subject: [PATCH 03/15] refactor/rename and update documentation for the gpg-sign-rpm action Signed-off-by: Sean Tronsen --- actions/gpg-sign-rpm/README.md | 59 +++++++++++ actions/{sign-rpm => gpg-sign-rpm}/action.yml | 74 +++++++++----- actions/sign-rpm/README.md | 98 ------------------- 3 files changed, 106 insertions(+), 125 deletions(-) create mode 100644 actions/gpg-sign-rpm/README.md rename actions/{sign-rpm => gpg-sign-rpm}/action.yml (52%) delete mode 100644 actions/sign-rpm/README.md diff --git a/actions/gpg-sign-rpm/README.md b/actions/gpg-sign-rpm/README.md new file mode 100644 index 0000000..3b61fdb --- /dev/null +++ b/actions/gpg-sign-rpm/README.md @@ -0,0 +1,59 @@ + + +# RPM Signing Action + +Signs an RPM file using a GPG key fingerprint (typically an ephemeral key produced by the `gpg-configure-release-keys` action). Designed to pair with ephemeral, short-lived keys to reduce long-term key exposure in CI. + +## How It Works + +1. Assumes the GPG key (secret) is already present in the GNUPGHOME (e.g. from `gpg-configure-release-keys`). +2. Configures RPM macros to use GPG (sets `%_signature`, `%_gpg_name`, `%__gpg`, and SHA-256 digest). +3. Signs the RPM with `rpmsign --addsign` (or re-signs if `resign: true`). +4. Verifies the signature and exposes the verification output. + +Notes: +- The action supports Ubuntu and Fedora runners (installs via `apt-get` or `dnf`). +- This action imports the signer's public key into rpmdb immediately before its own verify step, so its `verification` output won't normally show `NOKEY`. Downstream consumers who checksig the RPM without importing that public key first will see `NOKEY` there; the signature is still valid, they just haven't imported the key locally. + +## Inputs + +| Name | Required | Description | +|------|----------|-------------| +| `gpg-fingerprint` | Yes | Fingerprint of the (secret) GPG key to use | +| `gnupg-home` | No | GNUPGHOME directory produced by previous step (optional) | +| `resign` | No | If `true`, remove existing signature before adding new one (default: `false`) | + +## Outputs + +| Name | Description | +|------|-------------| +| `verification` | Raw output of `rpm --checksig` after signing | + +## Example Usage + +See [`gpg-sign-artifacts.yml`](../../.github/workflows/gpg-sign-artifacts.yml) in this repo for a real usage example. + +## Security Notes + +- Prefer ephemeral keys: generate -> sign -> cleanup (see Key Lifecycle below; cleanup is the calling workflow's responsibility, not optional). +- Provide `gnupg-home` explicitly to avoid leaking into default `~/.gnupg`. +- Set `resign: true` only if you intentionally need to replace a signature. + +## Key Lifecycle + +Combine with `gpg-configure-release-keys`. That action leaves the ephemeral secret key in `GNUPGHOME` for this step to use and does not clean it up itself. The calling workflow is responsible for shredding `GNUPGHOME` once signing is done. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `no RPMs found under $(pwd)` | No `*.rpm` files under the working directory | Check the build step produced RPMs before this action runs | +| `NOKEY` when a consumer checksigs the RPM elsewhere | They haven't imported the signer's public key into their own rpmdb | Import the exported public key into rpmdb before checking | +| `BAD` in verification output | Signature mismatch or corruption | Rebuild and re-sign; ensure correct key | +| Already signed message | Existing signature and `resign` not set | Set `resign: true` | + +## License +MIT diff --git a/actions/sign-rpm/action.yml b/actions/gpg-sign-rpm/action.yml similarity index 52% rename from actions/sign-rpm/action.yml rename to actions/gpg-sign-rpm/action.yml index 72381d8..16ab580 100644 --- a/actions/sign-rpm/action.yml +++ b/actions/gpg-sign-rpm/action.yml @@ -9,9 +9,6 @@ branding: description: 'Signs an RPM using the provided GPG key fingerprint (expects GNUPGHOME from previous step)' inputs: - rpm-path: - description: 'Path to the RPM file to sign' - required: true gpg-fingerprint: description: 'Fingerprint of the GPG key to use for signing' required: true @@ -38,23 +35,22 @@ runs: apt-get update -y apt-get install -y --no-install-recommends rpm gnupg elif command -v dnf >/dev/null 2>&1; then - dnf install -y rpm-build gnupg + dnf install -y rpm-build rpm-sign gnupg fi - - - name: Configure RPM to use GPG key shell: bash env: GNUPGHOME: ${{ inputs['gnupg-home'] }} + GPG_FINGERPRINT: ${{ inputs['gpg-fingerprint'] }} run: | set -euo pipefail GPG_BIN="$(command -v gpg || command -v gpg2)" { echo "%_signature gpg" - echo "%_gpg_name ${{ inputs['gpg-fingerprint'] }}" + echo "%_gpg_name $GPG_FINGERPRINT" echo "%__gpg $GPG_BIN" - echo "%_gpg_digest_algo sha256" # <— + echo "%_gpg_digest_algo sha256" } >> "$HOME/.rpmmacros" echo "Using gpg at: $GPG_BIN" rpm --eval "%{__gpg}" || true @@ -63,35 +59,46 @@ runs: shell: bash env: GNUPGHOME: ${{ inputs['gnupg-home'] }} + GPG_FINGERPRINT: ${{ inputs['gpg-fingerprint'] }} + RESIGN: ${{ inputs.resign }} run: | set -euo pipefail echo "GNUPGHOME=$GNUPGHOME" gpg --list-secret-keys || { echo "No secret keys in GNUPGHOME"; exit 1; } - # If already signed and resign=false, skip - if rpm --checksig "${{ inputs['rpm-path'] }}" 2>/dev/null | grep -qi 'pgp signature'; then - if [ "${{ inputs.resign }}" = "true" ]; then - rpm --delsign "${{ inputs['rpm-path'] }}" || true - else - echo "Already signed; skipping (set resign=true to force)." - exit 0 + found=0 + while IFS= read -r f; do + found=1 + if rpm --checksig "$f" 2>/dev/null | grep -qi 'pgp signature'; then + if [ "$RESIGN" = "true" ]; then + rpm --delsign "$f" || true + else + echo "Already signed; skipping $f (set resign=true to force)." + continue + fi fi + # Use rpmsign (more reliable) with explicit defines + rpmsign --addsign \ + --define "_signature gpg" \ + --define "_gpg_name $GPG_FINGERPRINT" \ + --define "__gpg $(command -v gpg)" \ + --define "_gpg_digest_algo sha256" \ + "$f" + done < <(find . -type f -name '*.rpm') + if [ "$found" -eq 0 ]; then + echo "::error::no RPMs found under $(pwd)"; exit 1 fi - # Use rpmsign (more reliable) with explicit defines - rpmsign --addsign \ - --define "_signature gpg" \ - --define "_gpg_name ${{ inputs['gpg-fingerprint'] }}" \ - --define "__gpg $(command -v gpg)" \ - --define "_gpg_digest_algo sha256" \ - "${{ inputs['rpm-path'] }}" - - name: Import Signer Public Key for Verification shell: bash + env: + GNUPGHOME: ${{ inputs['gnupg-home'] }} + GPG_FINGERPRINT: ${{ inputs['gpg-fingerprint'] }} run: | set -euo pipefail - gpg --armor --export "${{ inputs.gpg-fingerprint }}" > signer.pub - rpm --import signer.pub 2>/dev/null || sudo rpm --import signer.pub 2>/dev/null || true + gpg --armor --export "$GPG_FINGERPRINT" > signer.pub + test -s signer.pub || { echo "Export produced no key for $GPG_FINGERPRINT"; exit 1; } + rpm --import signer.pub || sudo rpm --import signer.pub rm -f signer.pub - name: Verify RPM signature @@ -99,6 +106,19 @@ runs: shell: bash run: | set -euo pipefail - out=$(rpm --checksig "${{ inputs.rpm-path }}") + + RPMS=() + while IFS= read -r f; do + RPMS+=("$f") + done < <(find . -type f -name '*.rpm') + if [ "${#RPMS[@]}" -eq 0 ]; then + echo "::error::no RPMs found under $(pwd)"; exit 1 + fi + + out=$(rpm --checksig "${RPMS[@]}") echo "$out" - echo "result=$out" >> "$GITHUB_OUTPUT" + { + echo "result<> "$GITHUB_OUTPUT" diff --git a/actions/sign-rpm/README.md b/actions/sign-rpm/README.md deleted file mode 100644 index f50ca24..0000000 --- a/actions/sign-rpm/README.md +++ /dev/null @@ -1,98 +0,0 @@ - - -# 📦 RPM Signing Action - -Signs an RPM file using a GPG key fingerprint (typically an ephemeral key produced by the `gpg-ephemeral-key` action). Designed to pair with ephemeral, short‑lived keys to reduce long‑term key exposure in CI. - -## 🔧 How It Works - -1. Assumes the GPG key (secret) is already present in the GNUPGHOME (e.g. from `gpg-ephemeral-key`). -2. Configures RPM macros to use GPG (sets `%_signature`, `%_gpg_name`, `%__gpg`, and SHA‑256 digest). -3. Signs the RPM with `rpmsign --addsign` (or re‑signs if `resign: true`). -4. Verifies the signature and exposes the verification output. - -Notes: -- The action supports Ubuntu and Fedora runners (installs via `apt-get` or `dnf`). -- Verification may show `NOKEY` if the rpm database doesn’t have the public key; the signature is still valid, but rpm cannot confirm it without the public key imported. - -## 📥 Inputs - -| Name | Required | Description | -|------|----------|-------------| -| `rpm-path` | ✅ | Path to the RPM file to sign | -| `gpg-fingerprint` | ✅ | Fingerprint of the (secret) GPG key to use | -| `gnupg-home` | ❌ | GNUPGHOME directory produced by previous step (optional) | -| `resign` | ❌ | If `true`, remove existing signature before adding new one (default: `false`) | - -## 📤 Outputs - -| Name | Description | -|------|-------------| -| `verification` | Raw output of `rpm --checksig` after signing | - -## 🚀 Example Usage - -```yaml -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Generate ephemeral key - id: gpg - uses: OpenCHAMI/github-actions/actions/gpg-ephemeral-key@v1 - with: - subkey-armored: ${{ secrets.GPG_SUBKEY_B64 }} - comment: build:${{ github.run_id }} - - - name: Build RPM - run: ./scripts/build-rpm.sh - - - name: Sign RPM - id: sign - uses: OpenCHAMI/github-actions/actions/sign-rpm@v1 - with: - rpm-path: dist/my.rpm - gpg-fingerprint: ${{ steps.gpg.outputs.ephemeral-fingerprint }} - gnupg-home: ${{ steps.gpg.outputs.gnupg-home }} - - - name: Show verification - run: echo "${{ steps.sign.outputs.verification }}" -``` - -Optional: Import the public key into rpmdb to avoid `NOKEY` in verification output: - -```yaml - - name: Import public key for rpm verification - run: | - GNUPGHOME="${{ steps.gpg.outputs.gnupg-home }}"; export GNUPGHOME - gpg --armor --export "${{ steps.gpg.outputs.ephemeral-fingerprint }}" > signer.pub - sudo rpm --import signer.pub - rm -f signer.pub -``` - -## 🔐 Security Notes - -- Prefer ephemeral keys: generate -> sign -> (optionally) cleanup. -- Provide `gnupg-home` explicitly to avoid leaking into default `~/.gnupg`. -- Set `resign: true` only if you intentionally need to replace a signature. - -## ♻️ Key Lifecycle - -Combine with the ephemeral key action and set `cleanup: true` unless later steps need the secret key. - -## 🛠 Troubleshooting - -| Symptom | Cause | Fix | -|---------|-------|-----| -| `No such file or directory` | Wrong `rpm-path` | Check artifact path and build step | -| `NOKEY` in verification output | rpmdb missing public key | Import the exported public key into rpmdb | -| `BAD` in verification output | Signature mismatch or corruption | Rebuild and re‑sign; ensure correct key | -| Already signed message | Existing signature and `resign` not set | Set `resign: true` | - -## 📝 License -MIT From 0f05d1f2fd77bb27fd61feaa654223b00163f6af Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:19:13 -0600 Subject: [PATCH 04/15] add action to check for expired gpg keys Signed-off-by: Sean Tronsen --- actions/gpg-check-key-expiration/README.md | 14 +++ actions/gpg-check-key-expiration/action.yml | 95 +++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 actions/gpg-check-key-expiration/README.md create mode 100644 actions/gpg-check-key-expiration/action.yml diff --git a/actions/gpg-check-key-expiration/README.md b/actions/gpg-check-key-expiration/README.md new file mode 100644 index 0000000..0d36c08 --- /dev/null +++ b/actions/gpg-check-key-expiration/README.md @@ -0,0 +1,14 @@ + + +# gpg-check-key-expiration + +Fails the job if a provided secret key is expired or expires within +`warn-days`. Imports into an isolated `GNUPGHOME` that's shredded on exit; +never touches the runner's default keyring. + +Expiry check itself is fetched at runtime, pinned to a commit SHA, from +[gpg-signing-manager](https://github.com/OpenCHAMI/gpg-signing-manager)'s +`check-key-expiry.sh`. diff --git a/actions/gpg-check-key-expiration/action.yml b/actions/gpg-check-key-expiration/action.yml new file mode 100644 index 0000000..3522307 --- /dev/null +++ b/actions/gpg-check-key-expiration/action.yml @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: 'Check key expiration' +author: 'OpenCHAMI' +branding: { icon: 'clock', color: 'orange' } +description: >- + Fails if the provided signing secret material is expired or expiring within + warn-days. Imports into an isolated, shredded GNUPGHOME; never touches the + runner default keyring. + +inputs: + repo-key-armored-b64: + description: >- + Base64-encoded ASCII-armored secret key export to check. b64 wrapping + exists only to survive newline mangling when provisioning the secret + (gh secret set); it is decoded exactly once here. + required: true + secret-name: + description: 'GitHub secret name expected to hold repo-key-armored-b64 (used in error messages only)' + required: false + default: 'GPG_REPO_KEY_B64' + legacy-secret-name: + description: 'Legacy secret name mentioned in migration hints (error messages only)' + required: false + default: 'GPG_SUBKEY_B64' + warn-days: + description: 'Fail if any key expires within this many days' + required: false + default: '30' + +runs: + using: composite + steps: + - name: Install GnuPG + shell: bash + run: | + set -euo pipefail + command -v gpg >/dev/null 2>&1 && exit 0 + SUDO=''; command -v sudo >/dev/null 2>&1 && SUDO=sudo + if command -v apt-get >/dev/null 2>&1; then + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq --no-install-recommends gnupg + elif command -v dnf >/dev/null 2>&1; then + $SUDO dnf install -y -q gnupg2 + else + echo '::error::Unsupported package manager: need apt-get or dnf' + exit 1 + fi + + - name: Check signing key expiry + shell: bash + env: + REPO_KEY_ARMORED_B64: ${{ inputs.repo-key-armored-b64 }} + SECRET_NAME: ${{ inputs.secret-name }} + LEGACY_SECRET_NAME: ${{ inputs.legacy-secret-name }} + WARN_DAYS: ${{ inputs.warn-days }} + run: | + set -euo pipefail + if [[ -z "${REPO_KEY_ARMORED_B64//[[:space:]]/}" ]]; then + echo "::error::Input 'repo-key-armored-b64' is empty. No key material was provided." + echo "::error::Expected repo secret '${SECRET_NAME}' to contain a base64 armored secret signing key." + echo "::notice::Set secret: gh secret set ${SECRET_NAME} --repo ${GITHUB_REPOSITORY} < -secret-subkeys.b64" + echo "::notice::If you still use '${LEGACY_SECRET_NAME}', rename it or pass it explicitly." + exit 1 + fi + + export GNUPGHOME="$(mktemp -d)" + chmod 700 "$GNUPGHOME" + trap 'find "$GNUPGHOME" -type f -exec shred -u {} + 2>/dev/null || true; rm -rf "$GNUPGHOME"' EXIT + + if ! printf '%s' "$REPO_KEY_ARMORED_B64" | base64 -d \ + > "$GNUPGHOME/signing-key.asc" 2>"$GNUPGHOME/decode.log"; then + echo "::error::Failed to decode repo-key-armored-b64 as base64." + sed 's/^/::error::base64: /' "$GNUPGHOME/decode.log" + echo "::notice::Re-export the secret payload and store it as ${SECRET_NAME}." + exit 1 + fi + if ! gpg --batch --import "$GNUPGHOME/signing-key.asc" >/dev/null 2>"$GNUPGHOME/import.log"; then + echo "::error::Failed to import decoded key material into a temporary keyring." + sed 's/^/::error::gpg: /' "$GNUPGHOME/import.log" + echo "::notice::Ensure ${SECRET_NAME} contains a SECRET key export (not a public key)." + exit 1 + fi + + workdir=$(mktemp -d) + trap 'rm -rf "$workdir"' EXIT + script="$workdir/check-key-expiry.sh" + curl -fsSL --retry 3 -o "$script" \ + "https://raw.githubusercontent.com/OpenCHAMI/gpg-signing-manager/d4366a45c94be50cbb7f90d229c10f395b6d10d8/scripts/check-key-expiry.sh" + + bash "$script" \ + --gnupghome "$GNUPGHOME" \ + --threshold-days "$WARN_DAYS" \ + --github-annotations From d84ac70172059929565bfe6c69f3ae729cd3d166 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:19:49 -0600 Subject: [PATCH 05/15] add action to verify hierarchical gpg trust chain Signed-off-by: Sean Tronsen --- actions/gpg-verify-trust-chain/README.md | 14 +++ actions/gpg-verify-trust-chain/action.yml | 120 ++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 actions/gpg-verify-trust-chain/README.md create mode 100644 actions/gpg-verify-trust-chain/action.yml diff --git a/actions/gpg-verify-trust-chain/README.md b/actions/gpg-verify-trust-chain/README.md new file mode 100644 index 0000000..201ed28 --- /dev/null +++ b/actions/gpg-verify-trust-chain/README.md @@ -0,0 +1,14 @@ + + +# gpg-verify-trust-chain + +Verifies the release trust chain (master certifies repo key, repo key +certifies ephemeral key) and optionally checksigs any RPMs found under +`rpm-dir`. Standalone: installs its own deps, no prior GPG state assumed. + +Verification itself is fetched at runtime, pinned to a commit SHA, from +[gpg-signing-manager](https://github.com/OpenCHAMI/gpg-signing-manager)'s +`verify-chain.sh`. diff --git a/actions/gpg-verify-trust-chain/action.yml b/actions/gpg-verify-trust-chain/action.yml new file mode 100644 index 0000000..b25ea2a --- /dev/null +++ b/actions/gpg-verify-trust-chain/action.yml @@ -0,0 +1,120 @@ +name: 'Verify gpg trust chain' +description: >- + Verifies the release GPG trust chain (master certifies repo key, repo key certifies ephemeral key) and optionally checks RPM signatures. Standalone: installs its own dependencies and runs verify-chain.sh, fetched at runtime from a pinned commit in gpg-signing-manager. +inputs: + master-public-key: + description: >- + ASCII-armored master public key content (e.g. from a secret). If empty and master-public-key-file is also empty, the chain check is skipped with a warning (exit 0) unless require-master is 'true'. + required: false + default: '' + master-public-key-file: + description: 'Path to the master public key file. Takes precedence over master-public-key.' + required: false + default: '' + repo-public-key-file: + description: 'Path to the repo public key, certified by the master key' + required: false + default: 'repo-public.asc' + ephemeral-public-key-file: + description: 'Path to the ephemeral public key, certified by the repo key' + required: false + default: 'ephemeral-public.asc' + rpm-dir: + description: >- + Directory searched recursively for *.rpm to signature-check. Empty dir or no matches is not an error; key-chain checks still run. + required: false + default: 'dist' + require-master: + description: "If 'true', fail instead of skipping when no master key is provided" + required: false + default: 'false' +outputs: + verified: + description: "'true' if the chain was verified, 'skipped' if no master key was provided" + value: ${{ steps.verify.outputs.verified }} +runs: + using: 'composite' + steps: + - name: Install dependencies + shell: bash + run: | + need=() + command -v gpg >/dev/null 2>&1 || need+=(gnupg2) + command -v rpm >/dev/null 2>&1 || need+=(rpm) + [[ ${#need[@]} -eq 0 ]] && exit 0 + SUDO='' + command -v sudo >/dev/null 2>&1 && SUDO=sudo + if command -v apt-get >/dev/null 2>&1; then + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq "${need[@]/#rpm/rpm}" + elif command -v dnf >/dev/null 2>&1; then + $SUDO dnf install -y -q "${need[@]}" + else + echo '::error::Unsupported package manager: need apt-get or dnf' >&2 + exit 1 + fi + - name: Verify trust chain + id: verify + shell: bash + env: + MASTER_KEY_CONTENT: ${{ inputs.master-public-key }} + MASTER_KEY_FILE: ${{ inputs.master-public-key-file }} + REPO_KEY_FILE: ${{ inputs.repo-public-key-file }} + EPHEMERAL_KEY_FILE: ${{ inputs.ephemeral-public-key-file }} + RPM_DIR: ${{ inputs.rpm-dir }} + REQUIRE_MASTER: ${{ inputs.require-master }} + run: |- + set -euo pipefail + + # --- Resolve the master public key ----------------------------------- + workdir=$(mktemp -d) + trap 'rm -rf "$workdir"' EXIT + + master_key='' + if [[ -n "$MASTER_KEY_FILE" ]]; then + master_key="$MASTER_KEY_FILE" + elif [[ -n "$MASTER_KEY_CONTENT" ]]; then + master_key="$workdir/master-public.asc" + printf '%s\n' "$MASTER_KEY_CONTENT" > "$master_key" + fi + + if [[ -z "$master_key" ]]; then + if [[ "$REQUIRE_MASTER" == 'true' ]]; then + echo '::error::No master public key provided and require-master is true' + exit 1 + fi + echo '::warning::No master public key provided; skipping trust chain verification' + echo 'verified=skipped' >> "$GITHUB_OUTPUT" + exit 0 + fi + + for f in "$master_key" "$REPO_KEY_FILE" "$EPHEMERAL_KEY_FILE"; do + if [[ ! -f "$f" ]]; then + echo "::error::Key file not found: $f" + exit 1 + fi + done + + # --- Collect RPMs (optional) ----------------------------------------- + rpm_args=() + if [[ -n "$RPM_DIR" && -d "$RPM_DIR" ]]; then + while IFS= read -r -d '' r; do + rpm_args+=(--rpm "$r") + done < <(find "$RPM_DIR" -name '*.rpm' -print0) + fi + if [[ ${#rpm_args[@]} -eq 0 ]]; then + echo "No RPMs found under '${RPM_DIR:-}'; verifying key chain only." + fi + + # --- Run the verifier --------------------------------------- + script="$workdir/verify-chain.sh" + curl -fsSL --retry 3 -o "$script" \ + "https://raw.githubusercontent.com/OpenCHAMI/gpg-signing-manager/d4366a45c94be50cbb7f90d229c10f395b6d10d8/scripts/verify-chain.sh" + + bash "$script" \ + --master "$master_key" \ + --repo "$REPO_KEY_FILE" \ + --ephemeral "$EPHEMERAL_KEY_FILE" \ + "${rpm_args[@]+"${rpm_args[@]}"}" + + echo 'verified=true' >> "$GITHUB_OUTPUT" From 177704880c6828b7fb6275a15cbf798e77644488 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:20:49 -0600 Subject: [PATCH 06/15] add reusable workflow for software build + registry publish via goreleaser Signed-off-by: Sean Tronsen --- .../build-publish-container-goreleaser.yml | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .github/workflows/build-publish-container-goreleaser.yml diff --git a/.github/workflows/build-publish-container-goreleaser.yml b/.github/workflows/build-publish-container-goreleaser.yml new file mode 100644 index 0000000..0e812f4 --- /dev/null +++ b/.github/workflows/build-publish-container-goreleaser.yml @@ -0,0 +1,104 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# +# Reusable workflow: builds and publishes a container image via GoReleaser, +# with multi-arch builds, build provenance attestation, and PR snapshot +# support. + +name: Build and publish container using goreleaser +on: + workflow_call: + inputs: + cgo_enabled: + type: number + required: false + default: 0 + is_pr_build: + type: boolean + required: false + default: false + pr_number: + type: number + required: false + default: ${{ github.event.number || 0 }} + registry_subject_name: + type: string + required: true +jobs: + container_build_publish: + runs-on: ubuntu-latest + steps: + - name: Set up latest stable Go + uses: actions/setup-go@v6.4.0 + with: + go-version: stable + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + image=moby/buildkit:master + network=host + - name: Docker Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-tags: true + fetch-depth: 0 + # Set environment variables required by GoReleaser + - name: Set build environment variables + run: | + { + GIT_STATE='dirty' + if git diff-index --quiet HEAD -- >/dev/null 2>&1; then + GIT_STATE='clean' + fi + + echo "GIT_STATE=${GIT_STATE}" + echo "BUILD_HOST=$(hostname)" + echo "GO_VERSION=$(go version | awk '{print $3}')" + echo "BUILD_USER=$(whoami)" + echo "CGO_ENABLED=${{ inputs.cgo_enabled }}" + echo "IS_PR_BUILD=${{ inputs.is_pr_build }}" + } >> "${GITHUB_ENV}" + - name: Create Tag for PR + if: ${{ inputs.is_pr_build }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git tag -f -a pr-${{ inputs.pr_number }} -m "PR Release" + - name: Build/Push/Release container with goreleaser + uses: goreleaser/goreleaser-action@v6 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + version: '~> 2' + args: release --clean ${{ inputs.is_pr_build && '--skip=announce,validate,archive' || '' }} + id: goreleaser + - name: Process goreleaser output + id: process_goreleaser_output + run: | + node - <<'EOF' + const fs = require('fs'); + const artifacts = ${{ steps.goreleaser.outputs.artifacts }}; + const firstNonNullDigest = artifacts.find(artifact => artifact.extra && artifact.extra.Digest != null)?.extra.Digest; + console.log(firstNonNullDigest); + fs.writeFileSync('digest.txt', firstNonNullDigest); + EOF + echo "digest=$(cat digest.txt)" >> "${GITHUB_OUTPUT}" + - name: Attest Binaries + uses: actions/attest-build-provenance@v4.1.0 + with: + subject-path: dist/** + - name: generate build provenance + uses: actions/attest-build-provenance@v4.1.0 + with: + subject-name: ${{ inputs.registry_subject_name }} + subject-digest: ${{ steps.process_goreleaser_output.outputs.digest }} + push-to-registry: true From bd582e14bfd74427c23d91d767d28b67eeec0fb4 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:21:21 -0600 Subject: [PATCH 07/15] add reusable workflow for building RPMs to distribute podman quadlets Signed-off-by: Sean Tronsen --- .github/workflows/build-rpm-quadlet.yml | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/build-rpm-quadlet.yml diff --git a/.github/workflows/build-rpm-quadlet.yml b/.github/workflows/build-rpm-quadlet.yml new file mode 100644 index 0000000..4cf0614 --- /dev/null +++ b/.github/workflows/build-rpm-quadlet.yml @@ -0,0 +1,41 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# +# Reusable workflow: builds the caller repo's podman quadlet RPM and +# uploads it as an unsigned artifact for downstream signing. + +name: Build RPM for Podman Quadlet Files +run-name: Create Podman Quadlet RPM for ${{ github.ref }} +on: + workflow_call: + inputs: + artifact-name-unsigned-rpms: + description: 'Artifact-name for unsigned RPM artifacts' + default: 'rpms-unsigned' + type: string +jobs: + rpmbuild: + runs-on: ubuntu-latest + container: + image: rockylinux:9 + steps: + - name: Install build dependencies + run: dnf install -y -q git make rpm-build rpmlint tar gzip + + - name: Mark workspace as a safe git directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-tags: true + fetch-depth: 0 + + - name: Build RPM + run: make rpm-build + + - name: Upload RPM + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.artifact-name-unsigned-rpms }} + path: '**/*.rpm' From 3216a5cdf687561a150337075ad55e9af3f39b98 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:21:52 -0600 Subject: [PATCH 08/15] add distro-agnostic reusable workflow for gpg signing distributable artifacts Signed-off-by: Sean Tronsen --- .github/workflows/gpg-sign-artifacts.yml | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 .github/workflows/gpg-sign-artifacts.yml diff --git a/.github/workflows/gpg-sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml new file mode 100644 index 0000000..66822b2 --- /dev/null +++ b/.github/workflows/gpg-sign-artifacts.yml @@ -0,0 +1,114 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# +# Reusable workflow: intended as the common entry point for signing all +# release artifact types (RPMs today; other formats later) with a per-run +# ephemeral key certified through the repo's release key chain. +# +# Keep this file up to date and maintained as we add other package signing +# tasks (e.g. .deb, Arch packages). + +name: GPG Sign artifacts +run-name: Create signed artifacts for ${{ github.ref }} +on: + workflow_call: + inputs: + artifact-name-unsigned-rpms: + description: 'Artifact-name for unsigned RPM artifacts' + default: 'rpms-unsigned' + type: string + artifact-name-signed-rpms: + description: 'Name for the signed RPM composite artifact' + type: string + default: 'rpms-signed' + artifact-name-public-keys: + description: 'Name for the public key composite artifact' + type: string + default: 'public-keys' +jobs: + artifacts-sign: + runs-on: ubuntu-latest + container: + image: rockylinux:9 + steps: + + - name: Install build dependencies + run: | + dnf install -y -q git make rpm-build rpmlint tar gzip + + - name: Mark workspace as a safe git directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-tags: true + fetch-depth: 0 + + - name: Check for repo key expiry + uses: OpenCHAMI/github-actions/actions/gpg-check-key-expiration@dev-rpm-quadlets + with: + repo-key-armored-b64: ${{ secrets.GPG_REPO_KEY_B64 }} + warn-days: '30' + + - name: Configure GPG release keys + id: gpg + uses: OpenCHAMI/github-actions/actions/gpg-configure-release-keys@dev-rpm-quadlets + with: + repo-cert-key-armored-b64: ${{ secrets.GPG_REPO_CERT_KEY_B64 }} + master-public-key-asc: ${{ secrets.MASTER_PUBLIC_ASC }} + master-fpr: ${{ secrets.MASTER_FPR }} + name: '${{ github.repository }} Release' + comment: 'ephemeral key for ${{ github.ref_name }}' + email: 'release@packages.openchami.org' + expire-days: '1' + + - name: Download RPM artifacts requested for release + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name-unsigned-rpms }} + path: dist + + - name: Sign rpms + id: rpmsign + uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@dev-rpm-quadlets + with: + resign: true + gnupg-home: ${{ steps.gpg.outputs.gnupg-home }} + gpg-fingerprint: ${{ steps.gpg.outputs.ephemeral-fingerprint }} + + - name: Verify trust chain + uses: OpenCHAMI/github-actions/actions/gpg-verify-trust-chain@dev-rpm-quadlets + with: + master-public-key: ${{ secrets.MASTER_PUBLIC_ASC }} + require-master: false + repo-public-key-file: ${{ steps.gpg.outputs.repo-cert-public-key-file }} + ephemeral-public-key-file: ${{ steps.gpg.outputs.ephemeral-public-key-file }} + rpm-dir: . + + - name: rpmlint + run: rpmlint "$(find . -name '*.rpm')" || true + + - name: Upload signed RPMs + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.artifact-name-signed-rpms }} + path: '**/*.rpm' + overwrite: true + + - name: Upload public signing keys + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.artifact-name-public-keys }} + path: '**/*.pub.asc' + overwrite: true + + - name: Cleanup GNUPGHOME + if: always() + env: + GNUPGHOME_PATH: ${{ steps.gpg.outputs.gnupg-home }} + run: | + set -euo pipefail + [ -n "$GNUPGHOME_PATH" ] && [ -d "$GNUPGHOME_PATH" ] || exit 0 + find "$GNUPGHOME_PATH" -type f -exec shred -u {} + 2>/dev/null || true + rm -rf "$GNUPGHOME_PATH" From 6c741d892e23e3923acf517a4542db9330d33875 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:23:01 -0600 Subject: [PATCH 09/15] add reusable workflow for validating RPM release artifacts Signed-off-by: Sean Tronsen --- .github/workflows/validate-rpm-quadlet.yml | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/validate-rpm-quadlet.yml diff --git a/.github/workflows/validate-rpm-quadlet.yml b/.github/workflows/validate-rpm-quadlet.yml new file mode 100644 index 0000000..962345a --- /dev/null +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -0,0 +1,66 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# +# Reusable workflow: validates a signed quadlet RPM's installed file list +# against the set of files the caller expects it to ship. + +name: Validate Podman Quadlet RPM +run-name: Validate Podman Quadlet RPM for ${{ github.ref }} +on: + workflow_call: + inputs: + artifact-name-signed-rpms: + description: 'Artifact-name for signed RPM artifacts' + default: 'rpms-signed' + type: string + expected-files: + description: 'a list of files the RPM is expected to install (newline delimited or multiline yaml string)' + required: true + type: string + +jobs: + rpmvalidate: + runs-on: ubuntu-latest + container: + image: rockylinux:9 + steps: + + - name: Install build dependencies + run: dnf install -y -q git rpmlint tar gzip diffutils + + - name: Mark workspace as a safe git directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - uses: actions/checkout@v6.0.2 + with: + fetch-tags: true + fetch-depth: 0 + + - name: Download signed RPM artifacts + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name-signed-rpms }} + path: dist/rpms + + - name: Find Quadlet RPM + run: | + set -euo pipefail + + quadlet_rpm=$(find . -type f -iname "*.rpm" | head -n 1) + if [ -z "${quadlet_rpm}" ]; then + echo "could not locate quadlet rpm file" + exit 1 + fi + + echo "using QUADLET_RPM_PATH=${quadlet_rpm}" + echo "QUADLET_RPM_PATH=${quadlet_rpm}" >> "$GITHUB_ENV" + + - name: Verify installed file list is exactly what's expected + shell: bash + env: + EXPECTED_FILES: ${{ inputs.expected-files }} + run: | + set -euo pipefail + rpm -qlp "${QUADLET_RPM_PATH}" | sort | grep -v '^$' > /tmp/actual-files.txt + printf '%s\n' "${EXPECTED_FILES}" | sort | grep -v '^$' > /tmp/expected-files.txt + diff /tmp/expected-files.txt /tmp/actual-files.txt From 25e463197d80869b41fe3d3d48fa55dcebb2ee6c Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:23:49 -0600 Subject: [PATCH 10/15] add distro-agnostic reusable workflow for publishing releases with docs for OpenCHAMI org signing conventions Signed-off-by: Sean Tronsen --- .../workflows/release-signed-artifacts.yml | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/release-signed-artifacts.yml diff --git a/.github/workflows/release-signed-artifacts.yml b/.github/workflows/release-signed-artifacts.yml new file mode 100644 index 0000000..526f895 --- /dev/null +++ b/.github/workflows/release-signed-artifacts.yml @@ -0,0 +1,95 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# +# Reusable workflow: publishes a GitHub Release for a tag, attaching signed +# RPMs and public keys, with trust-chain verification instructions in the +# release body. +name: Release signed artifacts +run-name: Generate release with signed artifacts for ${{ github.ref }} +permissions: + contents: write +on: + workflow_call: + inputs: + artifact-name-signed-rpms: + description: 'Name for the signed RPM composite artifact' + type: string + default: 'rpms-signed' + required: false + artifact-name-public-keys: + description: 'Name for the public key composite artifact' + type: string + default: 'public-keys' + required: false +jobs: + artifacts-release: + runs-on: ubuntu-latest + container: + image: rockylinux:9 + steps: + - name: Install dependencies + run: | + dnf install -y -q git tar gzip zip + - name: Mark workspace as a safe git directory + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-tags: true + fetch-depth: 0 + - name: Download signed RPM artifacts + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name-signed-rpms }} + path: dist/rpms + - name: Download public key artifacts + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name-public-keys }} + path: dist/keys + - name: Create GitHub Release + uses: softprops/action-gh-release@v3.0.2 + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + fail_on_unmatched_files: true + files: | + dist/rpms/**/*.rpm + dist/keys/**/*.asc + body: |- + ## GPG Signature Verification + + Each RPM in this release is signed with a short-lived ephemeral key that + is certified by the repository signing key, which is itself certified by + the OpenCHAMI offline master key. + + ### Trust chain + + ``` + offline master key + └─[certifies]─> repo key + └─[certifies]─> ephemeral key (${{ github.ref_name }}) + └─[signs]─> RPM files + ``` + + ### How to verify + + 1. Download `repo-cert.pub.asc` and `ephemeral.pub.asc` from this release. + 2. Import both keys: + ```bash + gpg --import repo-public.asc ephemeral-public.asc + ``` + 3. Verify each RPM: + ```bash + rpm --checksig *.rpm + ``` + 4. For full chain verification (requires the master public key): + ```bash + curl -LO \ + https://raw.githubusercontent.com/OpenCHAMI/gpg-signing-manager/main/scripts/verify-chain.sh + bash verify-chain.sh \ + --master master.pub.asc \ + --repo repo-cert.pub.asc \ + --ephemeral ephemeral.pub.asc \ + --rpm *.rpm + ``` From 16d0c8cb02d82095745144f54c3b88d93c4076b8 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:24:32 -0600 Subject: [PATCH 11/15] update README with documentation for new reusable actions + workflows Signed-off-by: Sean Tronsen --- README.md | 208 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 162 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index a398d65..ee82ca8 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,18 @@ Reusable GitHub Actions for CI/CD. ## Structure -- `actions/gpg-ephemeral-key`: Ephemeral key generation for RPM/GPG signing -- `actions/sign-rpm`: RPM signing with ephemeral keys +- `actions/gpg-ephemeral-key`: **Deprecated** - use `actions/gpg-configure-release-keys` instead +- `actions/gpg-configure-release-keys`: Generates and certifies a per-run ephemeral GPG key through the repo's release key chain +- `actions/gpg-sign-rpm`: RPM signing with ephemeral keys +- `actions/gpg-check-key-expiration`: Fails CI if a signing key is expired or expiring soon +- `actions/gpg-verify-trust-chain`: Verifies the master/repo-cert/ephemeral trust chain and optionally checksigs RPMs - `.github/workflows/go-build-release.yml`: Reusable workflow for GoReleaser builds - `.github/workflows/docker-build-release.yml`: Reusable workflow for multi-arch container image builds +- `.github/workflows/build-publish-container-goreleaser.yml`: Builds and publishes a container image via GoReleaser +- `.github/workflows/build-rpm-quadlet.yml`: Builds a caller repo's podman quadlet RPM +- `.github/workflows/gpg-sign-artifacts.yml`: Signs unsigned RPM artifacts with a per-run ephemeral key +- `.github/workflows/validate-rpm-quadlet.yml`: Validates a signed quadlet RPM's installed file list +- `.github/workflows/release-signed-artifacts.yml`: Publishes a GitHub Release with signed RPMs and public keys - `.github/workflows/lint-workflows.yml`: Reusable workflow that lints workflow files (actionlint + zizmor) - `.github/workflows/govulncheck.yml`: Reusable workflow that scans Go modules for known CVEs - `.github/workflows/dependency-review.yml`: Reusable workflow that gates PRs introducing CVE-flagged deps @@ -24,8 +32,8 @@ Use major version tags for stability: ```yaml # For actions -- uses: OpenCHAMI/github-actions/actions/gpg-ephemeral-key@v1 -- uses: OpenCHAMI/github-actions/actions/sign-rpm@v1 +- uses: OpenCHAMI/github-actions/actions/gpg-configure-release-keys@v1 +- uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@v1 # For reusable workflows jobs: @@ -33,9 +41,9 @@ jobs: uses: OpenCHAMI/github-actions/.github/workflows/go-build-release.yml@v3.3 ``` -Pin a commit SHA internally for maximum supply‑chain safety if desired. +Pin a commit SHA internally for maximum supply-chain safety if desired. -## Actions and Workflows Overview +## Workflows ### go-build-release (Reusable Workflow) Standardized GoReleaser workflow for building and releasing Go applications with: @@ -75,8 +83,8 @@ See the [workflow](.github/workflows/go-build-release.yml) for additional input ### lint-workflows (Reusable Workflow) Lints the caller repo's GitHub Actions workflow files. -- **actionlint** — syntax validation, shellcheck on `run:` steps, deprecated-action checks. -- **zizmor** — security-focused static analysis (script injection, excessive permissions, unpinned third-party actions). Uploads SARIF findings to the caller's GitHub Advanced Security tab. +- **actionlint** - syntax validation, shellcheck on `run:` steps, deprecated-action checks. +- **zizmor** - security-focused static analysis (script injection, excessive permissions, unpinned third-party actions). Uploads SARIF findings to the caller's GitHub Advanced Security tab. **Usage:** ```yaml @@ -145,68 +153,176 @@ jobs: image-ref: ghcr.io/openchami/foo:${{ github.sha }} ``` -### gpg-ephemeral-key -Generates a short‑lived RSA key (default 3072‑bit, 1 day) using an isolated `GNUPGHOME`, signs it with a repo‑scoped subkey you provide, and outputs: -- `ephemeral-fingerprint` -- `ephemeral-public-key` (base64 of armored) -- `gnupg-home` (path for downstream steps) +### build-publish-container-goreleaser (Reusable Workflow) +Builds and publishes a container image via GoReleaser, with multi-arch builds, build provenance attestation, and PR snapshot support. -### sign-rpm -Signs an RPM using a provided GPG fingerprint (works with the ephemeral key output) and exposes signature verification output. +**Usage:** +```yaml +jobs: + build: + uses: OpenCHAMI/github-actions/.github/workflows/build-publish-container-goreleaser.yml@v3.5 + with: + registry_subject_name: ghcr.io/openchami/foo +``` + +### build-rpm-quadlet (Reusable Workflow) +Builds the caller repo's podman quadlet RPM and uploads it as an unsigned artifact for downstream signing. + +**Usage:** +```yaml +jobs: + build: + uses: OpenCHAMI/github-actions/.github/workflows/build-rpm-quadlet.yml@v3.5 +``` + +### gpg-sign-artifacts (Reusable Workflow) +Signs unsigned RPM artifacts with a per-run ephemeral key certified through the repo's release key chain, verifies the chain, and uploads the signed RPMs and public keys. Intended as the common entry point for signing all release artifact types (RPMs today; other formats later). + +**Usage:** +```yaml +jobs: + sign: + uses: OpenCHAMI/github-actions/.github/workflows/gpg-sign-artifacts.yml@v3.5 + secrets: inherit +``` + +### validate-rpm-quadlet (Reusable Workflow) +Validates a signed quadlet RPM's installed file list against the set of files the caller expects it to ship. + +**Usage:** +```yaml +jobs: + validate: + uses: OpenCHAMI/github-actions/.github/workflows/validate-rpm-quadlet.yml@v3.5 + with: + expected-files: | + /etc/containers/systemd/foo.container +``` + +### release-signed-artifacts (Reusable Workflow) +Publishes a GitHub Release for a tag, attaching signed RPMs and public keys, with trust-chain verification instructions in the release body. + +**Usage:** +```yaml +jobs: + release: + uses: OpenCHAMI/github-actions/.github/workflows/release-signed-artifacts.yml@v3.5 +``` + +## Actions + +### gpg-ephemeral-key (Deprecated - use gpg-configure-release-keys) +Generates a short-lived RSA key and signs it with a repo-scoped subkey. See the [action README](actions/gpg-ephemeral-key/README.md). + +### gpg-configure-release-keys +Generates a per-run ephemeral GPG key, certified through the repo's release key chain (master certifies a repo cert key, which certifies the ephemeral key). See the [action README](actions/gpg-configure-release-keys/README.md). + +### gpg-sign-rpm +Signs an RPM using a provided GPG fingerprint (works with the ephemeral key output from `gpg-configure-release-keys`) and exposes signature verification output. See the [action README](actions/gpg-sign-rpm/README.md). + +### gpg-check-key-expiration +Fails CI if the provided signing key is expired or expiring within a threshold. See the [action README](actions/gpg-check-key-expiration/README.md). + +### gpg-verify-trust-chain +Verifies the master/repo-cert/ephemeral trust chain and optionally checksigs RPMs. See the [action README](actions/gpg-verify-trust-chain/README.md). ## Security Model -Trust chain: `Ephemeral Key ← Repo Subkey ← Offline Master Key`. +Trust chain: `Ephemeral Key <- Repo Cert Key <- Offline Master Key`. Design principles: - Ephemeral keys reduce exposure window. -- Repo subkeys are easily revocable & rotated. +- Repo cert keys are easily revocable & rotated. - Isolated `GNUPGHOME` avoids polluting runner defaults. -- Optional cleanup to remove secrets post‑sign. +- GNUPGHOME cleanup is the calling workflow's responsibility, not optional. Key expiration limits future signing only; existing signatures remain valid if the trust chain remains intact. ## Example Workflow (Combined) +Adapted from metadata-service's PR build workflow, chaining container build, RPM build, signing, and validation: + ```yaml +name: Build each PR for testing and validation +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened, edited] + workflow_dispatch: + inputs: + pr_number: + description: 'PR Number to build (optional, for manual PR builds)' + required: false + type: string + +permissions: write-all # Necessary for the generate-build-provenance action with containers jobs: - build-and-sign: + + config: runs-on: ubuntu-latest + outputs: + rpm-unsigned: ${{ steps.names.outputs.rpm-unsigned }} + rpm-signed: ${{ steps.names.outputs.rpm-signed }} + keys-public: ${{ steps.names.outputs.keys-public }} steps: - - uses: actions/checkout@v4 - - name: Generate ephemeral key - id: gpg - uses: OpenCHAMI/github-actions/actions/gpg-ephemeral-key@v1 - with: - subkey-armored: ${{ secrets.GPG_SUBKEY_B64 }} - comment: build:${{ github.run_id }} - cleanup: false # keep for subsequent signing - - name: Build RPM - run: ./scripts/build-rpm.sh - - name: Sign RPM - id: sign - uses: OpenCHAMI/github-actions/actions/sign-rpm@v1 - with: - rpm-path: dist/my.rpm - gpg-fingerprint: ${{ steps.gpg.outputs.ephemeral-fingerprint }} - gnupg-home: ${{ steps.gpg.outputs.gnupg-home }} - - name: (Optional) Cleanup GNUPGHOME - if: always() - run: rm -rf "${{ steps.gpg.outputs.gnupg-home }}" + - id: names + run: | + { + echo "rpm-unsigned=rpms-unsigned" + echo "rpm-signed=rpms-signed" + echo "keys-public=public-keys" + } >> "$GITHUB_OUTPUT" + + build: + uses: OpenCHAMI/github-actions/.github/workflows/build-publish-container-goreleaser.yml@v3.5 + secrets: inherit + with: + cgo_enabled: 0 + registry_subject_name: ghcr.io/openchami/metadata-service + is_pr_build: true + pr_number: ${{ inputs.pr_number || github.event.pull_request.number || 0 }} + + rpmbuild: + needs: [config, build] + uses: OpenCHAMI/github-actions/.github/workflows/build-rpm-quadlet.yml@v3.5 + secrets: inherit + with: + artifact-name-unsigned-rpms: ${{ needs.config.outputs.rpm-unsigned }} + + rpmsign: + needs: [config, rpmbuild] + uses: OpenCHAMI/github-actions/.github/workflows/gpg-sign-artifacts.yml@v3.5 + secrets: inherit + with: + artifact-name-unsigned-rpms: ${{ needs.config.outputs.rpm-unsigned }} + artifact-name-signed-rpms: ${{ needs.config.outputs.rpm-signed }} + artifact-name-public-keys: ${{ needs.config.outputs.keys-public }} + + rpmvalidate: + needs: [config, rpmsign] + uses: OpenCHAMI/github-actions/.github/workflows/validate-rpm-quadlet.yml@v3.5 + secrets: inherit + with: + artifact-name-signed-rpms: ${{ needs.config.outputs.rpm-signed }} + expected-files: | + /usr/share/containers/systemd/metadata-data.volume + /usr/share/containers/systemd/metadata-service.container + /usr/share/licenses/metadata-service + /usr/share/licenses/metadata-service/MIT.txt ``` ## Continuous Integration -A future CI workflow will: -- Lint action metadata (actionlint) -- Perform a matrix test invoking each action -- Validate RPM signing round‑trip +- Workflow files are linted via `lint-workflows.yml` (actionlint + zizmor). +- RPM/quadlet output is validated via `validate-rpm-quadlet.yml`. +- TODO: matrix test invoking each action directly. ## Rotation & Revocation -1. Revoke and replace repo subkeys periodically. -2. Update `GPG_SUBKEY_B64` secret. -3. Tag a new release if behavior changes. +Repo cert key and master key rotation/revocation procedures live in +[gpg-signing-manager](https://github.com/OpenCHAMI/gpg-signing-manager). Tag +a new release here if this repo's actions or workflows change as a result. ## Contributing From 6647bc5294664aada26e4fc1ed89c5fd048edfa4 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 13:35:11 -0600 Subject: [PATCH 12/15] prepare for release by swapping out placeholder dev tag for gpg actions Signed-off-by: Sean Tronsen --- .github/workflows/gpg-sign-artifacts.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gpg-sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml index 66822b2..19e02b9 100644 --- a/.github/workflows/gpg-sign-artifacts.yml +++ b/.github/workflows/gpg-sign-artifacts.yml @@ -46,14 +46,14 @@ jobs: fetch-depth: 0 - name: Check for repo key expiry - uses: OpenCHAMI/github-actions/actions/gpg-check-key-expiration@dev-rpm-quadlets + uses: OpenCHAMI/github-actions/actions/gpg-check-key-expiration@v3.5 with: repo-key-armored-b64: ${{ secrets.GPG_REPO_KEY_B64 }} warn-days: '30' - name: Configure GPG release keys id: gpg - uses: OpenCHAMI/github-actions/actions/gpg-configure-release-keys@dev-rpm-quadlets + uses: OpenCHAMI/github-actions/actions/gpg-configure-release-keys@v3.5 with: repo-cert-key-armored-b64: ${{ secrets.GPG_REPO_CERT_KEY_B64 }} master-public-key-asc: ${{ secrets.MASTER_PUBLIC_ASC }} @@ -71,14 +71,14 @@ jobs: - name: Sign rpms id: rpmsign - uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@dev-rpm-quadlets + uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@v3.5 with: resign: true gnupg-home: ${{ steps.gpg.outputs.gnupg-home }} gpg-fingerprint: ${{ steps.gpg.outputs.ephemeral-fingerprint }} - name: Verify trust chain - uses: OpenCHAMI/github-actions/actions/gpg-verify-trust-chain@dev-rpm-quadlets + uses: OpenCHAMI/github-actions/actions/gpg-verify-trust-chain@v3.5 with: master-public-key: ${{ secrets.MASTER_PUBLIC_ASC }} require-master: false From 48f68da85848e1e49e0a1aa21771901703c49562 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Tue, 4 Aug 2026 08:57:38 -0600 Subject: [PATCH 13/15] add missing copyright information to gpg-verify-trust-chain action Signed-off-by: Sean Tronsen --- actions/gpg-verify-trust-chain/action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/actions/gpg-verify-trust-chain/action.yml b/actions/gpg-verify-trust-chain/action.yml index b25ea2a..3f09b90 100644 --- a/actions/gpg-verify-trust-chain/action.yml +++ b/actions/gpg-verify-trust-chain/action.yml @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + name: 'Verify gpg trust chain' description: >- Verifies the release GPG trust chain (master certifies repo key, repo key certifies ephemeral key) and optionally checks RPM signatures. Standalone: installs its own dependencies and runs verify-chain.sh, fetched at runtime from a pinned commit in gpg-signing-manager. From 85b10a53af5aa3a93ea887ade12792bdc91192d6 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Tue, 4 Aug 2026 09:31:43 -0600 Subject: [PATCH 14/15] change gpg-sign-artifacts default to enforce master keys Signed-off-by: Sean Tronsen --- .github/workflows/gpg-sign-artifacts.yml | 2 +- actions/gpg-verify-trust-chain/action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gpg-sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml index 19e02b9..19cc070 100644 --- a/.github/workflows/gpg-sign-artifacts.yml +++ b/.github/workflows/gpg-sign-artifacts.yml @@ -81,7 +81,7 @@ jobs: uses: OpenCHAMI/github-actions/actions/gpg-verify-trust-chain@v3.5 with: master-public-key: ${{ secrets.MASTER_PUBLIC_ASC }} - require-master: false + require-master: true repo-public-key-file: ${{ steps.gpg.outputs.repo-cert-public-key-file }} ephemeral-public-key-file: ${{ steps.gpg.outputs.ephemeral-public-key-file }} rpm-dir: . diff --git a/actions/gpg-verify-trust-chain/action.yml b/actions/gpg-verify-trust-chain/action.yml index 3f09b90..03b33a8 100644 --- a/actions/gpg-verify-trust-chain/action.yml +++ b/actions/gpg-verify-trust-chain/action.yml @@ -30,7 +30,7 @@ inputs: require-master: description: "If 'true', fail instead of skipping when no master key is provided" required: false - default: 'false' + default: 'true' outputs: verified: description: "'true' if the chain was verified, 'skipped' if no master key was provided" From 43788dcec7bf96f7227b2315b527b4fb6f8db460 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Tue, 4 Aug 2026 10:13:23 -0600 Subject: [PATCH 15/15] update rpm validation workflow to support testing multiple rpms Signed-off-by: Sean Tronsen --- .github/workflows/validate-rpm-quadlet.yml | 33 ++++++++++++++++++---- README.md | 18 +++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.github/workflows/validate-rpm-quadlet.yml b/.github/workflows/validate-rpm-quadlet.yml index 962345a..c3fc0fc 100644 --- a/.github/workflows/validate-rpm-quadlet.yml +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -13,20 +13,37 @@ on: description: 'Artifact-name for signed RPM artifacts' default: 'rpms-signed' type: string - expected-files: - description: 'a list of files the RPM is expected to install (newline delimited or multiline yaml string)' + rpms: + description: 'YAML list of RPM specs, each with a `name` glob pattern (e.g. `foo-*.rpm`) and a `files` list' required: true type: string jobs: + parse: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.parse.outputs.matrix }} + steps: + - name: Parse RPM specs + id: parse + env: + RPMS: ${{ inputs.rpms }} + run: echo "matrix=$(yq -o=json -I=0 '.' <<< "$RPMS")" >> "$GITHUB_OUTPUT" + rpmvalidate: + needs: parse + name: validate (${{ matrix.rpm.name }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rpm: ${{ fromJSON(needs.parse.outputs.matrix) }} container: image: rockylinux:9 steps: - name: Install build dependencies - run: dnf install -y -q git rpmlint tar gzip diffutils + run: dnf install -y -q git rpmlint tar gzip diffutils jq - name: Mark workspace as a safe git directory run: git config --global --add safe.directory "$GITHUB_WORKSPACE" @@ -43,10 +60,14 @@ jobs: path: dist/rpms - name: Find Quadlet RPM + env: + RPM_NAME: ${{ matrix.rpm.name }} run: | set -euo pipefail - quadlet_rpm=$(find . -type f -iname "*.rpm" | head -n 1) + # RPM_NAME is a glob (built filenames carry version/release/arch); + # sort makes the pick deterministic if it matches more than one file. + quadlet_rpm=$(find . -type f -iname "${RPM_NAME}" | sort | head -n 1) if [ -z "${quadlet_rpm}" ]; then echo "could not locate quadlet rpm file" exit 1 @@ -58,9 +79,9 @@ jobs: - name: Verify installed file list is exactly what's expected shell: bash env: - EXPECTED_FILES: ${{ inputs.expected-files }} + FILES: ${{ toJSON(matrix.rpm.files) }} run: | set -euo pipefail rpm -qlp "${QUADLET_RPM_PATH}" | sort | grep -v '^$' > /tmp/actual-files.txt - printf '%s\n' "${EXPECTED_FILES}" | sort | grep -v '^$' > /tmp/expected-files.txt + jq -r '.[]' <<< "$FILES" | sort | grep -v '^$' > /tmp/expected-files.txt diff /tmp/expected-files.txt /tmp/actual-files.txt diff --git a/README.md b/README.md index ee82ca8..4b2bebf 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,10 @@ jobs: validate: uses: OpenCHAMI/github-actions/.github/workflows/validate-rpm-quadlet.yml@v3.5 with: - expected-files: | - /etc/containers/systemd/foo.container + rpms: | + - name: foo-*.rpm + files: + - /etc/containers/systemd/foo.container ``` ### release-signed-artifacts (Reusable Workflow) @@ -305,11 +307,13 @@ jobs: secrets: inherit with: artifact-name-signed-rpms: ${{ needs.config.outputs.rpm-signed }} - expected-files: | - /usr/share/containers/systemd/metadata-data.volume - /usr/share/containers/systemd/metadata-service.container - /usr/share/licenses/metadata-service - /usr/share/licenses/metadata-service/MIT.txt + rpms: | + - name: metadata-service-*.rpm + files: + - /usr/share/containers/systemd/metadata-data.volume + - /usr/share/containers/systemd/metadata-service.container + - /usr/share/licenses/metadata-service + - /usr/share/licenses/metadata-service/MIT.txt ``` ## Continuous Integration