diff --git a/.github/workflows/build-check-goreleaser.yml b/.github/workflows/build-check-goreleaser.yml new file mode 100644 index 0000000..375df20 --- /dev/null +++ b/.github/workflows/build-check-goreleaser.yml @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: build-check-goreleaser + +# Reusable workflow that proves the caller's GoReleaser config builds +# everything (binaries, archives, packages, images) without publishing +# anything: `goreleaser release --snapshot`. Read-only and secret-free, so it +# is safe for fork PRs. Use build-release-goreleaser to publish. +# +# Sets the same environment as build-release-goreleaser (GIT_STATE, +# BUILD_HOST, GO_VERSION, BUILD_USER, IS_PR_BUILD=true, GPG_KEY_PATH empty) so +# the same .goreleaser.yaml works in both. + +on: + workflow_call: + inputs: + go-version: + description: 'Go version to use (e.g. "1.26.7", "stable"). Mutually exclusive with go-version-file.' + required: false + type: string + go-version-file: + description: 'Path to a go.mod or .go-version file containing the Go version. Mutually exclusive with go-version.' + required: false + type: string + goreleaser-version: + description: 'GoReleaser version to run' + required: false + type: string + default: 'v2.18.2' + build-deps: + description: 'Space-separated apt packages to install before building (e.g. "gcc-aarch64-linux-gnu libc6-dev-arm64-cross")' + required: false + type: string + default: '' + cgo-enabled: + description: 'Value for CGO_ENABLED' + required: false + type: number + default: 0 + skip: + description: 'Comma-separated GoReleaser pipes to skip (e.g. "docker")' + required: false + type: string + default: '' + dist-artifact-name: + description: 'Artifact name for the packages, archives, and checksums in dist/ (e.g. for validate-packages); empty skips the upload' + required: false + type: string + default: 'goreleaser-dist' + +permissions: + contents: read # baseline for checkout + +jobs: + build-check-goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - name: Install build dependencies + if: ${{ inputs.build-deps != '' }} + env: + BUILD_DEPS: ${{ inputs.build-deps }} + run: | + sudo apt-get update + # shellcheck disable=SC2086 # intentional word splitting + sudo apt-get install -y --no-install-recommends ${BUILD_DEPS} + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: ${{ inputs.go-version || (inputs.go-version-file == '' && 'stable' || '') }} + go-version-file: ${{ inputs.go-version-file || '' }} + cache: false # published artifacts must not come from a poisonable cache + - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Set build environment + id: build-env + 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)" + } >> "$GITHUB_OUTPUT" + - name: Assemble GoReleaser arguments + id: args + env: + SKIP: ${{ inputs.skip }} + run: | + args="release --snapshot --clean" + [[ -n "$SKIP" ]] && args+=" --skip=${SKIP}" + echo "GoReleaser arguments: ${args}" + echo "args=${args}" >> "$GITHUB_OUTPUT" + - name: Run GoReleaser (snapshot) + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + env: + CGO_ENABLED: ${{ inputs.cgo-enabled }} + IS_PR_BUILD: 'true' + GIT_STATE: ${{ steps.build-env.outputs.git-state }} + BUILD_HOST: ${{ steps.build-env.outputs.build-host }} + GO_VERSION: ${{ steps.build-env.outputs.go-version }} + BUILD_USER: ${{ steps.build-env.outputs.build-user }} + GPG_KEY_PATH: '' + with: + distribution: goreleaser + version: ${{ inputs.goreleaser-version }} + args: ${{ steps.args.outputs.args }} + + - name: Upload dist + if: ${{ inputs.dist-artifact-name != '' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.dist-artifact-name }} + path: | + dist/*.rpm + dist/*.deb + dist/*.tar.gz + dist/*.zip + dist/*checksums.txt + if-no-files-found: warn diff --git a/.github/workflows/build-publish-container-goreleaser.yml b/.github/workflows/build-publish-container-goreleaser.yml index b254612..34a25f2 100644 --- a/.github/workflows/build-publish-container-goreleaser.yml +++ b/.github/workflows/build-publish-container-goreleaser.yml @@ -1,6 +1,14 @@ # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT # +# ############################################################################ +# # DEPRECATED WORKFLOW -- DO NOT USE IN NEW CALLERS +# # +# # build-publish-container-goreleaser is replaced by build-release-goreleaser.yml (build-only: build-check-goreleaser.yml). +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ +# # Reusable workflow: builds and publishes a container image via GoReleaser, # with multi-arch builds, build provenance attestation, and PR snapshot # support. @@ -52,6 +60,9 @@ jobs: container-build-publish: runs-on: ubuntu-latest steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::build-publish-container-goreleaser.yml is deprecated; migrate to build-release-goreleaser.yml (build-only: build-check-goreleaser.yml)." - name: Checkout uses: actions/checkout@v6.0.2 with: diff --git a/.github/workflows/build-release-goreleaser.yml b/.github/workflows/build-release-goreleaser.yml new file mode 100644 index 0000000..f8a7492 --- /dev/null +++ b/.github/workflows/build-release-goreleaser.yml @@ -0,0 +1,339 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: build-release-goreleaser + +# Reusable workflow that runs the caller's GoReleaser config in one of two +# modes, chosen from the triggering event (no manual override; any other +# trigger fails fast): +# +# - PR (`pull_request` event): tags HEAD locally as `pr-` and pushes +# the resulting images (e.g. `:pr-12`). No GitHub release, no packages +# (nfpm is skipped; use build-check-goreleaser to validate PR packages), no +# signing, no attestation. Fork PRs build but publish nothing. +# - Release (`v*` tag push): semver images, (optionally signed) packages, a +# GitHub release (draft by default) with GitHub-generated notes, and build +# provenance attestations for the binaries and every published image. +# +# Contract with the caller's .goreleaser.yaml: these environment variables +# are always set (empty when unused), so templates can reference them: +# GIT_STATE, BUILD_HOST, GO_VERSION, BUILD_USER, IS_PR_BUILD, GPG_KEY_PATH +# Packages are signed when `key_file: "{{ .Env.GPG_KEY_PATH }}"` is set in +# the nfpm config and the `gpg-key` secret is passed (release mode only). The +# matching public key is derived from it, attached to the release as +# gpg-public-key.asc, and included in the dist artifact, where +# validate-packages picks it up to verify signatures. + +on: + workflow_call: + inputs: + go-version: + description: 'Go version to use (e.g. "1.26.7", "stable"). Mutually exclusive with go-version-file.' + required: false + type: string + go-version-file: + description: 'Path to a go.mod or .go-version file containing the Go version. Mutually exclusive with go-version.' + required: false + type: string + goreleaser-version: + description: 'GoReleaser version to run' + required: false + type: string + default: 'v2.18.2' + build-deps: + description: 'Space-separated apt packages to install before building (e.g. "gcc-aarch64-linux-gnu libc6-dev-arm64-cross")' + required: false + type: string + default: '' + cgo-enabled: + description: 'Value for CGO_ENABLED' + required: false + type: number + default: 0 + release-draft: + description: 'Create the GitHub release as a draft (publish it later with publish-release)' + required: false + type: boolean + default: true + generate-release-notes: + description: "Pass GitHub's generated release notes for the tag to GoReleaser (`--release-notes`)" + required: false + type: boolean + default: true + attestation-subject-path: + description: 'Glob of release files to attest' + required: false + type: string + default: 'dist/**' + goreleaser-args: + description: 'Extra arguments appended to `goreleaser release`' + required: false + type: string + default: '' + dist-artifact-name: + description: 'Artifact name for the packages, archives, and checksums in dist/ (e.g. for validate-packages)' + required: false + type: string + default: 'goreleaser-dist' + secrets: + gpg-key: + description: 'ASCII-armored (or base64-encoded armored) private key for package signing; release mode only' + required: false + gpg-key-passphrase: + description: 'Passphrase for gpg-key (handed to nfpm as NFPM_PASSPHRASE)' + required: false + outputs: + version: + description: 'Version GoReleaser built' + value: ${{ jobs.build-release-goreleaser.outputs.version }} + digest: + description: 'Image digest (manifest when present), empty if no image was published' + value: ${{ jobs.build-release-goreleaser.outputs.digest }} + +permissions: + contents: write # release creation, uploading assets + packages: write # image push, container provenance + id-token: write # Sigstore signing (attest-build-provenance) + attestations: write # build provenance attestations + +jobs: + build-release-goreleaser: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.metadata.outputs.version }} + digest: ${{ steps.metadata.outputs.digest }} + images: ${{ steps.metadata.outputs.images }} + env: + IS_PR_BUILD: ${{ github.event_name == 'pull_request' }} + # Fork PRs get a read-only token: build, but publish nothing. + SKIP_PUBLISH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} + steps: + - name: Check trigger + if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') }} + env: + EVENT: ${{ github.event_name }} + REF: ${{ github.ref }} + run: | + echo "::error::build-release-goreleaser runs on pull_request (PR images) or a v* tag push (release); got ${EVENT} on ${REF}" + exit 1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - name: Install build dependencies + if: ${{ inputs.build-deps != '' }} + env: + BUILD_DEPS: ${{ inputs.build-deps }} + run: | + sudo apt-get update + # shellcheck disable=SC2086 # intentional word splitting + sudo apt-get install -y --no-install-recommends ${BUILD_DEPS} + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: ${{ inputs.go-version || (inputs.go-version-file == '' && 'stable' || '') }} + go-version-file: ${{ inputs.go-version-file || '' }} + cache: false # published artifacts must not come from a poisonable cache + - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - name: Warn on fork PR + if: ${{ env.SKIP_PUBLISH == 'true' }} + run: echo "::warning title=Fork PR - publish skipped::Images are built but not pushed for fork PRs (read-only token)." + - name: Log in to GHCR + if: ${{ env.SKIP_PUBLISH == 'false' }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set build environment + id: build-env + 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)" + } >> "$GITHUB_OUTPUT" + - name: Create PR tag + if: ${{ env.IS_PR_BUILD == 'true' }} + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -f -a "pr-${PR_NUMBER}" -m "PR build" + - name: Generate release notes + if: ${{ env.IS_PR_BUILD == 'false' && inputs.generate-release-notes }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -F tag_name="${TAG}" --jq .body > "${RUNNER_TEMP}/release-notes.md" + - name: Prepare GPG key + id: signing + if: ${{ env.IS_PR_BUILD == 'false' }} + env: + GPG_KEY: ${{ secrets.gpg-key }} + run: | + if [[ -z "$GPG_KEY" ]]; then + echo "No gpg-key secret; packages will not be signed." + echo "key-path=" >> "$GITHUB_OUTPUT" + echo "public-key-path=" >> "$GITHUB_OUTPUT" + exit 0 + fi + key="${RUNNER_TEMP}/gpg-key.asc" + umask 077 + if [[ "$GPG_KEY" == *"-----BEGIN PGP PRIVATE KEY BLOCK-----"* ]]; then + printf '%s\n' "$GPG_KEY" > "$key" + else + printf '%s' "$GPG_KEY" | base64 -d > "$key" + fi + # Derive the public key for the release and for signature checks. + # Importing a secret key doesn't need its passphrase. + pub="${RUNNER_TEMP}/gpg-public-key.asc" + gnupg=$(mktemp -d) + gpg --homedir "$gnupg" --batch --quiet --import "$key" + gpg --homedir "$gnupg" --batch --armor --export > "$pub" + rm -rf "$gnupg" + test -s "$pub" + { + echo "key-path=${key}" + echo "public-key-path=${pub}" + } >> "$GITHUB_OUTPUT" + - name: Assemble GoReleaser arguments + id: args + env: + RELEASE_DRAFT: ${{ inputs.release-draft }} + GENERATE_NOTES: ${{ inputs.generate-release-notes }} + EXTRA_ARGS: ${{ inputs.goreleaser-args }} + run: | + args=(release --clean) + skip=() + if [[ "$IS_PR_BUILD" == true ]]; then + # nfpm: package versions can't be derived from a `pr-` tag (deb + # requires a leading digit). Validate PR packages with + # build-check-goreleaser (snapshot) instead. + skip+=(archive nfpm) + if [[ "$SKIP_PUBLISH" == true ]]; then + # dockers_v2 builds images in the publish phase, so skipping + # publish skips them. Snapshot builds them locally instead. + args+=(--snapshot) + else + skip+=(announce validate) + fi + else + [[ "$RELEASE_DRAFT" == true ]] && args+=(--draft) + [[ "$GENERATE_NOTES" == true ]] && args+=(--release-notes "${RUNNER_TEMP}/release-notes.md") + fi + [[ "$SKIP_PUBLISH" == true ]] && skip+=(publish) + (( ${#skip[@]} )) && args+=("--skip=$(IFS=,; echo "${skip[*]}")") + [[ -n "$EXTRA_ARGS" ]] && args+=("$EXTRA_ARGS") + echo "GoReleaser arguments: ${args[*]}" + echo "args=${args[*]}" >> "$GITHUB_OUTPUT" + - name: Run GoReleaser + id: goreleaser + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + env: + GITHUB_TOKEN: ${{ github.token }} + CGO_ENABLED: ${{ inputs.cgo-enabled }} + GIT_STATE: ${{ steps.build-env.outputs.git-state }} + BUILD_HOST: ${{ steps.build-env.outputs.build-host }} + GO_VERSION: ${{ steps.build-env.outputs.go-version }} + BUILD_USER: ${{ steps.build-env.outputs.build-user }} + GPG_KEY_PATH: ${{ steps.signing.outputs.key-path }} + # GPG key passphrase. The name is fixed by nfpm, which only reads the + # passphrase from NFPM_[ID_][FORMAT_]PASSPHRASE. + NFPM_PASSPHRASE: ${{ github.event_name != 'pull_request' && secrets.gpg-key-passphrase || '' }} + with: + distribution: goreleaser + version: ${{ inputs.goreleaser-version }} + args: ${{ steps.args.outputs.args }} + - name: Remove GPG key + if: ${{ always() && steps.signing.outputs.key-path != '' }} + env: + KEY_PATH: ${{ steps.signing.outputs.key-path }} + run: shred -u "$KEY_PATH" 2>/dev/null || rm -f "$KEY_PATH" + + - name: Read GoReleaser metadata + id: metadata + env: + METADATA: ${{ steps.goreleaser.outputs.metadata }} + ARTIFACTS: ${{ steps.goreleaser.outputs.artifacts }} + run: | + version=$(jq -r '.version // empty' <<< "$METADATA") + # Prefer the multi-arch manifest, then a dockers_v2 image, then a + # single published image. + digest=$(jq -r ' + [.[] | select(.extra.Digest != null)] as $a + | ([$a[] | select(.type == "Docker Manifest")] + + [$a[] | select(.type == "Docker Image")] + + [$a[] | select(.type == "Published Docker Image")])[0].extra.Digest // empty + ' <<< "$ARTIFACTS") + # Every published image (tag stripped) by digest, for attest-images. + images=$(jq -c ' + [.[] | select(.extra.Digest != null and (.type | IN("Docker Manifest", "Docker Image", "Published Docker Image"))) + | {name: (.name | sub(":[^:/]+$"; "")), digest: .extra.Digest}] | unique + ' <<< "$ARTIFACTS") + { + echo "version=${version}" + echo "digest=${digest}" + echo "images=${images:-[]}" + } >> "$GITHUB_OUTPUT" + echo "version=${version} digest=${digest:-}" + - name: Attest binaries + if: ${{ env.IS_PR_BUILD == 'false' && env.SKIP_PUBLISH == 'false' }} + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: ${{ inputs.attestation-subject-path }} + - name: Publish GPG public key + if: ${{ steps.signing.outputs.public-key-path != '' }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + PUBLIC_KEY_PATH: ${{ steps.signing.outputs.public-key-path }} + run: | + cp "$PUBLIC_KEY_PATH" dist/gpg-public-key.asc + if [[ "$SKIP_PUBLISH" == false ]]; then + gh release upload "$TAG" dist/gpg-public-key.asc --clobber --repo "$GITHUB_REPOSITORY" + fi + + - name: Upload dist + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.dist-artifact-name }} + path: | + dist/*.rpm + dist/*.deb + dist/*.tar.gz + dist/*.zip + dist/*checksums.txt + dist/gpg-public-key.asc + if-no-files-found: warn + + attest-images: + needs: build-release-goreleaser + if: ${{ github.event_name != 'pull_request' && needs.build-release-goreleaser.outputs.images != '[]' }} + runs-on: ubuntu-latest + strategy: + matrix: + image: ${{ fromJSON(needs.build-release-goreleaser.outputs.images) }} + steps: + - name: Log in to GHCR + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Attest image + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ matrix.image.name }} + subject-digest: ${{ matrix.image.digest }} + push-to-registry: true diff --git a/.github/workflows/build-rpm-quadlet.yml b/.github/workflows/build-rpm-quadlet.yml index e9602ab..09074d8 100644 --- a/.github/workflows/build-rpm-quadlet.yml +++ b/.github/workflows/build-rpm-quadlet.yml @@ -1,6 +1,14 @@ # SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT # +# ############################################################################ +# # DEPRECATED WORKFLOW -- DO NOT USE IN NEW CALLERS +# # +# # build-rpm-quadlet is replaced by nfpm packages in build-release-goreleaser.yml / build-check-goreleaser.yml. +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ +# # Reusable workflow: builds the caller repo's podman quadlet RPM and # uploads it as an unsigned artifact for downstream signing. @@ -22,6 +30,9 @@ jobs: container: image: rockylinux:9 steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::build-rpm-quadlet.yml is deprecated; migrate to nfpm packages in build-release-goreleaser.yml / build-check-goreleaser.yml." - name: Install build dependencies run: dnf install -y -q git make rpm-build rpmlint diff --git a/.github/workflows/docker-build-release.yml b/.github/workflows/docker-build-release.yml index 2bfe731..3f96976 100644 --- a/.github/workflows/docker-build-release.yml +++ b/.github/workflows/docker-build-release.yml @@ -1,5 +1,13 @@ # SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT +# +# ############################################################################ +# # DEPRECATED WORKFLOW -- DO NOT USE IN NEW CALLERS +# # +# # docker-build-release is replaced by build-release-goreleaser.yml (GoReleaser builds the images; build-only: build-check-goreleaser.yml). +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ name: docker-build-release @@ -63,6 +71,9 @@ jobs: id-token: write # Sigstore signing (built-in provenance) attestations: write # built-in provenance & SBOM steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::docker-build-release.yml is deprecated; migrate to build-release-goreleaser.yml (GoReleaser builds the images; build-only: build-check-goreleaser.yml)." - name: checkout repository uses: actions/checkout@v6.0.3 with: @@ -158,6 +169,9 @@ jobs: permissions: contents: write # create GitHub release steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::docker-build-release.yml is deprecated; migrate to build-release-goreleaser.yml (GoReleaser builds the images; build-only: build-check-goreleaser.yml)." - name: Parse semver string id: semver-parser uses: booxmedialtd/ws-action-parse-semver@v1.4.7 diff --git a/.github/workflows/go-build-release.yml b/.github/workflows/go-build-release.yml index 03e1d94..c9db3bc 100644 --- a/.github/workflows/go-build-release.yml +++ b/.github/workflows/go-build-release.yml @@ -1,5 +1,13 @@ # SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT +# +# ############################################################################ +# # DEPRECATED WORKFLOW -- DO NOT USE IN NEW CALLERS +# # +# # go-build-release is replaced by build-release-goreleaser.yml (build-only: build-check-goreleaser.yml). +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ name: go-build-release @@ -81,6 +89,9 @@ jobs: name: GoReleaser ${{ (inputs.snapshot == 'true' || (inputs.snapshot != 'false' && !startsWith(github.ref, 'refs/tags/v'))) && 'Snapshot' || 'Release' }} runs-on: ubuntu-latest steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::go-build-release.yml is deprecated; migrate to build-release-goreleaser.yml (build-only: build-check-goreleaser.yml)." - name: Checkout uses: actions/checkout@v6.0.3 with: diff --git a/.github/workflows/gpg-sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml index 51defc3..1166ab0 100644 --- a/.github/workflows/gpg-sign-artifacts.yml +++ b/.github/workflows/gpg-sign-artifacts.yml @@ -1,6 +1,14 @@ # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT # +# ############################################################################ +# # DEPRECATED WORKFLOW -- DO NOT USE IN NEW CALLERS +# # +# # gpg-sign-artifacts is replaced by nfpm package signing in build-release-goreleaser.yml (gpg-key secret). +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ +# # 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. @@ -34,6 +42,9 @@ jobs: container: image: rockylinux:9 steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::gpg-sign-artifacts.yml is deprecated; migrate to nfpm package signing in build-release-goreleaser.yml (gpg-key secret)." - name: Install build dependencies run: | diff --git a/.github/workflows/pr-registry-cleanup.yml b/.github/workflows/pr-registry-cleanup.yml index ee7be5c..e5d7f4f 100644 --- a/.github/workflows/pr-registry-cleanup.yml +++ b/.github/workflows/pr-registry-cleanup.yml @@ -2,64 +2,84 @@ # SPDX-License-Identifier: MIT # # Reusable workflow: deletes the GHCR container image versions a pull request -# published, once that pull request closes. +# published, once that pull request closes. Call it from a `pull_request` +# `closed` trigger; the PR number comes from the event. GITHUB_TOKEN can't list +# packages, so image names come from `images`, else the ghcr.io entries in the +# GoReleaser config (`dockers_v2[].images`), else the repository name (see +# actions/discover-ghcr-images). name: pr-registry-cleanup on: workflow_call: inputs: - pr-number: - description: 'Number of the PR whose container tags should be removed' - default: ${{ github.event.pull_request.number || github.event.number }} - type: string - tag-prefix: - description: 'Prefix of the tags published by PR builds; the base tag is ``' - default: 'pr-' + images: + description: 'Space- or newline-separated GHCR package names to clean up (default: from the GoReleaser config, else the repository name)' + required: false type: string + default: '' permissions: + contents: read # checkout, to read the GoReleaser config packages: write # delete container image versions from GHCR jobs: - cleanup: + discover: + # Fork PRs publish nothing (and get a read-only token). Other events run + # so the trigger check can fail them. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-slim timeout-minutes: 10 + outputs: + packages: ${{ steps.packages.outputs.packages }} steps: - - name: Resolve PR number + - name: Check trigger + if: ${{ github.event_name != 'pull_request' }} env: - PR_NUMBER: ${{ inputs.pr-number }} + EVENT: ${{ github.event_name }} run: | - set -euo pipefail + echo "::error::pr-registry-cleanup runs on pull_request (closed); got ${EVENT}" + exit 1 - if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then - echo "pr-number must be a PR number, got '${PR_NUMBER}'" - exit 1 - fi + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Discover images + id: discover + uses: OpenCHAMI/github-actions/actions/discover-ghcr-images@26748ab38b9b17fa895919ef937aca32376a23e2 + with: + images: ${{ inputs.images }} + + - name: List packages for the matrix + id: packages + env: + NAMES: ${{ steps.discover.outputs.names }} + run: echo "packages=$(jq -cn --arg names "$NAMES" '$names | split(" ")')" >> "$GITHUB_OUTPUT" + + cleanup: + needs: discover + runs-on: ubuntu-slim + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.discover.outputs.packages) }} + steps: - name: Delete PR container image versions - # Cleanup is best effort: a package that was never published, or a tag a - # concurrent run already removed, must not fail the caller's PR close. - continue-on-error: true env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} OWNER: ${{ github.repository_owner }} - PACKAGE: ${{ github.event.repository.name }} - PR_NUMBER: ${{ inputs.pr-number }} - TAG_PREFIX: ${{ inputs.tag-prefix }} + PACKAGE: ${{ matrix.package }} + # PR builds are tagged pr- by build-release-goreleaser. + BASE_TAG: pr-${{ github.event.pull_request.number }} run: | set -euo pipefail - - base_tag="${TAG_PREFIX}${PR_NUMBER}" - - echo "Cleaning up container images of ${PACKAGE} tagged ${base_tag}*..." - # A version is ours when one of its tags is the base tag itself or the # base tag plus a separated qualifier (pr-12, pr-12-arm64, # pr-12-dirty-abc123). Requiring the separator keeps a cleanup of # pr-12 away from pr-123. - versions=$(gh api --paginate \ - "/orgs/${OWNER}/packages/container/${PACKAGE}/versions" \ - | jq -r --arg base "${base_tag}" ' + versions=$(gh api --paginate "/orgs/${OWNER}/packages/container/${PACKAGE}/versions" \ + | jq -r --arg base "${BASE_TAG}" ' .[] | select( (.metadata.container.tags // []) @@ -69,18 +89,20 @@ jobs: ) ) | .id - ') + ') || { echo "::error::Can't read versions of ghcr.io package '${PACKAGE}'."; exit 1; } if [[ -z "${versions}" ]]; then - echo "No versions found for ${PACKAGE} tagged ${base_tag}*" + echo "No versions tagged ${BASE_TAG}*" exit 0 fi + failed=false while read -r version_id; do - echo "Deleting version ID ${version_id}" - gh api --method DELETE \ - "/orgs/${OWNER}/packages/container/${PACKAGE}/versions/${version_id}" \ - || echo "Failed to delete version ${version_id}" + echo "Deleting version ${version_id}" + if ! gh api --method DELETE "/orgs/${OWNER}/packages/container/${PACKAGE}/versions/${version_id}"; then + echo "::error::Can't delete version ${version_id} of ghcr.io package '${PACKAGE}'." + failed=true + fi done <<< "${versions}" - - echo "Cleanup completed for PR ${PR_NUMBER}" + [[ "$failed" == false ]] || exit 1 + echo "Cleanup completed for ${BASE_TAG}" diff --git a/.github/workflows/release-signed-artifacts.yml b/.github/workflows/release-signed-artifacts.yml index b5d80fd..0f64989 100644 --- a/.github/workflows/release-signed-artifacts.yml +++ b/.github/workflows/release-signed-artifacts.yml @@ -1,6 +1,14 @@ # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT # +# ############################################################################ +# # DEPRECATED WORKFLOW -- DO NOT USE IN NEW CALLERS +# # +# # release-signed-artifacts is replaced by the GoReleaser release created by build-release-goreleaser.yml. +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ +# # Reusable workflow: publishes a GitHub Release for a tag, attaching signed # RPMs and public keys, with trust-chain verification instructions in the # release body. @@ -39,6 +47,9 @@ jobs: artifacts-release: runs-on: ubuntu-slim steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::release-signed-artifacts.yml is deprecated; migrate to the GoReleaser release created by build-release-goreleaser.yml." - name: Checkout uses: actions/checkout@v6.0.2 with: diff --git a/.github/workflows/trivy-image-scan.yml b/.github/workflows/trivy-image-scan.yml index dd6a4e6..0caf980 100644 --- a/.github/workflows/trivy-image-scan.yml +++ b/.github/workflows/trivy-image-scan.yml @@ -3,22 +3,25 @@ name: trivy-image-scan -# Reusable workflow that scans an already-built and -pushed container image -# with Trivy. Emits SARIF and uploads it to GitHub Advanced Security so -# findings land in the caller's Security tab. +# Reusable workflow that scans already-pushed container images with Trivy. +# Emits SARIF and uploads it to GitHub Advanced Security so findings land in +# the caller's Security tab. # -# Expects the image to already exist (either in a registry or as a local -# `docker save` tar at `image-path`). To run as a build-time gate inside -# docker-build-release, call this workflow after the build step with the -# fully-qualified `/@` ref. +# Scans the caller's GHCR images at the release tag (the tag that triggered the +# run, the triggering run's tag under `workflow_run`, or `pr-` on pull +# requests), one job per image, by digest. GITHUB_TOKEN can't list packages, so +# image names come from `images`, else the ghcr.io entries in the GoReleaser +# config (`dockers_v2[].images`), else the repository name (see +# actions/discover-ghcr-images). on: workflow_call: inputs: - image-ref: - description: 'Image to scan (e.g., ghcr.io/openchami/foo@sha256:...)' - required: true + images: + description: 'Space- or newline-separated GHCR package names to scan (default: from the GoReleaser config, else the repository name)' + required: false type: string + default: '' severity: description: 'Comma-separated severities to report (default: HIGH,CRITICAL)' required: false @@ -37,11 +40,67 @@ on: permissions: contents: read # baseline for checkout + packages: read # read image versions security-events: write # for SARIF upload to GHAS jobs: + discover: + runs-on: ubuntu-slim + outputs: + images: ${{ steps.images.outputs.images }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Discover images + id: discover + uses: OpenCHAMI/github-actions/actions/discover-ghcr-images@26748ab38b9b17fa895919ef937aca32376a23e2 + with: + images: ${{ inputs.images }} + + # TODO: IF: Candidate for deduplication into another reusable action + # IFF another workflow needs image digests. + - name: Find image digests + id: images + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + NAMES: ${{ steps.discover.outputs.names }} + # PR builds are tagged pr- by build-release-goreleaser. + TAG: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.event.workflow_run.head_branch || github.ref_name }} + run: | + set -euo pipefail + registry="ghcr.io/${OWNER,,}" + images='[]' + failed=false + # shellcheck disable=SC2086 # intentional word splitting + for package in $NAMES; do + if ! versions=$(gh api --paginate "/orgs/${OWNER}/packages/container/${package}/versions"); then + echo "::error::Can't read versions of ghcr.io package '${package}'." + failed=true + continue + fi + digest=$(jq -r --arg tag "$TAG" '[.[] | select((.metadata.container.tags // []) | index($tag)) | .name][0] // empty' <<< "$versions") + if [[ -z "$digest" ]]; then + echo "::notice::${package} has no image tagged ${TAG}; skipping" + continue + fi + images=$(jq -c --arg name "${registry}/${package}" --arg digest "$digest" \ + '. + [{name: $name, digest: $digest}]' <<< "$images") + done + [[ "$failed" == false ]] || exit 1 + echo "images=${images}" + echo "images=${images}" >> "$GITHUB_OUTPUT" + trivy: + needs: discover + if: ${{ needs.discover.outputs.images != '[]' }} runs-on: ubuntu-slim + strategy: + fail-fast: false + matrix: + image: ${{ fromJSON(needs.discover.outputs.images) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -50,7 +109,7 @@ jobs: - name: Run Trivy uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: - image-ref: ${{ inputs.image-ref }} + image-ref: ${{ matrix.image.name }}@${{ matrix.image.digest }} format: sarif output: trivy-results.sarif severity: ${{ inputs.severity }} @@ -62,4 +121,4 @@ jobs: uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: sarif_file: trivy-results.sarif - category: trivy-image-scan + category: trivy-image-scan/${{ matrix.image.name }} # one category per image diff --git a/.github/workflows/validate-packages.yml b/.github/workflows/validate-packages.yml new file mode 100644 index 0000000..7d7bed0 --- /dev/null +++ b/.github/workflows/validate-packages.yml @@ -0,0 +1,197 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: validate-packages + +# Reusable workflow that validates the rpm/deb packages produced by +# build-check-goreleaser or build-release-goreleaser (their dist artifact). +# For each package it checks, inside a matching distro container: +# +# - the file list is exactly what the caller expects (regular files and +# symlinks only: dpkg owns parent directories implicitly, and rpm matches so +# one spec covers both) +# - the rpm signature verifies against the gpg-public-key.asc that +# build-release-goreleaser puts in the artifact when it signs (skipped when +# absent, e.g. PR/check builds or no gpg-key) +# - the package installs (`dnf install` / `apt-get install`), which resolves +# dependencies and runs scriptlets, and every expected path exists afterward +# - rpmlint / lintian, report-only + +on: + workflow_call: + inputs: + packages: + description: 'YAML list of package specs, each with a `name` glob, a `files` list, and optional `rpm-files`/`deb-files` extras; every matching .rpm/.deb is validated' + required: true + type: string + dist-artifact-name: + description: 'Artifact holding the packages (from build-check-goreleaser / build-release-goreleaser)' + required: false + type: string + default: 'goreleaser-dist' + rpm-image: + description: 'Container image used to validate .rpm packages' + required: false + type: string + default: 'docker.io/library/rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6' + deb-image: + description: 'Container image used to validate .deb packages' + required: false + type: string + default: 'docker.io/library/debian:13@sha256:913f6706df59a68922d1dd08f78c2476560a8d367897200a6005b00e5f67c2d5' + lint: + description: 'Run rpmlint / lintian (report-only)' + required: false + type: boolean + default: true + +permissions: + contents: read # baseline for checkout + +jobs: + parse: + runs-on: ubuntu-slim + outputs: + matrix: ${{ steps.parse.outputs.matrix }} + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.dist-artifact-name }} + path: dist + + - name: Parse package specs + id: parse + env: + PACKAGES: ${{ inputs.packages }} + run: | + set -euo pipefail + # One matrix entry per file matching each spec's glob. The arch is + # read from the package header and picks a runner that can install it. + entries="${RUNNER_TEMP}/matrix.jsonl" + touch "$entries" + while IFS= read -r spec; do + glob=$(jq -r '.name' <<< "$spec") + mapfile -d '' pkgs < <(find dist -type f -name "$glob" \( -name '*.rpm' -o -name '*.deb' \) -print0 | sort -z) + if (( ${#pkgs[@]} == 0 )); then + echo "::error::no package in dist matches ${glob}" + exit 1 + fi + for pkg in "${pkgs[@]}"; do + format=${pkg##*.} + if [[ "$format" == rpm ]]; then + arch=$(rpm -qp --nosignature --qf '%{ARCH}' "$pkg") + else + arch=$(dpkg-deb -f "$pkg" Architecture) + fi + case "$arch" in + x86_64|amd64|noarch|all) runner=ubuntu-latest ;; + aarch64|arm64) runner=ubuntu-24.04-arm ;; + *) echo "::error::unsupported architecture ${arch}: ${pkg}"; exit 1 ;; + esac + jq -c --arg path "$pkg" --arg file "${pkg##*/}" --arg format "$format" \ + --arg runner "$runner" \ + '. + {path: $path, file: $file, format: $format, runner: $runner, + files: ((.files // []) + (.["\($format)-files"] // []))}' \ + <<< "$spec" >> "$entries" + done + done < <(yq -o=json -I=0 '.[]' <<< "$PACKAGES") + matrix=$(jq -sc '.' "$entries") + echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" + + validate: + needs: parse + name: validate (${{ matrix.package.file }}) + runs-on: ${{ matrix.package.runner }} + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.parse.outputs.matrix) }} + container: + image: ${{ matrix.package.format == 'rpm' && inputs.rpm-image || inputs.deb-image }} # zizmor: ignore[unpinned-images] defaults are digest-pinned; overrides are the caller's + defaults: + run: + shell: bash + env: + PKG: ${{ matrix.package.path }} + PKG_FORMAT: ${{ matrix.package.format }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.dist-artifact-name }} + path: dist + + - name: Install tools + run: | + set -euo pipefail + if [[ "$PKG_FORMAT" == rpm ]]; then + dnf install -y -q jq diffutils + else + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq jq + fi + + - name: Verify file list is exactly what's expected + env: + FILES: ${{ toJSON(matrix.package.files) }} + run: | + set -euo pipefail + if [[ "$PKG_FORMAT" == rpm ]]; then + rpm -qp --nosignature --qf '[%{FILEMODES:perms} %{FILENAMES}\n]' "$PKG" | sed -n 's/^[^d][^ ]* //p' | sort > /tmp/actual-files.txt + else + dpkg-deb --fsys-tarfile "$PKG" | tar -t | sed 's|^\./|/|' | grep -v '/$' | sort | grep -v '^$' > /tmp/actual-files.txt + fi + jq -r '.[]' <<< "$FILES" | sort | grep -v '^$' > /tmp/expected-files.txt + diff /tmp/expected-files.txt /tmp/actual-files.txt + + - name: Verify signature + run: | + set -euo pipefail + key=dist/gpg-public-key.asc + if [[ ! -f "$key" ]]; then + echo "::notice::no gpg-public-key.asc in the artifact (unsigned build); signature not checked" + exit 0 + fi + if [[ "$PKG_FORMAT" != rpm ]]; then + echo "::notice::deb packages are not signature-checked" + exit 0 + fi + rpm --import "$key" + out=$(rpm -K "$PKG") + echo "$out" + grep -q 'signatures OK' <<< "$out" + + - name: Install package + run: | + set -euo pipefail + if [[ "$PKG_FORMAT" == rpm ]]; then + dnf install -y "./${PKG}" + else + DEBIAN_FRONTEND=noninteractive apt-get install -y "./${PKG}" + fi + # Every expected path must exist after install (catches e.g. dangling + # symlinks packaged in place of files). + missing=0 + while IFS= read -r f; do + [[ -z "$f" ]] && continue + if [[ ! -e "$f" ]]; then + echo "::error::installed path missing or dangling: ${f}" + missing=1 + fi + done < /tmp/expected-files.txt + exit "$missing" + + - name: Lint package (report-only) + if: ${{ inputs.lint }} + continue-on-error: true + run: | + set -uo pipefail + if [[ "$PKG_FORMAT" == rpm ]]; then + dnf install -y -q epel-release && dnf install -y -q rpmlint + rpmlint "$PKG" + else + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq lintian + lintian "$PKG" + fi diff --git a/.github/workflows/validate-rpm-quadlet.yml b/.github/workflows/validate-rpm-quadlet.yml index c5c258d..afeb38d 100644 --- a/.github/workflows/validate-rpm-quadlet.yml +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -1,6 +1,14 @@ # SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT # +# ############################################################################ +# # DEPRECATED WORKFLOW -- DO NOT USE IN NEW CALLERS +# # +# # validate-rpm-quadlet is replaced by validate-packages.yml. +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ +# # Reusable workflow: validates a signed quadlet RPM's installed file list # against the set of files the caller expects it to ship. @@ -26,6 +34,9 @@ jobs: outputs: matrix: ${{ steps.parse.outputs.matrix }} steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::validate-rpm-quadlet.yml is deprecated; migrate to validate-packages.yml." - name: Parse RPM specs id: parse env: @@ -41,6 +52,9 @@ jobs: matrix: rpm: ${{ fromJSON(needs.parse.outputs.matrix) }} steps: + - name: Deprecation warning + run: | + echo "::warning title=Deprecated workflow::validate-rpm-quadlet.yml is deprecated; migrate to validate-packages.yml." - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-tags: true diff --git a/.goreleaser.example.yml b/.goreleaser.example.yml new file mode 100644 index 0000000..ee78e0e --- /dev/null +++ b/.goreleaser.example.yml @@ -0,0 +1,329 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# +# /.goreleaser.yaml +# +# ============================================================================= +# GoReleaser - Example Configuration for OpenCHAMI/github-actions +# ============================================================================= +# +# Reference config for the build-check-goreleaser, build-release-goreleaser, +# and validate-packages reusable workflows. Copy it into your repo as +# .goreleaser.yaml, rename `foo`, and keep the sections you need. Each section +# notes which release effect it produces and links to the full reference. +# +# Release effect Section +# --------------------------------- ------------------------------------------ +# Binaries builds +# Bare-metal tarballs + checksums archives, checksum +# Multi-arch container images dockers_v2 +# :pr- images on PRs dockers_v2.tags (uses {{ .Tag }}) +# rpm / deb packages (incl. quadlet) nfpms +# Package signing nfpms[].rpm.signature (GPG_KEY_PATH) +# GitHub release + notes release +# Build provenance attestations (workflow: automatic for binaries and images) +# GPG public key on the release (workflow: derived from `gpg-key`) +# Publishing to a yum/apt repository (out of scope; see below) +# +# Out of scope: GoReleaser only attaches packages to the GitHub release. +# Publishing them to a package repository (yum/apt metadata, signed repomd) is +# handled by a separate post-release workflow, which is easier to manage than +# GoReleaser's `blob:` / `publishers:` (or Pro-only hosted integrations): +# repository metadata spans all packages and releases, and publishing runs +# only after validation and publish-release. +# +# Environment provided by the workflows (always set, so templates can rely on +# them; set them yourself when running goreleaser locally): +# +# IS_PR_BUILD "true" on PR builds and build checks, "false" on releases +# GPG_KEY_PATH path to the private key from the `gpg-key` secret on release +# builds; empty otherwise (packages are then unsigned) +# GIT_STATE "clean" or "dirty" +# BUILD_HOST runner hostname +# GO_VERSION `go version` output, e.g. go1.26.7 +# BUILD_USER runner user +# +# Mode behavior (set by the workflows, not this file): +# PR build HEAD is tagged locally as `pr-`, so {{ .Tag }} == "pr-". +# Images are pushed; archives, nfpm, and the release are skipped. +# Build check `--snapshot`: everything is built, nothing is published. +# Release `v*` tag push: everything is built and published; the release +# is a draft with GitHub-generated notes (`--release-notes`). +# +# Template syntax and variables: +# https://goreleaser.com/customization/general/templates/ +# +version: 2 + +# Name used in archive/package/image names. +# +# Default: the repository name +# Docs: https://goreleaser.com/customization/general/project/ +project_name: foo + +# ------------------------------------------------------------------------- +# Pre-build hooks +# ------------------------------------------------------------------------- + +# Commands run once before building. Templates are allowed, which is how +# packaged files get release-specific values (e.g. a quadlet's image tag). +# Write generated files outside dist/ and gitignore them: GoReleaser refuses +# to release from a dirty git tree. +# +# Docs: https://goreleaser.com/customization/general/hooks/ +before: + hooks: + - go mod tidy + # Pin the packaged quadlet to the image this run publishes + # (:vX.Y.Z on release, :pr-N on PR builds). + - make quadlet-render IMAGE_TAG={{ .Tag }} + +# ------------------------------------------------------------------------- +# Binaries +# ------------------------------------------------------------------------- + +# One entry per binary. Build metadata comes from the workflow environment. +# CGO cross-compiles need a C cross toolchain: pass it to the workflows with +# `build-deps` and `cgo-enabled`, and set CC per arch here. +# +# Docs: https://goreleaser.com/customization/builds/builders/go/ +builds: + - id: foo + main: ./cmd/foo + binary: foo + goos: [linux] + goarch: [amd64, arm64] + env: + - CGO_ENABLED=0 + ldflags: + - >- + -s -w + -X main.version={{ .Version }} + -X main.commit={{ .Commit }} + -X main.date={{ .Date }} + -X main.gitState={{ .Env.GIT_STATE }} + -X main.buildHost={{ .Env.BUILD_HOST }} + -X main.goVersion={{ .Env.GO_VERSION }} + -X main.buildUser={{ .Env.BUILD_USER }} + +# ------------------------------------------------------------------------- +# Bare-metal tarballs and checksums +# ------------------------------------------------------------------------- + +# Tarballs of the binaries, attached to the release. +# +# Docs: https://goreleaser.com/customization/package/archives/ +archives: + - id: foo + ids: [foo] + formats: [tar.gz] + files: + - LICENSES/* + - README.md + +# Checksums for every release file. +# +# Docs: https://goreleaser.com/customization/package/checksum/ +checksum: + name_template: "{{ .ProjectName }}_checksums.txt" + +# ------------------------------------------------------------------------- +# Container images +# ------------------------------------------------------------------------- + +# One multi-platform image per entry, pushed by buildx. The Dockerfile copies +# prebuilt binaries from the build context: +# +# FROM gcr.io/distroless/static +# ARG TARGETPLATFORM +# COPY $TARGETPLATFORM/foo /usr/bin/foo +# ENTRYPOINT ["/usr/bin/foo"] +# +# Tags that render empty are skipped, which keeps `latest` and the semver +# shortcuts off PR builds. build-release-goreleaser attests every published +# image by digest. +# +# Docs: https://goreleaser.com/customization/package/dockers_v2/ +dockers_v2: + - id: foo + ids: [foo] + dockerfile: Dockerfile + images: + - ghcr.io/openchami/foo + tags: + - "{{ .Tag }}" # vX.Y.Z or pr-N + - '{{ if ne .Env.IS_PR_BUILD "true" }}{{ .Major }}.{{ .Minor }}{{ end }}' + - '{{ if ne .Env.IS_PR_BUILD "true" }}{{ .Major }}{{ end }}' + - '{{ if ne .Env.IS_PR_BUILD "true" }}latest{{ end }}' + platforms: + - linux/amd64 + - linux/arm64 + labels: + org.opencontainers.image.title: "{{ .ProjectName }}" + org.opencontainers.image.version: "{{ .Version }}" + org.opencontainers.image.revision: "{{ .FullCommit }}" + org.opencontainers.image.created: "{{ .Date }}" + extra_files: + - LICENSES/MIT.txt + +# ------------------------------------------------------------------------- +# rpm / deb packages +# ------------------------------------------------------------------------- + +# Packages are built with nfpm (no spec file, no rpmbuild). One entry can +# produce both rpm and deb: `release`, `dependencies`, and `suggests` are set +# per format under `overrides`. Keep `contents` at the top level and mark +# single-format files with `packager:`, because `overrides..contents` +# replaces the whole list. +# +# `release` is a literal string: no templates, no env vars, and rpm macros +# such as %{?dist} are stored verbatim. Nothing is detected from the build +# host. Set the dist suffix yourself (e.g. "1.el9"). +# +# List every file AND directory the package creates (`type: dir`), so rpm +# owns them and removes them on erase. validate-packages compares the exact +# file list: rpm lists include owned directories, deb lists only files. +# +# nfpm content types (`type:`): config, config|noreplace, dir, license, doc, +# ghost, symlink. +# +# Targeting several EL or Debian/Ubuntu releases: add one entry per target +# with a unique `id` and its own `release` (e.g. "1.el9", "1.el10"; deb +# "1~deb12", "1~ubuntu24.04"), sharing the rest by anchoring the first list +# item (`- &quadlet` ... `- <<: *quadlet`; GoReleaser rejects top-level `x-` +# keys). That suits noarch packages and CGO_ENABLED=0 binaries. cgo binaries +# must be built against each release's glibc instead: run one build per +# release in CI (e.g. in an EL container) with a rendered config. With several +# entries, the filenames must differ, hence `file_name_template` below. GitHub +# release assets turn `~` into `.`, which breaks checksums, so strip `~` from +# the filename if a release uses it. +# +# Docs: https://goreleaser.com/customization/package/nfpm/ +# https://nfpm.goreleaser.com/docs/configuration/ +# https://github.com/goreleaser/goreleaser/blob/main/internal/pipe/nfpm/nfpm.go +nfpms: + - id: foo-quadlet + package_name: foo-quadlet + # Packages only the listed contents; no binaries. + meta: true + formats: [rpm, deb] + # Standard names: rpm `name-version-release.arch.rpm`, deb + # `name_version-release_arch.deb`. The default omits `release`. + file_name_template: "{{ .ConventionalFileName }}" + vendor: OpenCHAMI + homepage: https://www.openchami.org + maintainer: "Jane Doe " + description: Podman Quadlet unit files for running foo. + license: MIT + section: admin + contents: + - dst: /etc/openchami + type: dir + - dst: /etc/openchami/configs + type: dir + # Point at real files: nfpm packages a symlink source as a symlink. + - src: packaging/configs/foo.yaml + dst: /etc/openchami/configs/foo.yaml + type: "config|noreplace" + # Rendered by the `before` hook. + - src: build/quadlet/foo.container + dst: /usr/share/containers/systemd/foo.container + - dst: /usr/share/containers/systemd/foo.container.d + type: dir + - src: packaging/systemd/foo.container.d/10-defaults.conf + dst: /usr/share/containers/systemd/foo.container.d/10-defaults.conf + # License: %license under /usr/share/licenses for rpm ... + - dst: /usr/share/licenses/foo-quadlet + type: dir + packager: rpm + - src: LICENSES/MIT.txt + dst: /usr/share/licenses/foo-quadlet/MIT.txt + type: license + packager: rpm + # ... and a machine-readable copyright file for deb: + # https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + - src: packaging/deb/copyright + dst: /usr/share/doc/foo-quadlet/copyright + packager: deb + # One script per hook serves both formats by branching on $1: rpm passes + # the instance count (1 = install, 2+ = upgrade, 0 = erase), deb an action + # (configure, remove, upgrade, ...; old version in $2 on upgrade). e.g. + # + # case "$1" in + # configure) [ -n "$2" ] && upgrade=1 ;; # deb + # [0-9]*) [ "$1" -ge 2 ] && upgrade=1 ;; # rpm + # esac + # + # https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + # https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html + scripts: + postinstall: packaging/scripts/postinstall.sh + preremove: packaging/scripts/preremove.sh + postremove: packaging/scripts/postremove.sh + overrides: + rpm: + release: "1.el9" + # RPM dependency syntax: + # https://rpm-software-management.github.io/rpm/manual/dependencies.html + dependencies: + - podman >= 5.0.0 + - systemd + suggests: + - smd-quadlet >= 2.20.0 + deb: + release: "1" + # Debian dependency syntax: + # https://www.debian.org/doc/debian-policy/ch-relationships.html + dependencies: + - podman (>= 5.0.0) + - systemd + suggests: + - smd-quadlet (>= 2.20.0) + rpm: + # Signed only when GPG_KEY_PATH is set (release builds with the + # `gpg-key` secret); the passphrase comes from NFPM_PASSPHRASE, which + # the workflow fills from `gpg-key-passphrase`. The matching public key + # is attached to the release and checked by validate-packages. + # Docs: https://goreleaser.com/customization/package/nfpm/#signing-key-passphrases + signature: + key_file: "{{ .Env.GPG_KEY_PATH }}" + +# ------------------------------------------------------------------------- +# GitHub release +# ------------------------------------------------------------------------- + +# The workflows control draft state and notes: build-release-goreleaser passes +# `--draft` (input `release-draft`, default true; publish later with +# publish-release) and GitHub's generated notes via `--release-notes` +# (input `generate-release-notes`), which replace the changelog below. +# `header` / `footer` are added around those notes. +# +# Docs: https://goreleaser.com/customization/publish/scm/ +release: + # No GitHub release for PR builds. + disable: '{{ if eq .Env.IS_PR_BUILD "true" }}true{{ else }}false{{ end }}' + name_template: "v{{ .Version }}" + # Marks vX.Y.Z-rc1 and similar tags as prereleases. + prerelease: auto + footer: | + ## Verifying packages + + ```bash + rpm --import gpg-public-key.asc + rpm -K foo-quadlet-*.rpm + ``` + +# Used only when no `--release-notes` file is passed. `use: git` needs no API +# token; `github-native` matches GitHub's own notes but calls the API. +# +# Default: git +# Docs: https://goreleaser.com/customization/publish/changelog/ +changelog: + use: git + +# Version used by build checks (`--snapshot`). +# +# Default: "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}" +# Docs: https://goreleaser.com/customization/publish/snapshots/ +#snapshot: +# version_template: "{{ incpatch .Version }}-devel" diff --git a/README.md b/README.md index ad2edf7..01c3d64 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,18 @@ Reusable GitHub Actions for CI/CD. ## Structure -- `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-configure-release-keys`: **Deprecated** - use nfpm package signing in `build-release-goreleaser` instead. Generates and certifies a per-run ephemeral GPG key through the repo's release key chain +- `actions/gpg-sign-rpm`: **Deprecated** - use nfpm package signing in `build-release-goreleaser` instead. 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 +- `actions/discover-ghcr-images`: Finds and verifies a repo's GHCR image names (`images` input, GoReleaser config, or repo name) +- `actions/gpg-verify-trust-chain`: **Deprecated** - use the `rpm -K` check in `validate-packages` instead. Verifies the master/repo-cert/ephemeral trust chain and optionally checksigs RPMs +- `.github/workflows/go-build-release.yml`: **Deprecated** - use `build-release-goreleaser` (build-only: `build-check-goreleaser`) instead. Reusable workflow for GoReleaser builds +- `.github/workflows/docker-build-release.yml`: **Deprecated** - use `build-release-goreleaser` (build-only: `build-check-goreleaser`) instead. Reusable workflow for multi-arch container image builds +- `.github/workflows/build-publish-container-goreleaser.yml`: **Deprecated** - use `build-release-goreleaser` (build-only: `build-check-goreleaser`) instead. Builds and publishes a container image via GoReleaser +- `.github/workflows/build-rpm-quadlet.yml`: **Deprecated** - use nfpm packages built by `build-release-goreleaser` / `build-check-goreleaser` instead. Builds a caller repo's podman quadlet RPM +- `.github/workflows/gpg-sign-artifacts.yml`: **Deprecated** - use nfpm package signing in `build-release-goreleaser` instead. Signs unsigned RPM artifacts with a per-run ephemeral key +- `.github/workflows/validate-rpm-quadlet.yml`: **Deprecated** - use `validate-packages` instead. Validates a signed quadlet RPM's installed file list +- `.github/workflows/release-signed-artifacts.yml`: **Deprecated** - use the release created by `build-release-goreleaser` instead. Publishes a GitHub Release with signed RPMs and public keys - `.github/workflows/publish-release.yml`: Publishes the draft GitHub Release for a tag - `.github/workflows/lint-ci.yml`: Reusable workflow that lints workflow files (actionlint + zizmor) - `.github/workflows/lint-go.yml`: Reusable workflow that runs golangci-lint and checks go.mod/go.sum are tidy @@ -30,6 +31,9 @@ Reusable GitHub Actions for CI/CD. - `.github/workflows/trivy-image-scan.yml`: Reusable workflow that scans built container images for CVEs - `.github/workflows/scorecard.yml`: Reusable workflow that runs the OpenSSF Scorecard supply-chain analysis - `.github/workflows/pr-registry-cleanup.yml`: Deletes the GHCR container images a PR published, once it closes +- `.github/workflows/build-check-goreleaser.yml`: Reusable workflow that builds everything in a GoReleaser config (snapshot) without publishing +- `.github/workflows/build-release-goreleaser.yml`: Reusable workflow that publishes `pr-` images for PRs, or a full GoReleaser release for tags +- `.github/workflows/validate-packages.yml`: Reusable workflow that validates rpm/deb packages (file list, signature, install, lint) - `.github/workflows/stale.yml`: Reusable workflow that marks and closes inactive issues and PRs (org-default policy) - `.github/workflows/lint-codegen-fabrica.yml`: Reusable workflow that fails when a Fabrica project's committed generated code is out of date @@ -44,20 +48,127 @@ Pin a release tag: ```yaml # For actions -- uses: OpenCHAMI/github-actions/actions/gpg-configure-release-keys@v4.0 -- uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@v4.0 +- uses: OpenCHAMI/github-actions/actions/gpg-check-key-expiration@v4.0 # For reusable workflows jobs: release: - uses: OpenCHAMI/github-actions/.github/workflows/go-build-release.yml@v4.0 + uses: OpenCHAMI/github-actions/.github/workflows/build-release-goreleaser.yml@v4.0 ``` Pin a commit SHA instead for maximum supply-chain safety if desired. ## Workflows -### go-build-release (Reusable Workflow) +### Building and releasing with GoReleaser + +`build-check-goreleaser` (build only), `build-release-goreleaser` (PR images or tag release), and `validate-packages` share the caller's `.goreleaser.yaml`. A release chains build-release → validate-packages → `publish-release`, so the draft goes public only after validation. + +The build workflows export `IS_PR_BUILD` and `GPG_KEY_PATH` (empty when unsigned) for the config, e.g. `release.disable` on `IS_PR_BUILD` and nfpm `key_file: '{{ .Env.GPG_KEY_PATH }}'`. For nfpm packages, list every directory too (`type: dir`) and set `release` and dependencies per format under `overrides`. + +See [`.goreleaser.example.yml`](.goreleaser.example.yml) for an annotated config covering binaries, archives, images, rpm/deb packages, signing, and the release. + +### build-check-goreleaser (Reusable Workflow) +Runs `goreleaser release --snapshot --clean`: builds binaries, archives, packages, and images, and publishes nothing. Read-only and secret-free, so it is safe for fork PRs. Uploads the packages, archives, and checksums as an artifact for `validate-packages`. + +**Usage:** +```yaml +name: Build Check +on: + pull_request: + +jobs: + build-check: + uses: OpenCHAMI/github-actions/.github/workflows/build-check-goreleaser.yml@v4.0 + # Optional overrides: + # with: + # go-version: stable # mutually exclusive with go-version-file + # go-version-file: go.mod + # goreleaser-version: v2.18.2 + # build-deps: gcc-aarch64-linux-gnu libc6-dev-arm64-cross + # cgo-enabled: 1 + # skip: docker # GoReleaser pipes to skip + # dist-artifact-name: goreleaser-dist # empty skips the upload +``` + +### build-release-goreleaser (Reusable Workflow) +Runs the caller's GoReleaser config in one of two modes, chosen from the triggering event (no manual override; any other trigger fails): + +- **PR** (`pull_request` event): tags HEAD locally as `pr-` and pushes the resulting images (e.g. `:pr-12`). No GitHub release, no signing, no attestation, and no packages: nfpm is skipped because a `pr-` version isn't a valid deb version, so validate PR packages with `build-check-goreleaser` (snapshot) instead. Fork PRs build but publish nothing. +- **Release** (`v*` tag push): semver images, packages (signed when the `gpg-key` secret is passed and the nfpm config uses `GPG_KEY_PATH`), a GitHub release (draft by default) with GitHub-generated notes, and build provenance attestations for the release files and every published image. When signing, the public key is derived from `gpg-key` and attached to the release (and the dist artifact) as `gpg-public-key.asc`. + +Outputs `version` and `digest` (the image manifest digest). Uploads the packages, archives, and checksums as an artifact for `validate-packages`. + +**Usage:** +```yaml +name: Release +on: + pull_request: # pr- images + push: + tags: ['v*'] # release + +jobs: + release: + uses: OpenCHAMI/github-actions/.github/workflows/build-release-goreleaser.yml@v4.0 + permissions: + contents: write + packages: write + id-token: write + attestations: write + secrets: + gpg-key: ${{ secrets.GPG_KEY }} + gpg-key-passphrase: ${{ secrets.GPG_KEY_PASSPHRASE }} + # Optional overrides: + # with: + # go-version: stable # mutually exclusive with go-version-file + # go-version-file: go.mod + # goreleaser-version: v2.18.2 + # build-deps: gcc-aarch64-linux-gnu libc6-dev-arm64-cross + # cgo-enabled: 1 + # release-draft: false # default true; publish later with publish-release + # generate-release-notes: false + # attestation-subject-path: dist/** + # goreleaser-args: --timeout 60m + # dist-artifact-name: goreleaser-dist +``` + +### validate-packages (Reusable Workflow) +Validates the rpm/deb packages from either build workflow's artifact, one job per package, in a matching container (`rockylinux:9` for `.rpm`, `debian:13` for `.deb`): + +- **File list**: must match `files` exactly. rpm lists include every owned directory; deb lists contain regular files and symlinks only, since dpkg owns parent directories implicitly. +- **Signature**: when the artifact contains `gpg-public-key.asc` (a signed release from `build-release-goreleaser`), rpm signatures must verify (`rpm -K`). Skipped for unsigned builds; deb packages are not signature-checked. +- **Install**: `dnf install` / `apt-get install` of the package, which resolves dependencies and runs scriptlets; every expected path must exist afterward (catches dangling symlinks). +- **Lint**: rpmlint / lintian, report-only. + +**Usage:** +```yaml +jobs: + validate: + needs: release + uses: OpenCHAMI/github-actions/.github/workflows/validate-packages.yml@v4.0 + with: + packages: | + - name: foo-quadlet* + files: + - /etc/openchami/configs/foo.yaml + - /usr/share/containers/systemd/foo.container + rpm-files: + - /usr/share/licenses/foo-quadlet/MIT.txt + deb-files: + - /usr/share/doc/foo-quadlet/copyright + # Optional overrides: + # dist-artifact-name: goreleaser-dist + # rpm-image: docker.io/library/rockylinux:9 + # deb-image: docker.io/library/debian:13 + # lint: false +``` + + +### go-build-release (Reusable Workflow, Deprecated) + +> [!CAUTION] +> **Deprecated.** Use `build-release-goreleaser` (build-only: `build-check-goreleaser`) instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + Standardized GoReleaser workflow for building and releasing Go applications with: - Multi-architecture builds (linux/amd64, linux/arm64) - Flexible pre-build setup steps @@ -221,25 +332,27 @@ jobs: ``` ### trivy-image-scan (Reusable Workflow) -Scans an already-pushed container image with Trivy and uploads SARIF findings to GitHub Advanced Security. Designed to chain after `docker-build-release` by scanning an image tag it pushed (`docker-build-release` tags by branch, git tag, or `pr-`). +Scans already-pushed container images with Trivy and uploads SARIF findings to GitHub Advanced Security. It scans each image at the release tag (`pr-` on pull requests), one job per image, by digest. GitHub doesn't let `GITHUB_TOKEN` list packages, so image names come from `images`, else the `ghcr.io` entries in the GoReleaser config (`dockers_v2[].images`), else the repository name (see `discover-ghcr-images`). Every name must be an existing package; all bad names are reported, then the run fails. Run it after `build-release-goreleaser`. **Usage:** ```yaml jobs: build: - uses: OpenCHAMI/github-actions/.github/workflows/docker-build-release.yml@v4.0 - with: - registry-name: ghcr.io/openchami/foo + uses: OpenCHAMI/github-actions/.github/workflows/build-release-goreleaser.yml@v4.0 scan: needs: build uses: OpenCHAMI/github-actions/.github/workflows/trivy-image-scan.yml@v4.0 - with: - image-ref: ghcr.io/openchami/foo:${{ github.ref_name }} # on a tag push - # Optional overrides: - # severity: CRITICAL - # ignore-unfixed: true - # exit-code: '0' # report only + permissions: + contents: read + packages: read + security-events: write + # Optional overrides: + # with: + # images: foo foo-init # default: from the GoReleaser config, else the repo name + # severity: CRITICAL + # ignore-unfixed: true + # exit-code: '0' # report only ``` ### scorecard (Reusable Workflow) @@ -266,7 +379,11 @@ jobs: uses: OpenCHAMI/github-actions/.github/workflows/scorecard.yml@v4.0 ``` -### build-publish-container-goreleaser (Reusable Workflow) +### build-publish-container-goreleaser (Reusable Workflow, Deprecated) + +> [!CAUTION] +> **Deprecated.** Use `build-release-goreleaser` (build-only: `build-check-goreleaser`) instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + Builds and publishes a container image via GoReleaser, with multi-arch builds, build provenance attestation, and PR snapshot support. Release builds (`is-pr-build: false`) pass GitHub's auto-generated release notes for the pushed tag to GoReleaser via `--release-notes`. Optional `build-deps` is a space-separated list of apt packages installed before the build, for cases such as CGO cross-compilation that need a toolchain not present on the runner. @@ -283,7 +400,11 @@ jobs: build-deps: gcc-aarch64-linux-gnu libc6-dev-arm64-cross ``` -### build-rpm-quadlet (Reusable Workflow) +### build-rpm-quadlet (Reusable Workflow, Deprecated) + +> [!CAUTION] +> **Deprecated.** Use nfpm packages built by `build-release-goreleaser` / `build-check-goreleaser` instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + Builds the caller repo's podman quadlet RPM and uploads it as an unsigned artifact for downstream signing. **Usage:** @@ -296,7 +417,11 @@ jobs: # artifact-name-unsigned-rpms: rpms-unsigned ``` -### gpg-sign-artifacts (Reusable Workflow) +### gpg-sign-artifacts (Reusable Workflow, Deprecated) + +> [!CAUTION] +> **Deprecated.** Use nfpm package signing in `build-release-goreleaser` instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + 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:** @@ -307,7 +432,11 @@ jobs: secrets: inherit ``` -### validate-rpm-quadlet (Reusable Workflow) +### validate-rpm-quadlet (Reusable Workflow, Deprecated) + +> [!CAUTION] +> **Deprecated.** Use `validate-packages` instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + Validates a signed quadlet RPM's installed file list against the set of files the caller expects it to ship. **Usage:** @@ -323,7 +452,11 @@ jobs: - /etc/containers/systemd/foo.container ``` -### release-signed-artifacts (Reusable Workflow) +### release-signed-artifacts (Reusable Workflow, Deprecated) + +> [!CAUTION] +> **Deprecated.** Use the release created by `build-release-goreleaser` instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + Publishes a GitHub Release for a tag, attaching signed RPMs and public keys, with trust-chain verification instructions in the release body. **Usage:** @@ -351,7 +484,7 @@ jobs: ``` ### pr-registry-cleanup (Reusable Workflow) -Deletes the GHCR container image versions a pull request published, once that pull request closes. Matches the PR's base tag and any separated qualifier (`pr-12`, `pr-12-amd64`, `pr-12-dirty-abc123`). +Deletes the GHCR container image versions a pull request published, once that pull request closes. Matches the PR's base tag and any separated qualifier (`pr-12`, `pr-12-amd64`, `pr-12-dirty-abc123`) in each image, one job per image. Image names come from `images`, else the GoReleaser config, else the repository name (see `discover-ghcr-images`). Fork PRs are skipped, since they publish nothing. Every name must be an existing package; all failed lookups and deletes are reported, then the run fails. **Usage:** ```yaml @@ -364,11 +497,11 @@ jobs: cleanup: uses: OpenCHAMI/github-actions/.github/workflows/pr-registry-cleanup.yml@v4.0 permissions: + contents: read packages: write # Optional overrides: # with: - # pr-number: 123 - # tag-prefix: pr- + # images: foo foo-init # default: from the GoReleaser config, else the repo name ``` ### stale (Reusable Workflow) @@ -447,20 +580,38 @@ jobs: ## Actions -### gpg-configure-release-keys +### gpg-configure-release-keys (Deprecated) + +> [!CAUTION] +> **Deprecated.** Use nfpm package signing in `build-release-goreleaser` instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + 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 +### gpg-sign-rpm (Deprecated) + +> [!CAUTION] +> **Deprecated.** Use nfpm package signing in `build-release-goreleaser` instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + 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 +### discover-ghcr-images +Finds a repo's GHCR image names from the `images` input, else the GoReleaser config, else the repo name, and verifies each one exists. Used by `trivy-image-scan` and `pr-registry-cleanup`. See the [action README](actions/discover-ghcr-images/README.md). + +### gpg-verify-trust-chain (Deprecated) + +> [!CAUTION] +> **Deprecated.** Use the `rpm -K` check in `validate-packages` instead. Kept only for callers pinned to older refs; it will be removed in a future major release. + 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 +> [!CAUTION] +> **Deprecated.** The ephemeral-key trust chain below is being retired in favor of a single signing key used by `build-release-goreleaser`. It applies only to the deprecated signing workflows and actions. + Trust chain: `Ephemeral Key <- Repo Cert Key <- Offline Master Key`. Design principles: @@ -473,6 +624,9 @@ Key expiration limits future signing only; existing signatures remain valid if t ## Example Workflow (Combined) +> [!CAUTION] +> **Deprecated.** This example uses deprecated workflows. New pipelines should use `build-release-goreleaser` + `validate-packages`. + Adapted from metadata-service's PR build workflow, chaining container build, RPM build, signing, and validation: ```yaml @@ -556,7 +710,7 @@ jobs: ## Continuous Integration - Workflow files are linted via `0-local-ci.yml`, which calls `lint-ci.yml` (actionlint + zizmor). -- RPM/quadlet output is validated via `validate-rpm-quadlet.yml`. +- Packages built by the GoReleaser workflows are validated via `validate-packages.yml`. - TODO: matrix test invoking each action directly. ## Rotation & Revocation diff --git a/actions/discover-ghcr-images/README.md b/actions/discover-ghcr-images/README.md new file mode 100644 index 0000000..2730af9 --- /dev/null +++ b/actions/discover-ghcr-images/README.md @@ -0,0 +1,31 @@ + + +# discover-ghcr-images + +Finds and verifies the caller's GHCR package names. `GITHUB_TOKEN` can't list an +organization's packages, so names come from exactly one source: + +1. the `images` input, if set +2. else the `ghcr.io` entries in the GoReleaser config's `dockers_v2[].images` + (found the same way GoReleaser finds it) +3. else the repository name, lowercased + +Every name must be an existing package; all bad names are reported, then the +step fails. Check out the repository first, and grant the job `packages: read`. + +```yaml +- uses: actions/checkout@ + with: + persist-credentials: false +- id: discover + uses: OpenCHAMI/github-actions/actions/discover-ghcr-images@ + # with: + # images: foo foo-init +- run: echo "${{ steps.discover.outputs.names }} (from ${{ steps.discover.outputs.source }})" +``` + +Outputs `names` (space-separated, without registry or owner) and `source` +(`input`, `goreleaser`, or `repo`). diff --git a/actions/discover-ghcr-images/action.yml b/actions/discover-ghcr-images/action.yml new file mode 100644 index 0000000..44afa7a --- /dev/null +++ b/actions/discover-ghcr-images/action.yml @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: 'Discover GHCR images' +author: 'OpenCHAMI' +branding: { icon: 'search', color: 'blue' } +description: 'Finds and verifies the GHCR package names for a repository' + +inputs: + images: + description: 'Space- or newline-separated GHCR package names (overrides discovery)' + required: false + default: '' + +outputs: + names: + description: 'Space-separated GHCR package names (without registry or owner)' + value: ${{ steps.verify.outputs.names }} + source: + description: 'Where the names came from: input, goreleaser, or repo' + value: ${{ steps.source.outputs.source }} + +runs: + using: composite + steps: + - name: Select image source + id: source + shell: bash + env: + IMAGES: ${{ inputs.images }} + run: | + set -euo pipefail + config="" + # Same lookup order as GoReleaser itself. + for f in .config/goreleaser.yml .config/goreleaser.yaml .goreleaser.yml .goreleaser.yaml goreleaser.yml goreleaser.yaml; do + [[ -f "$f" ]] && { config=$f; break; } + done + if [[ -n "$IMAGES" ]]; then + source=input + elif [[ -n "$config" ]] && grep -q '^dockers_v2:' "$config"; then + source=goreleaser + else + source=repo + fi + echo "Image names from: ${source}" + { + echo "source=${source}" + echo "config=${config}" + } >> "$GITHUB_OUTPUT" + + - name: Read image names from input + id: input + if: ${{ steps.source.outputs.source == 'input' }} + shell: bash + env: + IMAGES: ${{ inputs.images }} + run: echo "names=$(xargs <<< "$IMAGES")" >> "$GITHUB_OUTPUT" + + - name: Read image names from GoReleaser config + id: goreleaser + if: ${{ steps.source.outputs.source == 'goreleaser' }} + shell: bash + env: + CONFIG: ${{ steps.source.outputs.config }} + run: | + set -euo pipefail + names=$(yq -r '.dockers_v2[].images[] | select(test("^ghcr\\.io/")) | sub("^ghcr\\.io/[^/]+/"; "")' "$CONFIG" | xargs) + if [[ -z "$names" ]]; then + echo "::error::No ghcr.io images in ${CONFIG} dockers_v2. Set the images input." + exit 1 + fi + if [[ "$names" == *"{{"* ]]; then + echo "::error::Templated image names in ${CONFIG} can't be resolved. Set the images input." + exit 1 + fi + echo "names=${names}" >> "$GITHUB_OUTPUT" + + - name: Guess image name from repository + id: repo + if: ${{ steps.source.outputs.source == 'repo' }} + shell: bash + env: + REPO_NAME: ${{ github.event.repository.name }} + run: echo "names=${REPO_NAME,,}" >> "$GITHUB_OUTPUT" + + - name: Check packages exist + id: verify + shell: bash + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + SOURCE: ${{ steps.source.outputs.source }} + NAMES: ${{ steps.input.outputs.names || steps.goreleaser.outputs.names || steps.repo.outputs.names }} + run: | + set -euo pipefail + # shellcheck disable=SC2086 # intentional word splitting + names=$(printf '%s\n' $NAMES | sort -u | xargs) + failed=false + for package in $names; do + if ! gh api "/orgs/${OWNER}/packages/container/${package}" --silent; then + echo "::error::No ghcr.io package '${package}' (name from ${SOURCE}). Set the images input to the right package names." + failed=true + fi + done + [[ "$failed" == false ]] || exit 1 + echo "names=${names}" >> "$GITHUB_OUTPUT" diff --git a/actions/gpg-configure-release-keys/README.md b/actions/gpg-configure-release-keys/README.md index c35f50d..bd047ee 100644 --- a/actions/gpg-configure-release-keys/README.md +++ b/actions/gpg-configure-release-keys/README.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: MIT # gpg-configure-release-keys +> [!CAUTION] +> **Deprecated.** Replaced by nfpm package signing in build-release-goreleaser.yml (gpg-key secret). Kept only for callers pinned to older refs; it will be removed in a future major release. + Creates a per-run ephemeral GPG key and certifies it with a repo-scoped certification key, for use by downstream signing steps. diff --git a/actions/gpg-configure-release-keys/action.yml b/actions/gpg-configure-release-keys/action.yml index 51b1f02..6ac3e56 100644 --- a/actions/gpg-configure-release-keys/action.yml +++ b/actions/gpg-configure-release-keys/action.yml @@ -1,7 +1,15 @@ # SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT - -name: 'configure gpg release keys for downstream tasks' +# +# ############################################################################ +# # DEPRECATED ACTION -- DO NOT USE IN NEW CALLERS +# # +# # gpg-configure-release-keys is replaced by nfpm package signing in build-release-goreleaser.yml (gpg-key secret). +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ + +name: '[DEPRECATED] configure gpg release keys for downstream tasks' author: 'OpenCHAMI' branding: icon: 'lock' @@ -72,6 +80,10 @@ outputs: runs: using: "composite" steps: + - name: Deprecation warning + shell: bash + run: | + echo "::warning title=Deprecated action::gpg-configure-release-keys is deprecated; migrate to nfpm package signing in build-release-goreleaser.yml (gpg-key secret)." - id: setup shell: bash run: | diff --git a/actions/gpg-sign-rpm/README.md b/actions/gpg-sign-rpm/README.md index 3b61fdb..f166c1a 100644 --- a/actions/gpg-sign-rpm/README.md +++ b/actions/gpg-sign-rpm/README.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: MIT # RPM Signing Action +> [!CAUTION] +> **Deprecated.** Replaced by nfpm package signing in build-release-goreleaser.yml (gpg-key secret). Kept only for callers pinned to older refs; it will be removed in a future major release. + 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 diff --git a/actions/gpg-sign-rpm/action.yml b/actions/gpg-sign-rpm/action.yml index 16ab580..c5a16da 100644 --- a/actions/gpg-sign-rpm/action.yml +++ b/actions/gpg-sign-rpm/action.yml @@ -1,7 +1,15 @@ # SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT +# +# ############################################################################ +# # DEPRECATED ACTION -- DO NOT USE IN NEW CALLERS +# # +# # gpg-sign-rpm is replaced by nfpm package signing in build-release-goreleaser.yml (gpg-key secret). +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ -name: 'Sign RPM Package' +name: '[DEPRECATED] Sign RPM Package' author: 'OpenCHAMI' branding: icon: 'package' @@ -27,6 +35,10 @@ outputs: runs: using: composite steps: + - name: Deprecation warning + shell: bash + run: | + echo "::warning title=Deprecated action::gpg-sign-rpm is deprecated; migrate to nfpm package signing in build-release-goreleaser.yml (gpg-key secret)." - name: Install RPM tools shell: bash run: | diff --git a/actions/gpg-verify-trust-chain/README.md b/actions/gpg-verify-trust-chain/README.md index 201ed28..20408b8 100644 --- a/actions/gpg-verify-trust-chain/README.md +++ b/actions/gpg-verify-trust-chain/README.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: MIT # gpg-verify-trust-chain +> [!CAUTION] +> **Deprecated.** Replaced by the rpm -K signature check in validate-packages.yml. Kept only for callers pinned to older refs; it will be removed in a future major release. + 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. diff --git a/actions/gpg-verify-trust-chain/action.yml b/actions/gpg-verify-trust-chain/action.yml index 03b33a8..399c3b7 100644 --- a/actions/gpg-verify-trust-chain/action.yml +++ b/actions/gpg-verify-trust-chain/action.yml @@ -1,7 +1,15 @@ # SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT +# +# ############################################################################ +# # DEPRECATED ACTION -- DO NOT USE IN NEW CALLERS +# # +# # gpg-verify-trust-chain is replaced by the rpm -K signature check in validate-packages.yml. +# # It is kept only so callers pinned to older refs keep working, and will be +# # removed in a future major release. See README.md for migration notes. +# ############################################################################ -name: 'Verify gpg trust chain' +name: '[DEPRECATED] 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: @@ -38,6 +46,10 @@ outputs: runs: using: 'composite' steps: + - name: Deprecation warning + shell: bash + run: | + echo "::warning title=Deprecated action::gpg-verify-trust-chain is deprecated; migrate to the rpm -K signature check in validate-packages.yml." - name: Install dependencies shell: bash run: | diff --git a/renovate.json b/renovate.json index bcbbf00..bb200d9 100644 --- a/renovate.json +++ b/renovate.json @@ -18,6 +18,15 @@ ], "depNameTemplate": "renovate", "datasourceTemplate": "npm" + }, + { + "customType": "regex", + "description": "Digest-pinned container image defaults in reusable workflow inputs", + "managerFilePatterns": ["/^\\.github/workflows/[^/]+\\.ya?ml$/"], + "matchStrings": [ + "default: '(?docker\\.io/[^:']+):(?[^@']+)@(?sha256:[a-f0-9]{64})'" + ], + "datasourceTemplate": "docker" } ] }