From dcf95d95f9d42b2d7fd4620484edd9c96831bbde Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Wed, 29 Jul 2026 17:03:22 -0600 Subject: [PATCH 01/20] rename action:sign-rpm to action:gpg-sign-rpm Signed-off-by: Sean Tronsen --- actions/{sign-rpm => gpg-sign-rpm}/README.md | 0 actions/{sign-rpm => gpg-sign-rpm}/action.yml | 53 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) rename actions/{sign-rpm => gpg-sign-rpm}/README.md (100%) rename actions/{sign-rpm => gpg-sign-rpm}/action.yml (64%) diff --git a/actions/sign-rpm/README.md b/actions/gpg-sign-rpm/README.md similarity index 100% rename from actions/sign-rpm/README.md rename to actions/gpg-sign-rpm/README.md diff --git a/actions/sign-rpm/action.yml b/actions/gpg-sign-rpm/action.yml similarity index 64% rename from actions/sign-rpm/action.yml rename to actions/gpg-sign-rpm/action.yml index 72381d8..1abd026 100644 --- a/actions/sign-rpm/action.yml +++ b/actions/gpg-sign-rpm/action.yml @@ -9,8 +9,8 @@ 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' + rpm-paths: + description: 'Comma-separated paths to the RPM file(s) to sign' required: true gpg-fingerprint: description: 'Fingerprint of the GPG key to use for signing' @@ -41,8 +41,6 @@ runs: dnf install -y rpm-build gnupg fi - - - name: Configure RPM to use GPG key shell: bash env: @@ -68,30 +66,34 @@ runs: 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 + IFS=',' read -ra RPMS <<< "${{ inputs['rpm-paths'] }}" + for f in "${RPMS[@]}"; do + if rpm --checksig "$f" 2>/dev/null | grep -qi 'pgp signature'; then + if [ "${{ inputs.resign }}" = "true" ]; then + rpm --delsign "$f" || true + else + echo "Already signed; skipping $f (set resign=true to force)." + continue + fi fi - 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'] }}" + # 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" \ + "$f" + done - name: Import Signer Public Key for Verification shell: bash + env: + GNUPGHOME: ${{ inputs['gnupg-home'] }} 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 + test -s signer.pub || { echo "Export produced no key for ${{ inputs.gpg-fingerprint }}"; exit 1; } + rpm --import signer.pub || sudo rpm --import signer.pub rm -f signer.pub - name: Verify RPM signature @@ -99,6 +101,11 @@ runs: shell: bash run: | set -euo pipefail - out=$(rpm --checksig "${{ inputs.rpm-path }}") + IFS=',' read -ra RPMS <<< "${{ inputs['rpm-paths'] }}" + out=$(rpm --checksig "${RPMS[@]}") echo "$out" - echo "result=$out" >> "$GITHUB_OUTPUT" + { + echo "result<> "$GITHUB_OUTPUT" From cdf71fe5a611b5d7057f8b6d7c9ae7a51b92673f Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Wed, 29 Jul 2026 17:04:27 -0600 Subject: [PATCH 02/20] add action:gpg-check-key-expiration based on gpg-signing-manager Signed-off-by: Sean Tronsen --- actions/gpg-check-key-expiration/action.yml | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 actions/gpg-check-key-expiration/action.yml diff --git a/actions/gpg-check-key-expiration/action.yml b/actions/gpg-check-key-expiration/action.yml new file mode 100644 index 0000000..70a029a --- /dev/null +++ b/actions/gpg-check-key-expiration/action.yml @@ -0,0 +1,92 @@ +# 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 + + SCRIPT_PATH="$(cd "$GITHUB_ACTION_PATH/../.." && pwd)/scripts/check-key-expiry.sh" + [[ -x "$SCRIPT_PATH" || -f "$SCRIPT_PATH" ]] \ + || { echo "::error::check-key-expiry.sh not found at $SCRIPT_PATH"; exit 1; } + bash "$SCRIPT_PATH" \ + --gnupghome "$GNUPGHOME" \ + --threshold-days "$WARN_DAYS" \ + --github-annotations From 434ace2d4e3a2f17d60b614c14653a7601f1fcef Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Wed, 29 Jul 2026 17:06:34 -0600 Subject: [PATCH 03/20] add action:gpg-configure-release-keys to simplify downstream keys Signed-off-by: Sean Tronsen --- actions/gpg-configure-release-keys/action.yml | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 actions/gpg-configure-release-keys/action.yml diff --git a/actions/gpg-configure-release-keys/action.yml b/actions/gpg-configure-release-keys/action.yml new file mode 100644 index 0000000..d7fcf32 --- /dev/null +++ b/actions/gpg-configure-release-keys/action.yml @@ -0,0 +1,198 @@ +# 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" + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends gnupg + + - 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 }}" + + 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" + + - id: export + shell: bash + run: | + set -euo pipefail + + gpg --armor --export "$EPHEMERAL_FPR" > "ephemeral.pub.asc" + gpg --armor --export "${REPO_CERT_FPR}!" > "repo-cert.pub.asc" + echo "${{ inputs.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 a383cce04dee188962e35a2ba116bef8701dc580 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Wed, 29 Jul 2026 17:07:57 -0600 Subject: [PATCH 04/20] add action:gpg-verify-trust-chain/action.yml to validate ephemeral key certification chain Signed-off-by: Sean Tronsen --- actions/gpg-verify-trust-chain/action.yml | 124 ++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 actions/gpg-verify-trust-chain/action.yml diff --git a/actions/gpg-verify-trust-chain/action.yml b/actions/gpg-verify-trust-chain/action.yml new file mode 100644 index 0000000..1a7d5bb --- /dev/null +++ b/actions/gpg-verify-trust-chain/action.yml @@ -0,0 +1,124 @@ +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 the verify-chain.sh vendored in this repository. +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 vendored verifier --------------------------------------- + # github.action_path = /actions/verify-trust-chain, script lives + # at /scripts/verify-chain.sh. Pinned implicitly to the same ref + # the action itself was resolved at -- no runtime curl from main. + # --- Fetch the verifier (pinned ref, not main) ------------------------ + script="$workdir/verify-chain.sh" + curl -fsSL --retry 3 -o "$script" \ + "https://raw.githubusercontent.com/OpenCHAMI/gpg-signing-manager/main/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 b43bc3533b6a5287926e1d8c071441ac9fab5410 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Wed, 29 Jul 2026 17:14:48 -0600 Subject: [PATCH 05/20] add generic reusable workflow for gpg signing artifacts before release Signed-off-by: Sean Tronsen --- .github/workflows/sign-artifacts.yml | 105 +++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/sign-artifacts.yml diff --git a/.github/workflows/sign-artifacts.yml b/.github/workflows/sign-artifacts.yml new file mode 100644 index 0000000..804b6e9 --- /dev/null +++ b/.github/workflows/sign-artifacts.yml @@ -0,0 +1,105 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# +# SPDX-License-Identifier: MIT +# In the future, to ensure consistent release behaviors and proper artifact +# signing, all release artifacts should travel through this workflow. We'll +# update it as the need arises to ensure artifacts of all types (distro +# packages, source tarballs, etc.) are properly signed. +name: Sign and release artifacts +run-name: Create signed artifacts for ${{ github.ref }} +on: + workflow_call: + inputs: + rpms: + description: 'Artifact-name glob matching unsigned RPM artifacts' + type: string + required: true + 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: 'rpms-signed' + required: false +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@a383cce04dee188962e35a2ba116bef8701dc580 + 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@a383cce04dee188962e35a2ba116bef8701dc580 + 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: + pattern: ${{ inputs.rpms }} + path: dist + merge-multiple: true + + - name: Sign rpms + id: rpmsign + uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@a383cce04dee188962e35a2ba116bef8701dc580 + with: + resign: true + rpm-paths: 'dist/**/*.rpm' + 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@a383cce04dee188962e35a2ba116bef8701dc580 + 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 dist -name '*.rpm') || true + + - name: Upload signed RPMs + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.artifact-name-signed-rpms }} + path: 'dist/**/*.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 From 0fc17691920c2eed011752c22c978bba3ec958fe Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Wed, 29 Jul 2026 18:33:20 -0600 Subject: [PATCH 06/20] wip: rename signing workflow and apply band-aids Signed-off-by: Sean Tronsen --- ...gn-artifacts.yml => gpg-sign-artifacts.yml} | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) rename .github/workflows/{sign-artifacts.yml => gpg-sign-artifacts.yml} (90%) diff --git a/.github/workflows/sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml similarity index 90% rename from .github/workflows/sign-artifacts.yml rename to .github/workflows/gpg-sign-artifacts.yml index 804b6e9..48230b8 100644 --- a/.github/workflows/sign-artifacts.yml +++ b/.github/workflows/gpg-sign-artifacts.yml @@ -5,13 +5,14 @@ # signing, all release artifacts should travel through this workflow. We'll # update it as the need arises to ensure artifacts of all types (distro # packages, source tarballs, etc.) are properly signed. -name: Sign and release artifacts +name: GPG Sign artifacts run-name: Create signed artifacts for ${{ github.ref }} on: workflow_call: inputs: - rpms: - description: 'Artifact-name glob matching unsigned RPM artifacts' + artifact-name-unsigned-rpms: + description: 'Artifact-name for unsigned RPM artifacts' + default: 'rpms-unsigned' type: string required: true artifact-name-signed-rpms: @@ -22,7 +23,7 @@ on: artifact-name-public-keys: description: 'Name for the public key composite artifact' type: string - default: 'rpms-signed' + default: 'public-keys' required: false jobs: artifacts-sign: @@ -65,16 +66,15 @@ jobs: - name: Download RPM artifacts requested for release uses: actions/download-artifact@v8 with: - pattern: ${{ inputs.rpms }} + name: ${{ inputs.artifact-name-unsigned-rpms }} path: dist - merge-multiple: true - name: Sign rpms id: rpmsign uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@a383cce04dee188962e35a2ba116bef8701dc580 with: resign: true - rpm-paths: 'dist/**/*.rpm' + rpm-paths: '**/*.rpm' gnupg-home: ${{ steps.gpg.outputs.gnupg-home }} gpg-fingerprint: ${{ steps.gpg.outputs.ephemeral-fingerprint }} @@ -88,13 +88,13 @@ jobs: rpm-dir: . - name: rpmlint - run: rpmlint $(find dist -name '*.rpm') || true + run: rpmlint $(find . -name '*.rpm') || true - name: Upload signed RPMs uses: actions/upload-artifact@v7 with: name: ${{ inputs.artifact-name-signed-rpms }} - path: 'dist/**/*.rpm' + path: '**/*.rpm' overwrite: true - name: Upload public signing keys From b6094de8334d79df5b3deccb1202a3be0341558b Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Wed, 29 Jul 2026 18:33:55 -0600 Subject: [PATCH 07/20] add workflow for generating a release with signed artifacts Signed-off-by: Sean Tronsen --- .../workflows/release-signed-artifacts.yml | 91 +++++++++++++++++++ 1 file changed, 91 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..0c36c07 --- /dev/null +++ b/.github/workflows/release-signed-artifacts.yml @@ -0,0 +1,91 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +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 3767c83b8873952dbcb72cd0759f8d1ca84afec7 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 10:15:51 -0600 Subject: [PATCH 08/20] add small reusable workflow for building rpm quadlets Signed-off-by: Sean Tronsen --- .github/workflows/build-rpm-quadlet.yml | 39 +++++++++++++++++++++++++ 1 file changed, 39 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..0a0e7a7 --- /dev/null +++ b/.github/workflows/build-rpm-quadlet.yml @@ -0,0 +1,39 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +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 + required: true +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 81bf04e059c08ab2d1917e51dc01c29b03b7dd5f Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 10:16:21 -0600 Subject: [PATCH 09/20] add abstracted workflow for using goreleaser to build/publish containers Signed-off-by: Sean Tronsen --- .../build-publish-container-goreleaser.yml | 93 +++++++++++++++++++ 1 file changed, 93 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..91395d7 --- /dev/null +++ b/.github/workflows/build-publish-container-goreleaser.yml @@ -0,0 +1,93 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +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: | + echo "GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" >> $GITHUB_ENV + echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV + echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV + echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV + echo "CGO_ENABLED=${{ inputs.cgo_enabled }}" >> $GITHUB_ENV + 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: | + echo "const fs = require('fs');" > process.js + echo 'const artifacts = ${{ steps.goreleaser.outputs.artifacts }}' >> process.js + echo "const firstNonNullDigest = artifacts.find(artifact => artifact.extra && artifact.extra.Digest != null)?.extra.Digest;" >> process.js + echo "console.log(firstNonNullDigest);" >> process.js + echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js + node process.js + 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 4b838a6e88e4813b97ba6307fb4949af0250f959 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 11:19:51 -0600 Subject: [PATCH 10/20] add abstract workflow for RPM validation Signed-off-by: Sean Tronsen --- .github/workflows/validate-rpm-quadlet.yml | 64 ++++++++++++++++++++++ 1 file changed, 64 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..1989b4a --- /dev/null +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -0,0 +1,64 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +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' + required: true + 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 + + - 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 + env: + EXPECTED_FILES: ${{ inputs.expected-files }} + run: | + set -euo pipefail + + rpm -qlp "${QUADLET_RPM_PATH}" | sort | tee /tmp/actual-files.txt + printf "%s" "${EXPECTED_FILES}" | sort | tee /tmp/expected-files.txt + diff /tmp/expected-files.txt /tmp/actual-files.txt From d83604fe181901bb49445f33f1231f653e6e046d Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 11:23:34 -0600 Subject: [PATCH 11/20] wip: spotfix check expiry script call Signed-off-by: Sean Tronsen --- actions/gpg-check-key-expiration/action.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/actions/gpg-check-key-expiration/action.yml b/actions/gpg-check-key-expiration/action.yml index 70a029a..552b83d 100644 --- a/actions/gpg-check-key-expiration/action.yml +++ b/actions/gpg-check-key-expiration/action.yml @@ -83,10 +83,11 @@ runs: exit 1 fi - SCRIPT_PATH="$(cd "$GITHUB_ACTION_PATH/../.." && pwd)/scripts/check-key-expiry.sh" - [[ -x "$SCRIPT_PATH" || -f "$SCRIPT_PATH" ]] \ - || { echo "::error::check-key-expiry.sh not found at $SCRIPT_PATH"; exit 1; } - bash "$SCRIPT_PATH" \ + script="$workdir/check-key-expiry.sh" + curl -fsSL --retry 3 -o "$script" \ + "https://raw.githubusercontent.com/OpenCHAMI/gpg-signing-manager/main/scripts/check-key-expiry.sh" + + bash "$script" \ --gnupghome "$GNUPGHOME" \ --threshold-days "$WARN_DAYS" \ --github-annotations From ae3478abdb6d4d06e078ac6e2c5857b86ab949d5 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 11:45:26 -0600 Subject: [PATCH 12/20] wip: adjust action versions 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 48230b8..7e62e99 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@a383cce04dee188962e35a2ba116bef8701dc580 + 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@a383cce04dee188962e35a2ba116bef8701dc580 + 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 }} @@ -71,7 +71,7 @@ jobs: - name: Sign rpms id: rpmsign - uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@a383cce04dee188962e35a2ba116bef8701dc580 + uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@dev-rpm-quadlets with: resign: true rpm-paths: '**/*.rpm' @@ -79,7 +79,7 @@ jobs: gpg-fingerprint: ${{ steps.gpg.outputs.ephemeral-fingerprint }} - name: Verify trust chain - uses: OpenCHAMI/github-actions/actions/gpg-verify-trust-chain@a383cce04dee188962e35a2ba116bef8701dc580 + uses: OpenCHAMI/github-actions/actions/gpg-verify-trust-chain@dev-rpm-quadlets with: master-public-key: ${{ secrets.MASTER_PUBLIC_ASC }} require-master: false From bd3786b130af6ce37406fc98e8aa99e679242140 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 11:54:48 -0600 Subject: [PATCH 13/20] wip: spotfix unbound var Signed-off-by: Sean Tronsen --- actions/gpg-check-key-expiration/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actions/gpg-check-key-expiration/action.yml b/actions/gpg-check-key-expiration/action.yml index 552b83d..cc2a403 100644 --- a/actions/gpg-check-key-expiration/action.yml +++ b/actions/gpg-check-key-expiration/action.yml @@ -83,6 +83,8 @@ runs: 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/main/scripts/check-key-expiry.sh" From 17edf256d19c74c32a3968d2a57f2d75a9e72450 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 11:57:35 -0600 Subject: [PATCH 14/20] wip: add ignore for blank lines in rpm validation workflow Signed-off-by: Sean Tronsen --- .github/workflows/validate-rpm-quadlet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-rpm-quadlet.yml b/.github/workflows/validate-rpm-quadlet.yml index 1989b4a..04d27e4 100644 --- a/.github/workflows/validate-rpm-quadlet.yml +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -61,4 +61,4 @@ jobs: rpm -qlp "${QUADLET_RPM_PATH}" | sort | tee /tmp/actual-files.txt printf "%s" "${EXPECTED_FILES}" | sort | tee /tmp/expected-files.txt - diff /tmp/expected-files.txt /tmp/actual-files.txt + diff -B /tmp/expected-files.txt /tmp/actual-files.txt From 635cf3c1d596ee86509f1c63777116c131f93752 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 12:01:19 -0600 Subject: [PATCH 15/20] wip: fix 'sudo: command not found' in keygen Signed-off-by: Sean Tronsen --- actions/gpg-configure-release-keys/action.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/actions/gpg-configure-release-keys/action.yml b/actions/gpg-configure-release-keys/action.yml index d7fcf32..16e4e4f 100644 --- a/actions/gpg-configure-release-keys/action.yml +++ b/actions/gpg-configure-release-keys/action.yml @@ -76,12 +76,23 @@ runs: shell: bash run: | set -euo pipefail + GNUPGHOME="$(mktemp -d)" chmod 700 "$GNUPGHOME" echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" echo "gnupghome=$GNUPGHOME" >> "$GITHUB_OUTPUT" - sudo apt-get update -y - sudo apt-get install -y --no-install-recommends gnupg + + 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 From e3f509d86f20c973aeff8f52cdc19d31524d9b99 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 12:09:01 -0600 Subject: [PATCH 16/20] update gpg sign to include signing deps for rhel envs Signed-off-by: Sean Tronsen --- actions/gpg-sign-rpm/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/gpg-sign-rpm/action.yml b/actions/gpg-sign-rpm/action.yml index 1abd026..ed9cf6c 100644 --- a/actions/gpg-sign-rpm/action.yml +++ b/actions/gpg-sign-rpm/action.yml @@ -38,7 +38,7 @@ 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 From ffa0a7e3cda9a9c5ed0513481e7fa31abef140d2 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 12:24:26 -0600 Subject: [PATCH 17/20] wip: remove rpm-paths input var in favor of generalized behaviors Signed-off-by: Sean Tronsen --- .github/workflows/gpg-sign-artifacts.yml | 1 - actions/gpg-sign-rpm/action.yml | 29 ++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gpg-sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml index 7e62e99..7e0736e 100644 --- a/.github/workflows/gpg-sign-artifacts.yml +++ b/.github/workflows/gpg-sign-artifacts.yml @@ -74,7 +74,6 @@ jobs: uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@dev-rpm-quadlets with: resign: true - rpm-paths: '**/*.rpm' gnupg-home: ${{ steps.gpg.outputs.gnupg-home }} gpg-fingerprint: ${{ steps.gpg.outputs.ephemeral-fingerprint }} diff --git a/actions/gpg-sign-rpm/action.yml b/actions/gpg-sign-rpm/action.yml index ed9cf6c..b53b8c8 100644 --- a/actions/gpg-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-paths: - description: 'Comma-separated paths to the RPM file(s) to sign' - required: true gpg-fingerprint: description: 'Fingerprint of the GPG key to use for signing' required: true @@ -61,15 +58,18 @@ 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; } - IFS=',' read -ra RPMS <<< "${{ inputs['rpm-paths'] }}" - for f in "${RPMS[@]}"; do + found=0 + while IFS= read -r f; do + found=1 if rpm --checksig "$f" 2>/dev/null | grep -qi 'pgp signature'; then - if [ "${{ inputs.resign }}" = "true" ]; then + if [ "$RESIGN" = "true" ]; then rpm --delsign "$f" || true else echo "Already signed; skipping $f (set resign=true to force)." @@ -79,11 +79,14 @@ runs: # Use rpmsign (more reliable) with explicit defines rpmsign --addsign \ --define "_signature gpg" \ - --define "_gpg_name ${{ inputs['gpg-fingerprint'] }}" \ + --define "_gpg_name $GPG_FINGERPRINT" \ --define "__gpg $(command -v gpg)" \ --define "_gpg_digest_algo sha256" \ "$f" - done + done < <(find . -type f -name '*.rpm') + if [ "$found" -eq 0 ]; then + echo "::error::no RPMs found under $(pwd)"; exit 1 + fi - name: Import Signer Public Key for Verification shell: bash @@ -101,7 +104,15 @@ runs: shell: bash run: | set -euo pipefail - IFS=',' read -ra RPMS <<< "${{ inputs['rpm-paths'] }}" + + 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" { From e125b357614ddee3d40c5c6518b734349af98e75 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 12:28:56 -0600 Subject: [PATCH 18/20] wip: fix diff in rpm validation workflow Signed-off-by: Sean Tronsen --- .github/workflows/validate-rpm-quadlet.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-rpm-quadlet.yml b/.github/workflows/validate-rpm-quadlet.yml index 04d27e4..765ce14 100644 --- a/.github/workflows/validate-rpm-quadlet.yml +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Install build dependencies - run: dnf install -y -q git rpmlint tar gzip + 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" @@ -60,5 +60,5 @@ jobs: set -euo pipefail rpm -qlp "${QUADLET_RPM_PATH}" | sort | tee /tmp/actual-files.txt - printf "%s" "${EXPECTED_FILES}" | sort | tee /tmp/expected-files.txt + printf "%s" "${EXPECTED_FILES}\n" | sort | tee /tmp/expected-files.txt diff -B /tmp/expected-files.txt /tmp/actual-files.txt From 22d9008e9f3f2886e492431585e2bd52e3b0fc9d Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Thu, 30 Jul 2026 12:43:21 -0600 Subject: [PATCH 19/20] wip: fix diff checker in rpm validation workflow Signed-off-by: Sean Tronsen --- .github/workflows/validate-rpm-quadlet.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validate-rpm-quadlet.yml b/.github/workflows/validate-rpm-quadlet.yml index 765ce14..03c37c6 100644 --- a/.github/workflows/validate-rpm-quadlet.yml +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -54,11 +54,11 @@ jobs: 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 | tee /tmp/actual-files.txt - printf "%s" "${EXPECTED_FILES}\n" | sort | tee /tmp/expected-files.txt - diff -B /tmp/expected-files.txt /tmp/actual-files.txt + 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 48c05b8bc4988a2fdbfb792e78d1f461fd979674 Mon Sep 17 00:00:00 2001 From: Sean Tronsen Date: Mon, 3 Aug 2026 08:41:17 -0600 Subject: [PATCH 20/20] wip: satisfy the linter Signed-off-by: Sean Tronsen --- .../build-publish-container-goreleaser.yml | 35 +++++++++++-------- .github/workflows/build-rpm-quadlet.yml | 1 - .github/workflows/gpg-sign-artifacts.yml | 5 +-- .github/workflows/validate-rpm-quadlet.yml | 1 - 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-publish-container-goreleaser.yml b/.github/workflows/build-publish-container-goreleaser.yml index 91395d7..c66556d 100644 --- a/.github/workflows/build-publish-container-goreleaser.yml +++ b/.github/workflows/build-publish-container-goreleaser.yml @@ -50,19 +50,25 @@ jobs: # Set environment variables required by GoReleaser - name: Set build environment variables run: | - echo "GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" >> $GITHUB_ENV - echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV - echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV - echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV - echo "CGO_ENABLED=${{ inputs.cgo_enabled }}" >> $GITHUB_ENV - echo "IS_PR_BUILD=${{ inputs.is_pr_build }}" >> $GITHUB_ENV + { + 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: @@ -74,13 +80,14 @@ jobs: - name: Process goreleaser output id: process_goreleaser_output run: | - echo "const fs = require('fs');" > process.js - echo 'const artifacts = ${{ steps.goreleaser.outputs.artifacts }}' >> process.js - echo "const firstNonNullDigest = artifacts.find(artifact => artifact.extra && artifact.extra.Digest != null)?.extra.Digest;" >> process.js - echo "console.log(firstNonNullDigest);" >> process.js - echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js - node process.js - echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT + 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: diff --git a/.github/workflows/build-rpm-quadlet.yml b/.github/workflows/build-rpm-quadlet.yml index 0a0e7a7..b3aaa3b 100644 --- a/.github/workflows/build-rpm-quadlet.yml +++ b/.github/workflows/build-rpm-quadlet.yml @@ -10,7 +10,6 @@ on: description: 'Artifact-name for unsigned RPM artifacts' default: 'rpms-unsigned' type: string - required: true jobs: rpmbuild: runs-on: ubuntu-latest diff --git a/.github/workflows/gpg-sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml index 7e0736e..714e481 100644 --- a/.github/workflows/gpg-sign-artifacts.yml +++ b/.github/workflows/gpg-sign-artifacts.yml @@ -14,17 +14,14 @@ on: description: 'Artifact-name for unsigned RPM artifacts' default: 'rpms-unsigned' type: string - required: true 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-sign: runs-on: ubuntu-latest @@ -87,7 +84,7 @@ jobs: rpm-dir: . - name: rpmlint - run: rpmlint $(find . -name '*.rpm') || true + run: rpmlint "$(find . -name '*.rpm')" || true - name: Upload signed RPMs uses: actions/upload-artifact@v7 diff --git a/.github/workflows/validate-rpm-quadlet.yml b/.github/workflows/validate-rpm-quadlet.yml index 03c37c6..3c3ed12 100644 --- a/.github/workflows/validate-rpm-quadlet.yml +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -9,7 +9,6 @@ on: artifact-name-signed-rpms: description: 'Artifact-name for signed RPM artifacts' default: 'rpms-signed' - required: true type: string expected-files: description: 'a list of files the RPM is expected to install (newline delimited or multiline yaml string)'