diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..04f1904 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +# actionlint doesn't recognize GitHub's self-repository `uses: $/...` syntax +# yet. Remove this once it does. +paths: + .github/workflows/0-local-ci.yml: + ignore: + - 'reusable workflow call "\$/' diff --git a/.github/workflows/0-local-ci.yml b/.github/workflows/0-local-ci.yml new file mode 100644 index 0000000..0dd0ef1 --- /dev/null +++ b/.github/workflows/0-local-ci.yml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: 0-local-ci + +# Local to this repo: CI for github-actions itself. Not a reusable workflow; +# do not call it from other repos. See "Workflow naming" in the README. + +on: + pull_request: + push: + branches: [ main ] + +permissions: + contents: read # baseline for checkout + +jobs: + lint: + uses: $/.github/workflows/lint-ci.yml + permissions: + contents: read # baseline for checkout + security-events: write # zizmor SARIF upload to GHAS + + lint-renovate: + uses: $/.github/workflows/lint-renovate.yml 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 1882568..34a25f2 100644 --- a/.github/workflows/build-publish-container-goreleaser.yml +++ b/.github/workflows/build-publish-container-goreleaser.yml @@ -1,44 +1,52 @@ # 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. -name: Build and publish container using goreleaser +name: build-publish-container-goreleaser on: workflow_call: inputs: - build_deps: + build-deps: type: string required: false description: 'Space-separated list of apt packages to install before building (e.g. "gcc-aarch64-linux-gnu libc6-dev-arm64-cross").' - cgo_enabled: + cgo-enabled: type: number required: false default: 0 - go_version: + go-version: type: string required: false - description: 'Go version to use (e.g. "1.26.7", "stable"). Mutually exclusive with go_version_file.' - go_version_file: + description: 'Go version to use (e.g. "1.26.7", "stable"). Mutually exclusive with go-version-file.' + go-version-file: type: string required: false - description: 'Path to a go.mod or .go-version file containing the Go version. Mutually exclusive with go_version.' - release_draft: + description: 'Path to a go.mod or .go-version file containing the Go version. Mutually exclusive with go-version.' + release-draft: type: boolean required: false default: false description: 'Create the release as a draft, overriding release.draft in .goreleaser.yml' - is_pr_build: + is-pr-build: type: boolean required: false default: false - pr_number: + pr-number: type: number required: false default: ${{ github.event.number || 0 }} - registry_subject_name: + registry-subject-name: type: string required: true @@ -49,18 +57,21 @@ permissions: attestations: write # write build provenance attestations jobs: - container_build_publish: + 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: fetch-tags: true fetch-depth: 0 - name: Install build dependencies - if: ${{ inputs.build_deps != '' }} + if: ${{ inputs.build-deps != '' }} env: - BUILD_DEPS: ${{ inputs.build_deps }} + BUILD_DEPS: ${{ inputs.build-deps }} run: | sudo apt update # shellcheck disable=SC2086 # intentional word splitting @@ -68,8 +79,8 @@ jobs: - name: Set up Go uses: actions/setup-go@v6.4.0 with: - go-version: ${{ inputs.go_version || (inputs.go_version_file == '' && 'stable' || '') }} - go-version-file: ${{ inputs.go_version_file || '' }} + go-version: ${{ inputs.go-version || (inputs.go-version-file == '' && 'stable' || '') }} + go-version-file: ${{ inputs.go-version-file || '' }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -107,17 +118,17 @@ jobs: 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 }}" + 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 }} + 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" + git tag -f -a pr-${{ inputs.pr-number }} -m "PR Release" - name: Generate release notes - if: ${{ !inputs.is_pr_build }} + if: ${{ !inputs.is-pr-build }} env: GITHUB_TOKEN: ${{ github.token }} run: gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" -F tag_name="${{ github.ref_name }}" --jq .body > ../notes.md @@ -127,11 +138,11 @@ jobs: GITHUB_TOKEN: ${{ github.token }} with: version: '~> 2' - args: release --clean ${{ inputs.is_pr_build && '--skip=announce,validate,archive' || '' }}${{ env.SKIP_CONTAINER_PUBLISH == 'true' && (inputs.is_pr_build && ',publish' || '--skip=publish') || '' }} ${{ inputs.release_draft && '--draft' || '' }} ${{ !inputs.is_pr_build && '--release-notes ../notes.md' || '' }} + args: release --clean ${{ inputs.is-pr-build && '--skip=announce,validate,archive' || '' }}${{ env.SKIP_CONTAINER_PUBLISH == 'true' && (inputs.is-pr-build && ',publish' || '--skip=publish') || '' }} ${{ inputs.release-draft && '--draft' || '' }} ${{ !inputs.is-pr-build && '--release-notes ../notes.md' || '' }} id: goreleaser - name: Process goreleaser output if: env.SKIP_CONTAINER_PUBLISH == 'false' - id: process_goreleaser_output + id: process-goreleaser-output run: | node - <<'EOF' const fs = require('fs'); @@ -149,14 +160,14 @@ jobs: echo "digest=$(cat digest.txt)" >> "${GITHUB_OUTPUT}" fi - name: Attest Binaries - if: ${{ (env.SKIP_CONTAINER_PUBLISH == 'false') && (inputs.is_pr_build == false) }} + if: ${{ (env.SKIP_CONTAINER_PUBLISH == 'false') && (inputs.is-pr-build == false) }} uses: actions/attest-build-provenance@v4.1.0 with: subject-path: dist/** - name: generate build provenance - if: ${{ (env.SKIP_CONTAINER_PUBLISH == 'false') && (inputs.is_pr_build == false) }} + if: ${{ (env.SKIP_CONTAINER_PUBLISH == 'false') && (inputs.is-pr-build == false) }} uses: actions/attest-build-provenance@v4.1.0 with: - subject-name: ${{ inputs.registry_subject_name }} - subject-digest: ${{ steps.process_goreleaser_output.outputs.digest }} + subject-name: ${{ inputs.registry-subject-name }} + subject-digest: ${{ steps.process-goreleaser-output.outputs.digest }} push-to-registry: true diff --git a/.github/workflows/build-release-goreleaser.yml b/.github/workflows/build-release-goreleaser.yml new file mode 100644 index 0000000..03fce7f --- /dev/null +++ b/.github/workflows/build-release-goreleaser.yml @@ -0,0 +1,331 @@ +# 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 }} + images: + description: 'Published images as a JSON array of {name, digest}; empty on PRs from forks' + value: ${{ jobs.build-release-goreleaser.outputs.images }} + +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 }} + 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") + # 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") + # Fork PRs build locally; nothing was pushed. + [[ "$SKIP_PUBLISH" == true ]] && images='[]' + { + echo "version=${version}" + echo "images=${images:-[]}" + } >> "$GITHUB_OUTPUT" + echo "version=${version} images=${images:-[]}" + - 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 4004cab..09074d8 100644 --- a/.github/workflows/build-rpm-quadlet.yml +++ b/.github/workflows/build-rpm-quadlet.yml @@ -1,11 +1,18 @@ -# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# 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. -name: Build RPM for Podman Quadlet Files -run-name: Create Podman Quadlet RPM for ${{ github.ref }} +name: build-rpm-quadlet on: workflow_call: inputs: @@ -18,19 +25,22 @@ permissions: contents: read # baseline for checkout jobs: - rpmbuild: + rpm-build: runs-on: ubuntu-latest 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 tar gzip + run: dnf install -y -q git make rpm-build rpmlint - 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 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-tags: true fetch-depth: 0 @@ -39,7 +49,7 @@ jobs: run: make rpm-build - name: Upload RPM - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.artifact-name-unsigned-rpms }} path: '**/*.rpm' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index c843c63..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC -# SPDX-License-Identifier: MIT - -name: CI - -on: - pull_request: - push: - branches: [ main ] - -permissions: - contents: read # baseline for checkout - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6.0.3 - - name: actionlint - uses: raven-actions/actionlint@v2.1.2 diff --git a/.github/workflows/coverage-go.yml b/.github/workflows/coverage-go.yml deleted file mode 100644 index 517d731..0000000 --- a/.github/workflows/coverage-go.yml +++ /dev/null @@ -1,68 +0,0 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC -# SPDX-License-Identifier: MIT - -name: coverage-go - -# Reusable workflow that produces a Go coverage profile, writes the total -# to the job summary, and uploads the profile to Coveralls. -# -# The caller repo must be enrolled in Coveralls; the upload authenticates -# with the automatically-provided GITHUB_TOKEN. - -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 - coverage-command: - description: 'Command that writes the coverage profile to `coverage-file`' - required: false - type: string - default: 'go test -coverprofile=coverage.out ./...' - coverage-file: - description: 'Path to the coverage profile written by `coverage-command`' - required: false - type: string - default: 'coverage.out' - -permissions: - contents: read # baseline for checkout - -jobs: - coverage-go: - name: coverage-go - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version: ${{ inputs.go_version || (inputs.go_version_file == '' && 'stable' || '') }} - go-version-file: ${{ inputs.go_version_file || '' }} - - - name: Run tests with coverage - env: - COVERAGE_COMMAND: ${{ inputs.coverage-command }} - run: eval "$COVERAGE_COMMAND" - - - name: Report total coverage - env: - COVERAGE_FILE: ${{ inputs.coverage-file }} - run: | - total=$(go tool cover -func="$COVERAGE_FILE" | awk '/^total:/ {print $3}') - echo "Total coverage: ${total}" | tee -a "$GITHUB_STEP_SUMMARY" - - - name: Upload coverage to Coveralls - uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8 - with: - file: ${{ inputs.coverage-file }} - format: golang - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1cf93e0..1543a86 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT name: dependency-review @@ -41,10 +41,9 @@ permissions: jobs: dependency-review: - name: dependency-review - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/docker-build-release.yml b/.github/workflows/docker-build-release.yml index 53e1e62..3f96976 100644 --- a/.github/workflows/docker-build-release.yml +++ b/.github/workflows/docker-build-release.yml @@ -1,8 +1,15 @@ # 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: Build image -run-name: Dockeer image build for ${{ github.event.push.ref }} +name: docker-build-release on: workflow_call: @@ -64,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: @@ -128,7 +138,7 @@ jobs: registry: ghcr.io - name: Build and push image - id: docker_build + id: docker-build uses: docker/build-push-action@v7.2.0 with: push: true @@ -153,14 +163,17 @@ jobs: ${{ env.CC != '' && format('"CC={0}"', env.CC) || '' }} publish-release: - runs-on: ubuntu-latest + runs-on: ubuntu-slim needs: build-push-images if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 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 + id: semver-parser uses: booxmedialtd/ws-action-parse-semver@v1.4.7 with: input_string: ${{ github.event.ref }} @@ -170,5 +183,5 @@ jobs: # by default this will use the tag push tag as the tag and name # if we want to trigger tagging from the workflow, "tag" and "commit" # need to be set to create a new one - prerelease: ${{ steps.semver_parser.outputs.prerelease != '' }} + prerelease: ${{ steps.semver-parser.outputs.prerelease != '' }} skipIfReleaseExists: true diff --git a/.github/workflows/go-build-release.yml b/.github/workflows/go-build-release.yml index 9cee8fe..c9db3bc 100644 --- a/.github/workflows/go-build-release.yml +++ b/.github/workflows/go-build-release.yml @@ -1,8 +1,15 @@ # 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: GoReleaser -run-name: GoReleaser ${{ (inputs.snapshot == 'true' || (inputs.snapshot != 'false' && !startsWith(github.ref, 'refs/tags/v'))) && 'Snapshot' || 'Release' }} +name: go-build-release on: workflow_call: @@ -82,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: @@ -140,7 +150,7 @@ jobs: run: ${{ inputs.pre-build-commands }} - name: Determine snapshot flags - id: snapshot_flags + id: snapshot-flags run: | if [[ "${{ inputs.snapshot }}" == "true" ]]; then echo "flags=--snapshot" >> "$GITHUB_OUTPUT" @@ -156,18 +166,18 @@ jobs: fi fi - - name: GoReleaser ${{ inputs.goreleaser-args }} ${{ steps.snapshot_flags.outputs.flags }} + - name: GoReleaser ${{ inputs.goreleaser-args }} ${{ steps.snapshot-flags.outputs.flags }} id: goreleaser uses: goreleaser/goreleaser-action@v7.2.2 env: GITHUB_TOKEN: ${{ github.token }} with: version: ${{ inputs.goreleaser-version }} - args: ${{ inputs.goreleaser-args }} ${{ steps.snapshot_flags.outputs.flags }} + args: ${{ inputs.goreleaser-args }} ${{ steps.snapshot-flags.outputs.flags }} - name: Process GoReleaser output - id: process_goreleaser_output - if: ${{ !contains(steps.snapshot_flags.outputs.flags, '--snapshot') }} + id: process-goreleaser-output + if: ${{ !contains(steps.snapshot-flags.outputs.flags, '--snapshot') }} run: | { echo "const fs = require('fs');" @@ -186,9 +196,9 @@ jobs: subject-path: ${{ inputs.attestation-binary-path }} - name: Generate build provenance for container - if: ${{ !contains(steps.snapshot_flags.outputs.flags, '--snapshot') && steps.process_goreleaser_output.outputs.digest != '' && steps.process_goreleaser_output.outputs.digest != 'undefined' }} + if: ${{ !contains(steps.snapshot-flags.outputs.flags, '--snapshot') && steps.process-goreleaser-output.outputs.digest != '' && steps.process-goreleaser-output.outputs.digest != 'undefined' }} uses: actions/attest-build-provenance@v4.1.0 with: subject-name: ${{ inputs.registry-name }} - subject-digest: ${{ steps.process_goreleaser_output.outputs.digest }} + subject-digest: ${{ steps.process-goreleaser-output.outputs.digest }} push-to-registry: true diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 7652753..d11896b 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT name: govulncheck @@ -29,13 +29,12 @@ permissions: jobs: govulncheck: - name: govulncheck - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 + - uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0 with: go-version-file: ${{ inputs.go-version == '' && 'go.mod' || '' }} go-version-input: ${{ inputs.go-version }} diff --git a/.github/workflows/gpg-sign-artifacts.yml b/.github/workflows/gpg-sign-artifacts.yml index 0ac1222..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. @@ -8,8 +16,7 @@ # Keep this file up to date and maintained as we add other package signing # tasks (e.g. .deb, Arch packages). -name: GPG Sign artifacts -run-name: Create signed artifacts for ${{ github.ref }} +name: gpg-sign-artifacts on: workflow_call: inputs: @@ -35,10 +42,13 @@ 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: | - dnf install -y -q git make rpm-build rpmlint tar gzip + dnf install -y -q git rpmlint - name: Mark workspace as a safe git directory run: git config --global --add safe.directory "$GITHUB_WORKSPACE" diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-ci.yml similarity index 57% rename from .github/workflows/lint-workflows.yml rename to .github/workflows/lint-ci.yml index 49d23de..39b9653 100644 --- a/.github/workflows/lint-workflows.yml +++ b/.github/workflows/lint-ci.yml @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT -name: lint-workflows +name: lint-ci # Reusable workflow that lints GitHub Actions workflow files in the caller repo. # Runs actionlint (syntax / shellcheck / action-version sanity) and zizmor @@ -15,24 +15,22 @@ permissions: jobs: actionlint: - name: actionlint - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2 + - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 with: flags: -color zizmor: - name: zizmor runs-on: ubuntu-latest permissions: contents: read # baseline for checkout security-events: write # for SARIF upload to GHAS steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + - uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 diff --git a/.github/workflows/lint-codegen-fabrica.yml b/.github/workflows/lint-codegen-fabrica.yml new file mode 100644 index 0000000..b0416a4 --- /dev/null +++ b/.github/workflows/lint-codegen-fabrica.yml @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: lint-codegen-fabrica + +# Reusable workflow that fails when a Fabrica project's committed generated +# code has drifted, by running the caller's `make generate-check` with a +# Fabrica built from source (passed as LOCAL_FABRICA). Fabrica stamps its +# version into generated code and only a source build carries that version; +# a `go run` build reports "dev" and always fails the check. + +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 + fabrica-ref: + description: 'Fabrica tag, branch, or commit SHA to build (default: version in go.mod)' + required: false + type: string + default: '' + +permissions: + contents: read # baseline for checkout + +jobs: + lint-codegen-fabrica: + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - 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 || '' }} + + - name: Resolve Fabrica ref + id: fabrica-ref + env: + FABRICA_REF: ${{ inputs.fabrica-ref }} + run: | + version="$FABRICA_REF" + if [[ -z "$version" ]]; then + version=$(go mod edit -json | jq -r ' + .Require[]? + | select(.Path == "github.com/openchami/fabrica") + | .Version + ') + if [[ -z "$version" ]]; then + echo "::error::fabrica-ref not set and no github.com/openchami/fabrica version found in go.mod" + exit 1 + fi + fi + # Resolve the tag, branch, commit, or pseudo-version to the commit it names + if ! ref=$(go list -mod=mod -m -json "github.com/openchami/fabrica@${version}" | + jq -er '.Origin.Hash | select(type == "string" and test("^[0-9a-f]{40}$"))'); then + echo "::error::Could not resolve Fabrica ref: ${version}" + exit 1 + fi + echo "Using Fabrica ref: ${ref}" + echo "ref=${ref}" >> "$GITHUB_OUTPUT" + - name: Checkout Fabrica + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: openchami/fabrica + ref: ${{ steps.fabrica-ref.outputs.ref }} + path: fabrica + fetch-depth: 0 # full history + tags so `git describe` stamps the same version as a local build + persist-credentials: false + - name: Build Fabrica + working-directory: fabrica + run: make build + - name: Verify generated code is committed + run: | + make generate-check \ + LOCAL_FABRICA="$GITHUB_WORKSPACE/fabrica" \ + FABRICA_SOURCE_ARG="--fabrica-source $GITHUB_WORKSPACE/fabrica --force" diff --git a/.github/workflows/lint-go.yml b/.github/workflows/lint-go.yml index b7534dc..960a2df 100644 --- a/.github/workflows/lint-go.yml +++ b/.github/workflows/lint-go.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT name: lint-go @@ -7,17 +7,17 @@ name: lint-go # and separately verifies go.mod/go.sum are tidy by running the tidy # command and failing on a dirty diff. # -# Pair with test-go for the unit-test half of the Go CI wave. +# Pair with test-unit-go for the unit-test half of the Go CI wave. on: workflow_call: inputs: - go_version: - description: 'Go version to use (e.g. "1.26.7", "stable"). Mutually exclusive with go_version_file.' + 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.' + 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 golangci-lint-version: @@ -31,31 +31,29 @@ permissions: jobs: golangci-lint: - name: golangci-lint - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - 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 || '' }} + go-version: ${{ inputs.go-version || (inputs.go-version-file == '' && 'stable' || '') }} + go-version-file: ${{ inputs.go-version-file || '' }} - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: version: ${{ inputs.golangci-lint-version }} modules: - name: modules - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - 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 || '' }} + go-version: ${{ inputs.go-version || (inputs.go-version-file == '' && 'stable' || '') }} + go-version-file: ${{ inputs.go-version-file || '' }} - name: Check go.mod and go.sum are tidy run: | go mod tidy diff --git a/.github/workflows/lint-renovate.yml b/.github/workflows/lint-renovate.yml new file mode 100644 index 0000000..00bbcd8 --- /dev/null +++ b/.github/workflows/lint-renovate.yml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: lint-renovate + +# Reusable workflow that validates the caller's Renovate config with +# renovate-config-validator. With no config-file, the validator checks every +# config file it finds in the repository (renovate.json, .github/renovate.json, +# renovate.json5, .renovaterc, ...). + +on: + workflow_call: + inputs: + renovate-version: + description: 'Renovate npm version that provides renovate-config-validator' + required: false + type: string + default: '44.115.13' + config-file: + description: 'Config file to validate (default: every Renovate config file found in the repo)' + required: false + type: string + default: '' + +permissions: + contents: read # baseline for checkout + +jobs: + lint-renovate: + runs-on: ubuntu-slim + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + - name: Validate Renovate config + env: + RENOVATE_VERSION: ${{ inputs.renovate-version }} + CONFIG_FILE: ${{ inputs.config-file }} + run: | + # --no-global: validate as repository config, not self-hosted config. + npx --yes --package "renovate@${RENOVATE_VERSION}" -- \ + renovate-config-validator --no-global ${CONFIG_FILE:+"$CONFIG_FILE"} diff --git a/.github/workflows/pr-registry-cleanup.yml b/.github/workflows/pr-registry-cleanup.yml index bc3934f..ee7be5c 100644 --- a/.github/workflows/pr-registry-cleanup.yml +++ b/.github/workflows/pr-registry-cleanup.yml @@ -1,11 +1,10 @@ -# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT # # Reusable workflow: deletes the GHCR container image versions a pull request # published, once that pull request closes. -name: Cleanup PR Container Images -run-name: Cleanup container images for PR ${{ inputs.pr-number }} +name: pr-registry-cleanup on: workflow_call: inputs: @@ -23,7 +22,7 @@ permissions: jobs: cleanup: - runs-on: ubuntu-latest + runs-on: ubuntu-slim timeout-minutes: 10 steps: - name: Resolve PR number diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 50e0c79..7a52ba6 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,27 +1,42 @@ -# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT # # Reusable workflow: publishes the draft GitHub Release for a tag once every # upstream job has finished attaching its artifacts. -name: Publish release -run-name: Publish release for ${{ github.ref }} +name: publish-release permissions: - contents: write + contents: write # publish the draft GitHub Release on: workflow_call: inputs: - make_latest: - description: 'Mark the published release as the latest release. Use "legacy" to defer to GitHub' + make-latest: + description: 'Mark the published release as the latest release: true, false, or legacy (GitHub picks by date and semver)' type: string - default: 'true' + # see this comment for details: + # https://github.com/OpenCHAMI/github-actions/pull/38#discussion_r4213262488 + default: 'legacy' + required: false + draft: + description: 'Leave the release in draft state' + type: boolean + default: false required: false jobs: publish-release: - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Publish release - uses: softprops/action-gh-release@v3.0.2 - with: - tag_name: ${{ github.ref_name }} - draft: false - make_latest: ${{ inputs.make_latest }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + DRAFT: ${{ inputs.draft }} + MAKE_LATEST: ${{ inputs.make-latest }} + run: | + release_id=$(gh release view "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --json databaseId --jq '.databaseId') + + gh api --method PATCH \ + "repos/$GITHUB_REPOSITORY/releases/$release_id" \ + -F "draft=$DRAFT" \ + -f "make_latest=$MAKE_LATEST" diff --git a/.github/workflows/release-signed-artifacts.yml b/.github/workflows/release-signed-artifacts.yml index 981daf9..0f64989 100644 --- a/.github/workflows/release-signed-artifacts.yml +++ b/.github/workflows/release-signed-artifacts.yml @@ -1,11 +1,18 @@ # 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. -name: Release signed artifacts -run-name: Generate release with signed artifacts for ${{ github.ref }} +name: release-signed-artifacts permissions: contents: write # create GitHub Release and upload artifacts on: @@ -21,32 +28,28 @@ on: type: string default: 'public-keys' required: false - append_body: + append-body: description: 'Append the verification instructions to the existing release body instead of replacing it' type: boolean default: true required: false - release_draft: + release-draft: description: 'Leave the release in draft state' type: boolean default: false required: false - release_name: + release-name: description: 'Release title. When empty, the existing release name is kept' type: string default: '' required: false jobs: artifacts-release: - runs-on: ubuntu-latest - container: - image: rockylinux:9 + runs-on: ubuntu-slim steps: - - name: Install dependencies + - name: Deprecation warning 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" + 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: @@ -66,9 +69,9 @@ jobs: uses: softprops/action-gh-release@v3.0.2 with: tag_name: ${{ github.ref_name }} - name: ${{ inputs.release_name }} - draft: ${{ inputs.release_draft }} - append_body: ${{ inputs.append_body }} + name: ${{ inputs.release-name }} + draft: ${{ inputs.release-draft }} + append_body: ${{ inputs.append-body }} fail_on_unmatched_files: true files: | dist/rpms/**/*.rpm diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index 33a6712..ab74536 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT name: reuse @@ -10,7 +10,7 @@ name: reuse on: workflow_call: inputs: - reuse_version: + reuse-version: type: string required: false default: '6.2.0' @@ -21,14 +21,13 @@ permissions: jobs: reuse: - name: reuse - runs-on: ubuntu-latest + runs-on: ubuntu-slim timeout-minutes: 10 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: REUSE compliance check env: - REUSE_VERSION: ${{ inputs.reuse_version }} + REUSE_VERSION: ${{ inputs.reuse-version }} run: pipx run --backend pip --spec "reuse==$REUSE_VERSION" reuse lint --lines diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3599233..8ee8c54 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT name: scorecard @@ -21,11 +21,10 @@ permissions: jobs: scorecard: - name: scorecard # Scorecard is a container action and needs a full-fat runner. runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -45,7 +44,7 @@ jobs: - name: Upload SARIF to GHAS if: github.event_name != 'pull_request' - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: sarif_file: results.sarif category: scorecard diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..adad710 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: stale + +# Reusable workflow that marks and closes inactive issues and PRs with the +# org-default actions/stale policy. Callers own the schedule (a reusable +# workflow can't schedule itself). + +on: + workflow_call: + inputs: + days-before-stale: + description: 'Days of inactivity before an issue or PR is marked stale' + required: false + type: number + default: 35 + days-before-close: + description: 'Days after being marked stale before an issue or PR is closed' + required: false + type: number + default: 7 + stale-issue-label: + description: 'Label applied to stale issues' + required: false + type: string + default: 'stale' + stale-pr-label: + description: 'Label applied to stale PRs' + required: false + type: string + default: 'stale' + exempt-issue-labels: + description: 'Comma-separated labels that exempt an issue from going stale' + required: false + type: string + default: 'pinned,security,help wanted' + exempt-pr-labels: + description: 'Comma-separated labels that exempt a PR from going stale' + required: false + type: string + default: 'pinned,security' + operations-per-run: + description: 'Maximum GitHub API operations per run' + required: false + type: number + default: 500 + stale-issue-message: + description: 'Comment posted when an issue is marked stale' + required: false + type: string + default: >- + This issue has been automatically marked as stale due to inactivity. + Please add a comment or label to keep it open; otherwise it will close in 7 days. + close-issue-message: + description: 'Comment posted when a stale issue is closed' + required: false + type: string + default: 'Closing due to inactivity. Feel free to reopen with new information.' + stale-pr-message: + description: 'Comment posted when a PR is marked stale' + required: false + type: string + default: 'This PR has been marked stale due to inactivity. Comment or push changes to keep it open.' + close-pr-message: + description: 'Comment posted when a stale PR is closed' + required: false + type: string + default: 'Closing due to inactivity. Please reopen when it’s ready again.' + +permissions: + issues: write # label, comment on, and close stale issues + pull-requests: write # label, comment on, and close stale PRs + +jobs: + stale: + runs-on: ubuntu-slim + steps: + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 + with: + days-before-stale: ${{ inputs.days-before-stale }} + days-before-close: ${{ inputs.days-before-close }} + stale-issue-label: ${{ inputs.stale-issue-label }} + stale-pr-label: ${{ inputs.stale-pr-label }} + exempt-issue-labels: ${{ inputs.exempt-issue-labels }} + exempt-pr-labels: ${{ inputs.exempt-pr-labels }} + operations-per-run: ${{ inputs.operations-per-run }} + remove-stale-when-updated: true + stale-issue-message: ${{ inputs.stale-issue-message }} + close-issue-message: ${{ inputs.close-issue-message }} + stale-pr-message: ${{ inputs.stale-pr-message }} + close-pr-message: ${{ inputs.close-pr-message }} diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml deleted file mode 100644 index d30dd34..0000000 --- a/.github/workflows/test-go.yml +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC -# SPDX-License-Identifier: MIT - -name: test-go - -# Reusable workflow that runs the caller's Go unit tests. Fetches tags so -# tests that assert on version metadata derived from `git describe` behave -# the same as they do locally. - -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 - test-command: - description: 'Command that runs the tests' - required: false - type: string - default: 'go test -race ./...' - -permissions: - contents: read # baseline for checkout - -jobs: - test-go: - name: test-go - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-tags: true - persist-credentials: false - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version: ${{ inputs.go_version || (inputs.go_version_file == '' && 'stable' || '') }} - go-version-file: ${{ inputs.go_version_file || '' }} - - name: Run tests - env: - TEST_COMMAND: ${{ inputs.test-command }} - run: eval "$TEST_COMMAND" diff --git a/.github/workflows/test-unit-go.yml b/.github/workflows/test-unit-go.yml new file mode 100644 index 0000000..f87a6d1 --- /dev/null +++ b/.github/workflows/test-unit-go.yml @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: test-unit-go + +# Reusable workflow that runs the caller's Go unit tests, optionally writing a +# coverage profile, reporting the total, and uploading it to Coveralls. Fetches +# tags so tests that assert on version metadata derived from `git describe` +# behave the same as they do locally. +# +# With coverage enabled, the caller repo must be enrolled in Coveralls; the +# upload authenticates with the automatically-provided GITHUB_TOKEN. + +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 + # see comment for details: + # https://github.com/OpenCHAMI/github-actions/pull/38#discussion_r4213283911 + test-args: + description: 'Arguments passed to `go test` (flags and packages), one per line; do not add shell quotes' + required: false + type: string + default: |- + -race + ./... + coverage: + description: 'Write a coverage profile, report the total, and upload it to Coveralls' + required: false + type: boolean + default: false + coverage-file: + description: 'Path of the coverage profile written when coverage is true' + required: false + type: string + default: 'coverage.out' + +permissions: + contents: read # baseline for checkout + +jobs: + test-unit-go: + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-tags: true + persist-credentials: false + - 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 || '' }} + + - name: Run tests + env: + TEST_ARGS: ${{ inputs.test-args }} + COVERAGE: ${{ inputs.coverage }} + COVERAGE_FILE: ${{ inputs.coverage-file }} + run: | + test_args=() + while IFS= read -r arg || [[ -n "$arg" ]]; do + test_args+=("$arg") + done < <(printf '%s' "$TEST_ARGS") + + cover_args=() + if [[ "$COVERAGE" == 'true' ]]; then + cover_args=("-coverprofile=${COVERAGE_FILE}") + fi + go test "${cover_args[@]}" "${test_args[@]}" + + - name: Report total coverage + if: ${{ inputs.coverage }} + env: + COVERAGE_FILE: ${{ inputs.coverage-file }} + run: | + total=$(go tool cover -func="$COVERAGE_FILE" | awk '/^total:/ {print $3}') + echo "Total coverage: ${total}" | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Upload coverage to Coveralls + if: ${{ inputs.coverage }} + uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8 + with: + file: ${{ inputs.coverage-file }} + format: golang + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/trivy-image-scan.yml b/.github/workflows/trivy-image-scan.yml index 0edd45f..dd6a4e6 100644 --- a/.github/workflows/trivy-image-scan.yml +++ b/.github/workflows/trivy-image-scan.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: © 2025 OpenCHAMI a Series of LF Projects, LLC # SPDX-License-Identifier: MIT name: trivy-image-scan @@ -41,10 +41,9 @@ permissions: jobs: trivy: - name: trivy - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -60,7 +59,7 @@ jobs: - name: Upload SARIF to GHAS if: always() # upload findings even when scan fails the job - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: sarif_file: trivy-results.sarif category: trivy-image-scan 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 509292e..afeb38d 100644 --- a/.github/workflows/validate-rpm-quadlet.yml +++ b/.github/workflows/validate-rpm-quadlet.yml @@ -1,11 +1,18 @@ -# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# 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. -name: Validate Podman Quadlet RPM -run-name: Validate Podman Quadlet RPM for ${{ github.ref }} +name: validate-rpm-quadlet on: workflow_call: inputs: @@ -23,41 +30,38 @@ permissions: jobs: parse: - runs-on: ubuntu-latest + runs-on: ubuntu-slim 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: RPMS: ${{ inputs.rpms }} run: echo "matrix=$(yq -o=json -I=0 '.' <<< "$RPMS")" >> "$GITHUB_OUTPUT" - rpmvalidate: + rpm-validate: needs: parse name: validate (${{ matrix.rpm.name }}) - runs-on: ubuntu-latest + runs-on: ubuntu-slim strategy: fail-fast: false matrix: rpm: ${{ fromJSON(needs.parse.outputs.matrix) }} - container: - image: rockylinux:9 steps: - - - name: Install build dependencies - run: dnf install -y -q git rpmlint tar gzip diffutils jq - - - name: Mark workspace as a safe git directory - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - uses: actions/checkout@v6.0.2 + - 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 fetch-depth: 0 - name: Download signed RPM artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ inputs.artifact-name-signed-rpms }} path: dist/rpms 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 a7bbe1b..9899461 100644 --- a/README.md +++ b/README.md @@ -9,54 +9,169 @@ Reusable GitHub Actions for CI/CD. ## Structure -- `actions/gpg-ephemeral-key`: **Deprecated** - use `actions/gpg-configure-release-keys` instead -- `actions/gpg-configure-release-keys`: Generates and certifies a per-run ephemeral GPG key through the repo's release key chain -- `actions/gpg-sign-rpm`: RPM signing with ephemeral keys +- `actions/gpg-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/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-workflows.yml`: Reusable workflow that lints workflow files (actionlint + zizmor) +- `.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 -- `.github/workflows/test-go.yml`: Reusable workflow that runs Go unit tests -- `.github/workflows/coverage-go.yml`: Reusable workflow that reports Go coverage and uploads it to Coveralls +- `.github/workflows/test-unit-go.yml`: Reusable workflow that runs Go unit tests, optionally reporting coverage to Coveralls - `.github/workflows/reuse.yml`: Reusable workflow that checks REUSE copyright/licensing compliance - `.github/workflows/govulncheck.yml`: Reusable workflow that scans Go modules for known CVEs - `.github/workflows/dependency-review.yml`: Reusable workflow that gates PRs introducing CVE-flagged deps - `.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 + +### Workflow naming + +`0-local-*` workflows are this repo's own CI, not reusable workflows. The `0` +just sorts them to the top. ## Versioning & Usage -Use major version tags for stability: +Pin a release tag: ```yaml # For actions -- uses: OpenCHAMI/github-actions/actions/gpg-configure-release-keys@v3.8 -- uses: OpenCHAMI/github-actions/actions/gpg-sign-rpm@v3.8 +- 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@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/build-release-goreleaser.yml@v4.0 ``` -Pin a commit SHA internally for maximum supply-chain safety if desired. +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 `images` (published images as a JSON array of `{name, digest}`, `[]` on fork PRs). 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 -- Wraps `goreleaser-action` action with all .gorelease.yaml configurations +- Wraps `goreleaser-action` action with all .goreleaser.yaml configurations - Container image builds and publishing - Binary and container attestation/signing - Snapshot builds on pull requests @@ -76,7 +191,7 @@ on: jobs: goreleaser: name: GoReleaser ${{ startsWith(github.ref, 'refs/tags/v') && 'Release' || 'Snapshot' }} - uses: OpenCHAMI/github-actions/.github/workflows/go-build-release.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/go-build-release.yml@v4.0 with: pre-build-commands: | go install github.com/swaggo/swag/cmd/swag@latest @@ -87,7 +202,7 @@ jobs: See the [workflow](.github/workflows/go-build-release.yml) for additional input parameters. -### lint-workflows (Reusable Workflow) +### lint-ci (Reusable Workflow) Lints the caller repo's GitHub Actions workflow files. - **actionlint** - syntax validation, shellcheck on `run:` steps, deprecated-action checks. @@ -95,7 +210,7 @@ Lints the caller repo's GitHub Actions workflow files. **Usage:** ```yaml -name: Lint Workflows +name: Lint CI on: pull_request: push: @@ -103,11 +218,11 @@ on: jobs: lint: - uses: OpenCHAMI/github-actions/.github/workflows/lint-workflows.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/lint-ci.yml@v4.0 ``` ### lint-go (Reusable Workflow) -Lints the caller's Go module. Runs `golangci-lint`, and separately verifies `go.mod`/`go.sum` are tidy by running the tidy command and failing on a dirty diff. Uses Go `stable` unless the caller sets `go_version` or `go_version_file`. `golangci-lint` tracks `latest` unless pinned. +Lints the caller's Go module. Runs `golangci-lint`, and separately verifies `go.mod`/`go.sum` are tidy by running the tidy command and failing on a dirty diff. Uses Go `stable` unless the caller sets `go-version` or `go-version-file`. `golangci-lint` tracks `latest` unless pinned. **Usage:** ```yaml @@ -119,16 +234,17 @@ on: jobs: lint: - uses: OpenCHAMI/github-actions/.github/workflows/lint-go.yml@v3.9 + uses: OpenCHAMI/github-actions/.github/workflows/lint-go.yml@v4.0 # Optional overrides: # with: # golangci-lint-version: v2.13.2 - # go_version_file: go.mod - # tidy-command: make mod + # go-version-file: go.mod ``` -### test-go (Reusable Workflow) -Runs the caller's Go unit tests. Fetches tags so tests asserting on `git describe` version metadata behave as they do locally. +### test-unit-go (Reusable Workflow) +Runs `go test` with `test-args`, one argument per line without shell quotes (default `-race` and `./...`). Fetches tags so tests asserting on `git describe` version metadata behave as they do locally. + +With `coverage: true`, it also writes a coverage profile, reports the total in the job summary, and uploads the profile to Coveralls using the automatically-provided `GITHUB_TOKEN`. The caller repo must be enrolled in Coveralls. **Usage:** ```yaml @@ -140,31 +256,17 @@ on: jobs: test: - uses: OpenCHAMI/github-actions/.github/workflows/test-go.yml@v3.9 + uses: OpenCHAMI/github-actions/.github/workflows/test-unit-go.yml@v4.0 # Optional overrides: # with: - # go_version_file: go.mod - # test-command: make test -``` - -### coverage-go (Reusable Workflow) -Produces a Go coverage profile, writes the total to the job summary, and uploads the profile to Coveralls using the automatically-provided `GITHUB_TOKEN`. The caller repo must be enrolled in Coveralls. - -**Usage:** -```yaml -name: Coverage -on: - pull_request: - push: - branches: [main] - -jobs: - coverage: - uses: OpenCHAMI/github-actions/.github/workflows/coverage-go.yml@v3.9 - # Optional overrides: - # with: - # go_version_file: go.mod - # coverage-command: make coverage + # go-version: stable # mutually exclusive with go-version-file + # go-version-file: go.mod + # test-args: |- + # -race + # -short + # ./... + # coverage: true + # coverage-file: coverage.out ``` ### reuse (Reusable Workflow) @@ -180,7 +282,10 @@ on: jobs: reuse: - uses: OpenCHAMI/github-actions/.github/workflows/reuse.yml@v3.9 + uses: OpenCHAMI/github-actions/.github/workflows/reuse.yml@v4.0 + # Optional overrides: + # with: + # reuse-version: 6.2.0 ``` ### govulncheck (Reusable Workflow) @@ -198,7 +303,11 @@ on: jobs: govulncheck: - uses: OpenCHAMI/github-actions/.github/workflows/govulncheck.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/govulncheck.yml@v4.0 + # Optional overrides: + # with: + # go-version: 1.26.7 # default: read from go.mod + # go-package: ./cmd/... ``` ### dependency-review (Reusable Workflow) @@ -212,29 +321,33 @@ on: jobs: dependency-review: - uses: OpenCHAMI/github-actions/.github/workflows/dependency-review.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/dependency-review.yml@v4.0 # Optional overrides: # with: # fail-on-severity: moderate # deny-licenses: GPL-3.0,AGPL-3.0 + # allow-licenses: MIT,Apache-2.0 + # comment-summary-in-pr: always # always | on-failure | never ``` ### 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` with a digest-pinned image reference. +Scans an already-pushed container image with Trivy and uploads SARIF findings to GitHub Advanced Security. Designed to chain after `build-release-goreleaser`. **Usage:** ```yaml jobs: build: - uses: OpenCHAMI/github-actions/.github/workflows/docker-build-release.yml@v3.8 - 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@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/trivy-image-scan.yml@v4.0 with: - image-ref: ghcr.io/openchami/foo:${{ github.sha }} + image-ref: ghcr.io/openchami/foo@ # TODO(#44): scan finds image digests itself + # Optional overrides: + # severity: CRITICAL + # ignore-unfixed: true + # exit-code: '0' # report only ``` ### scorecard (Reusable Workflow) @@ -258,83 +371,111 @@ permissions: jobs: scorecard: - uses: OpenCHAMI/github-actions/.github/workflows/scorecard.yml@v3.9 + uses: OpenCHAMI/github-actions/.github/workflows/scorecard.yml@v4.0 ``` -### build-publish-container-goreleaser (Reusable Workflow) -Builds and publishes a container image via GoReleaser, with multi-arch builds, build provenance attestation, and PR snapshot support. Release builds (`is_pr_build: false`) pass GitHub's auto-generated release notes for the pushed tag to GoReleaser via `--release-notes`. +### 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. +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. **Usage:** ```yaml jobs: build: - uses: OpenCHAMI/github-actions/.github/workflows/build-publish-container-goreleaser.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/build-publish-container-goreleaser.yml@v4.0 with: - registry_subject_name: ghcr.io/openchami/foo - release_draft: false - cgo_enabled: 1 - build_deps: gcc-aarch64-linux-gnu libc6-dev-arm64-cross + registry-subject-name: ghcr.io/openchami/foo + release-draft: false + cgo-enabled: 1 + 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:** ```yaml jobs: build: - uses: OpenCHAMI/github-actions/.github/workflows/build-rpm-quadlet.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/build-rpm-quadlet.yml@v4.0 + # Optional overrides: + # with: + # 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:** ```yaml jobs: sign: - uses: OpenCHAMI/github-actions/.github/workflows/gpg-sign-artifacts.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/gpg-sign-artifacts.yml@v4.0 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:** ```yaml jobs: validate: - uses: OpenCHAMI/github-actions/.github/workflows/validate-rpm-quadlet.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/validate-rpm-quadlet.yml@v4.0 with: + # artifact-name-signed-rpms: rpms-signed # optional rpms: | - name: foo-*.rpm files: - /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:** ```yaml jobs: release: - uses: OpenCHAMI/github-actions/.github/workflows/release-signed-artifacts.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/release-signed-artifacts.yml@v4.0 with: - release_draft: false + release-draft: false ``` ### publish-release (Reusable Workflow) -Publishes the draft GitHub Release for a tag, for pipelines that set `release_draft: true` upstream so the release only appears once every artifact is attached. +Publishes the draft GitHub Release for a tag, for pipelines that set `release-draft: true` upstream so the release only appears once every artifact is attached. **Usage:** ```yaml jobs: publish: needs: release - uses: OpenCHAMI/github-actions/.github/workflows/publish-release.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/publish-release.yml@v4.0 + # Optional overrides: + # with: + # make-latest: true # true | false | legacy (default) + # draft: true ``` ### pr-registry-cleanup (Reusable Workflow) @@ -349,30 +490,120 @@ on: jobs: cleanup: - uses: OpenCHAMI/github-actions/.github/workflows/pr-registry-cleanup.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/pr-registry-cleanup.yml@v4.0 permissions: packages: write + # Optional overrides: + # with: + # pr-number: 123 + # tag-prefix: pr- +``` + +### stale (Reusable Workflow) +Marks issues and PRs stale after a period of inactivity and closes them if nothing changes, using the org-default `actions/stale` policy (stale after 35 days, closed 7 days later; `pinned`/`security` items exempt). Updating an item removes the stale label. The caller owns the schedule. + +**Usage:** +```yaml +name: Stale +on: + schedule: + - cron: '23 3 * * *' # daily at 03:23 UTC + workflow_dispatch: + +jobs: + stale: + uses: OpenCHAMI/github-actions/.github/workflows/stale.yml@v4.0 + permissions: + issues: write + pull-requests: write + # Optional overrides: + # with: + # days-before-stale: 35 + # days-before-close: 7 + # stale-issue-label: stale + # stale-pr-label: stale + # exempt-issue-labels: pinned,security,help wanted + # exempt-pr-labels: pinned,security + # operations-per-run: 500 + # stale-issue-message: ... + # close-issue-message: ... + # stale-pr-message: ... + # close-pr-message: ... +``` + +### lint-codegen-fabrica (Reusable Workflow) +Fails when a Fabrica project's committed generated code is out of date by running the caller's `make generate-check` with Fabrica built from source (passed as `LOCAL_FABRICA`), at the version pinned in `go.mod` unless `fabrica-ref` (tag, branch, or commit SHA) is set. A source build is required: Fabrica stamps its version into generated code, and a `go run` build reports `dev` instead. + +**Usage:** +```yaml +name: Lint Codegen +on: + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + lint-codegen: + uses: OpenCHAMI/github-actions/.github/workflows/lint-codegen-fabrica.yml@v4.0 + # Optional overrides: + # with: + # go-version: stable # mutually exclusive with go-version-file + # go-version-file: go.mod + # fabrica-ref: v0.4.5 # tag, branch, or SHA; default: version in go.mod +``` + +### lint-renovate (Reusable Workflow) +Validates the caller's Renovate config with `renovate-config-validator` (as repository config). With no `config-file`, it checks every Renovate config file it finds (`renovate.json`, `.github/renovate.json`, `renovate.json5`, `.renovaterc`, ...). + +**Usage:** +```yaml +name: Lint Renovate +on: + pull_request: + paths: + - renovate.json + - .github/workflows/lint-renovate.yml + +jobs: + lint-renovate: + uses: OpenCHAMI/github-actions/.github/workflows/lint-renovate.yml@v4.0 + # Optional overrides: + # with: + # renovate-version: 44.115.13 + # config-file: renovate.json ``` ## Actions -### gpg-ephemeral-key (Deprecated - use gpg-configure-release-keys) -Generates a short-lived RSA key and signs it with a repo-scoped subkey. See the [action README](actions/gpg-ephemeral-key/README.md). +### gpg-configure-release-keys (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. -### gpg-configure-release-keys Generates a per-run ephemeral GPG key, certified through the repo's release key chain (master certifies a repo cert key, which certifies the ephemeral key). See the [action README](actions/gpg-configure-release-keys/README.md). -### gpg-sign-rpm +### 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 +### 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: @@ -385,6 +616,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 @@ -396,12 +630,13 @@ on: types: [opened, synchronize, reopened, edited] workflow_dispatch: inputs: - pr_number: + pr-number: description: 'PR Number to build (optional, for manual PR builds)' required: false type: string -permissions: write-all # Necessary for the generate-build-provenance action with containers +permissions: + contents: read # baseline; jobs that need more request it below jobs: config: @@ -420,33 +655,38 @@ jobs: } >> "$GITHUB_OUTPUT" build: - uses: OpenCHAMI/github-actions/.github/workflows/build-publish-container-goreleaser.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/build-publish-container-goreleaser.yml@v4.0 secrets: inherit + 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 with: - cgo_enabled: 0 - registry_subject_name: ghcr.io/openchami/metadata-service - is_pr_build: true - pr_number: ${{ inputs.pr_number || github.event.pull_request.number || 0 }} + cgo-enabled: 0 + registry-subject-name: ghcr.io/openchami/metadata-service + is-pr-build: true + pr-number: ${{ inputs.pr-number || github.event.pull_request.number || 0 }} - rpmbuild: + rpm-build: needs: [config, build] - uses: OpenCHAMI/github-actions/.github/workflows/build-rpm-quadlet.yml@v3.8 + uses: OpenCHAMI/github-actions/.github/workflows/build-rpm-quadlet.yml@v4.0 secrets: inherit with: artifact-name-unsigned-rpms: ${{ needs.config.outputs.rpm-unsigned }} - rpmsign: - needs: [config, rpmbuild] - uses: OpenCHAMI/github-actions/.github/workflows/gpg-sign-artifacts.yml@v3.8 + rpm-sign: + needs: [config, rpm-build] + uses: OpenCHAMI/github-actions/.github/workflows/gpg-sign-artifacts.yml@v4.0 secrets: inherit with: artifact-name-unsigned-rpms: ${{ needs.config.outputs.rpm-unsigned }} artifact-name-signed-rpms: ${{ needs.config.outputs.rpm-signed }} artifact-name-public-keys: ${{ needs.config.outputs.keys-public }} - rpmvalidate: - needs: [config, rpmsign] - uses: OpenCHAMI/github-actions/.github/workflows/validate-rpm-quadlet.yml@v3.8 + rpm-validate: + needs: [config, rpm-sign] + uses: OpenCHAMI/github-actions/.github/workflows/validate-rpm-quadlet.yml@v4.0 secrets: inherit with: artifact-name-signed-rpms: ${{ needs.config.outputs.rpm-signed }} @@ -461,8 +701,8 @@ jobs: ## Continuous Integration -- Workflow files are linted via `lint-workflows.yml` (actionlint + zizmor). -- RPM/quadlet output is validated via `validate-rpm-quadlet.yml`. +- Workflow files are linted via `0-local-ci.yml`, which calls `lint-ci.yml` (actionlint + zizmor). +- 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/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-ephemeral-key/README.md b/actions/gpg-ephemeral-key/README.md deleted file mode 100644 index aa4e146..0000000 --- a/actions/gpg-ephemeral-key/README.md +++ /dev/null @@ -1,109 +0,0 @@ - - -# 🛡️ GPG Ephemeral Key Generator - -> [!WARNING] -> -> Deprecated: use [`gpg-configure-release-keys`](../gpg-configure-release-keys) instead. - -This GitHub composite action generates a new ephemeral GPG key on every build, signs it using a repo‑scoped subkey, and exports the fingerprint and public key. It’s designed for use in CI pipelines where artifacts need secure signing without long‑lived keys in GitHub Actions. - ---- - -## 🔧 How It Works - -- Generates a short‑lived RSA key (default RSA‑3072, expiration 1 day) -- Signs it with your repo‑specific subkey (stored as `GPG_SUBKEY_B64`) -- Returns: - - `ephemeral-fingerprint` → use to sign artifacts - - `ephemeral-public-key` → base64-encoded armored public key - - `gnupg-home` → isolated GNUPGHOME path for downstream steps - ---- - -## 📦 Inputs - -| Name | Required | Description | -|------------------|----------|-------------| -| `subkey-armored` | ✅ | Base64‑encoded ASCII‑armored GPG subkey (secret) used to sign the ephemeral key | -| `name` | ❌ | Name for the ephemeral key (default: Ephemeral Key) | -| `comment` | ❌ | Metadata like build ID, ref, etc. A random suffix is appended | -| `email` | ❌ | Email for ephemeral key (default: ci@build.local) | -| `key-length` | ❌ | RSA key length (default: 3072) | -| `expire-days` | ❌ | Expiration in days (default: 1) | -| `cleanup` | ❌ | If `true`, remove GNUPGHOME after export (default: false) | - ---- - -## 🔑 Outputs - -| Name | Description | -|-------------------------|-------------| -| `ephemeral-fingerprint` | Fingerprint of the generated ephemeral key | -| `ephemeral-public-key` | Base64‑encoded ASCII‑armored public key | -| `gnupg-home` | Path to isolated GNUPGHOME for subsequent actions | - ---- - -## 🛠️ Setup (Per Repo) - -1. Create a GPG subkey tied to this repository, signed by your org's offline master key. -2. Export it (subkey only): - ```bash - gpg --export-secret-subkeys --armor > repo-subkey.asc - base64 < repo-subkey.asc | tr -d '\n' > subkey.b64 - ``` -3. Store as a GitHub Secret in your repo: - `GPG_SUBKEY_B64` = contents of `subkey.b64` - -## ✅ Example: Prove Signing Works - -```yaml -jobs: - test-ephemeral-signing: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Generate ephemeral key - id: gpg - uses: OpenCHAMI/github-actions/actions/gpg-ephemeral-key@v1 - with: - subkey-armored: ${{ secrets.GPG_SUBKEY_B64 }} - comment: "build:${{ github.run_id }}" - key-length: '3072' - expire-days: '1' - cleanup: false - - - name: Sign and verify a test file - run: | - GNUPGHOME="${{ steps.gpg.outputs.gnupg-home }}"; export GNUPGHOME - echo "hello" > test.txt - gpg --batch --yes --local-user "${{ steps.gpg.outputs.ephemeral-fingerprint }}" --detach-sign --output test.txt.sig test.txt - gpg --verify test.txt.sig test.txt - - - name: Show trust chain - run: | - GNUPGHOME="${{ steps.gpg.outputs.gnupg-home }}"; export GNUPGHOME - echo "Ephemeral: ${{ steps.gpg.outputs.ephemeral-fingerprint }}" - gpg --list-keys --with-colons - gpg --list-sigs "${{ steps.gpg.outputs.ephemeral-fingerprint }}" - - - name: Cleanup - if: always() - run: rm -rf "${{ steps.gpg.outputs.gnupg-home }}" -``` - -## 🔐 Security Notes - -- Ephemeral key is short‑lived; expiration limits future signing, not validation of past signatures. -- Subkey is repo‑scoped, easy to rotate/revoke. -- Use the isolated `GNUPGHOME` to avoid polluting runner defaults; set `cleanup: true` when possible. -- Trust chain: `Ephemeral key ← Repo subkey ← Offline master key`. - -## 📝 License - -MIT diff --git a/actions/gpg-ephemeral-key/action.yml b/actions/gpg-ephemeral-key/action.yml deleted file mode 100644 index 3d6ce89..0000000 --- a/actions/gpg-ephemeral-key/action.yml +++ /dev/null @@ -1,176 +0,0 @@ -# SPDX-FileCopyrightText: 2025 OpenCHAMI a Series of LF Projects, LLC -# SPDX-License-Identifier: MIT - -name: '[DEPRECATED] Generate and Sign Ephemeral GPG Key' -author: 'OpenCHAMI' -branding: - icon: 'lock' - color: 'purple' -description: 'Deprecated: use gpg-configure-release-keys instead. Creates an ephemeral GPG key per build and signs it with a repo-scoped subkey' - -inputs: - subkey-armored: - description: 'Base64-encoded ASCII-armored GPG subkey (secret) used to sign the ephemeral key' - required: true - 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: '3072' - expire-days: - description: 'Expiration in days for the ephemeral key' - default: '1' - cleanup: - description: 'If true, remove GNUPGHOME (keys) after export. Set to false if later steps need the key.' - default: 'false' - -outputs: - ephemeral-fingerprint: - description: 'Fingerprint of the generated ephemeral key' - value: ${{ steps.fpr.outputs.fingerprint }} - ephemeral-public-key: - description: 'Base64 of ASCII-armored ephemeral public key' - value: ${{ steps.export.outputs.pubkey }} - gnupg-home: - description: 'Path to isolated GNUPGHOME for subsequent actions' - value: ${{ steps.setup.outputs.gnupghome }} - -runs: - using: "composite" - steps: - - id: deprecation-warning - shell: bash - run: | - echo "::warning::gpg-ephemeral-key is deprecated; migrate to gpg-configure-release-keys." - - - id: setup - shell: bash - run: | - 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 subkey..." - - # Save subkey - decoded=$(echo "${{ inputs.subkey-armored }}" | base64 -d 2>/dev/null || true) - if [[ -z "$decoded" ]]; then - echo "ERROR: subkey-armored input is invalid or empty!" >&2 - exit 1 - fi - - echo "$decoded" > subkey.asc - - # Import key - gpg --batch --import subkey.asc || { - echo "ERROR: Failed to import subkey" >&2 - exit 1 - } - - # List keys for debug - echo "::group::GPG secret keys" - gpg --list-secret-keys - echo "::endgroup::" - - # Get signer fingerprint - signer_fpr=$(gpg --batch --with-colons --list-secret-keys | awk -F: '/^sec:/ {print $5; exit}') - if [[ -z "$signer_fpr" ]]; then - echo "ERROR: No secret key found after import" >&2 - gpg --list-secret-keys --with-colons - exit 1 - fi - - echo "Using SIGNER_FPR=$signer_fpr" - echo "SIGNER_FPR=$signer_fpr" >> "$GITHUB_ENV" - - shred -u subkey.asc || rm -f subkey.asc - - - - 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 "fingerprint=$ephemeral_fpr" >> "$GITHUB_OUTPUT" - echo "EPHEMERAL_FPR=$ephemeral_fpr" >> "$GITHUB_ENV" - - - id: sign-ephemeral - shell: bash - run: | - set -euo pipefail - gpg --batch --yes --quick-sign-key --local-user "$SIGNER_FPR" "$EPHEMERAL_FPR" - - - id: export - shell: bash - run: | - set -euo pipefail - gpg --armor --export "$EPHEMERAL_FPR" > pub.asc - b64=$(base64 < pub.asc | tr -d '\n') - echo "pubkey=$b64" >> "$GITHUB_OUTPUT" - shred -u pub.asc || rm -f pub.asc - - - id: optional-cleanup - if: ${{ inputs.cleanup == 'true' }} - shell: bash - run: | - set -euo pipefail - rm -rf "$GNUPGHOME" 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 8ec9325..bb200d9 100644 --- a/renovate.json +++ b/renovate.json @@ -3,5 +3,30 @@ "extends": [ "github>OpenCHAMI/.github//renovate-config/default", "config:recommended" + ], + "customManagers": [ + { + "customType": "regex", + "description": "Default renovate-config-validator version in lint-renovate and its README example", + "managerFilePatterns": [ + "/^\\.github/workflows/lint-renovate\\.yml$/", + "/^README\\.md$/" + ], + "matchStrings": [ + "renovate-version:\\s*\\n(?:\\s+[a-z]+:.*\\n)*?\\s+default: '(?\\d+\\.\\d+\\.\\d+)'", + "#\\s+renovate-version: (?\\d+\\.\\d+\\.\\d+)" + ], + "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" + } ] }