Files
x509-certificate-exporter/.github/workflows/release.yaml
T

277 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Release
# Triggered by pushing a vX.Y.Z tag (typically by release-please when
# its release PR is merged). Pre-release gates + three release jobs:
#
# 0. check — `task check` (lint + tests + security) on the tagged
# commit, by re-using the same three workflows that
# gate every PR (lint.yaml / security.yaml / test.yaml).
# Each gate runs on isolated runners with NO access to
# the `release` Environment, so a misbehaving check
# never sees the push secrets. The release jobs (1-3
# below) `needs:` every gate; a single failure
# short-circuits the whole workflow before any
# artifact moves.
# 1. goreleaser — builds binaries × OS/arch, archives, checksums,
# container images (busybox + scratch, multi-arch,
# pushed to ghcr/quay/docker.io), cosign-signs
# everything, attaches assets to the GitHub Release.
# Also produces a SLSA Build Level 3 provenance
# attestation over all binaries via GitHub's native
# `actions/attest-build-provenance`. Driven by
# .goreleaser.yaml + the attest step at the tail.
# 2. chart — packages the Helm chart, pushes as OCI artifact,
# cosign-signs.
#
# Container image SBOMs (CycloneDX via syft, attached as cosign
# attestations) are generated in a post-loop inside the goreleaser job,
# after GoReleaser has pushed the manifests.
#
# Verification by consumers (commands shown for the upstream repo;
# substitute the source-uri / certificate-identity / owner for forks):
# - Binary: gh attestation verify <archive> --owner enix
# - Image: cosign verify <ref> --certificate-identity-regexp '...' --certificate-oidc-issuer https://token.actions.githubusercontent.com
# - SBOM: cosign verify-attestation <ref> --type cyclonedx --certificate-identity-regexp ...
# - Chart: cosign verify <chart-oci-ref> --certificate-identity-regexp '...' --certificate-oidc-issuer https://token.actions.githubusercontent.com
#
# Manual setup required:
# - Repo environment "release" with required reviewers (Settings → Environments)
# - Environment variables on the `release` env (no fallbacks — workflow fails fast):
# - IMAGE_NAME — container image name (e.g. x509-certificate-exporter)
# - CHART_NAME — Helm chart name (often == IMAGE_NAME)
# - CHART_REGISTRY — OCI host/namespace where the chart is pushed,
# WITHOUT the `oci://` scheme (e.g. quay.io/enix/charts)
# - Repo secrets:
# - quay.io → QUAY_USERNAME, QUAY_TOKEN
# - docker.io → DOCKERHUB_USERNAME, DOCKERHUB_TOKEN
# - ghcr.io → no secret needed (uses workflow GITHUB_TOKEN)
# - Chart OCI registry credentials:
# - CHART_REGISTRY_USERNAME
# - CHART_REGISTRY_TOKEN
# - Branch protection on main configured separately
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+-*"
permissions: {}
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# ─── 0. Pre-release gates (`task check`) ─────────────────────────────────
# Three reusable workflows — same ones that gate every PR — re-run
# on the tagged commit. Each runs on its own runner (strict isolation
# from the build environment) and intentionally has NO access to the
# `release` Environment, so a misbehaving gate never sees the push
# secrets. The release jobs below `needs:` all three; a single failure
# aborts before anything moves to a registry.
lint:
uses: ./.github/workflows/lint.yaml
permissions:
contents: read
security:
uses: ./.github/workflows/security.yaml
permissions:
contents: read
pull-requests: write # gitleaks-action posts on PRs; no-op on tag pushes
test:
uses: ./.github/workflows/test.yaml
permissions:
contents: read
# ─── 1. Build, push, sign everything (binaries + images) ─────────────────
goreleaser:
name: Build, push & sign
needs: [lint, security, test]
runs-on: ubuntu-latest
timeout-minutes: 60
environment: release
permissions:
contents: write # create/append the GitHub Release
packages: write # push to ghcr.io
id-token: write # OIDC for cosign keyless + SLSA provenance signing
attestations: write # upload SLSA build provenance to the GitHub Attestations API
env:
IMAGE_NAME: ${{ vars.IMAGE_NAME }}
steps:
- name: Validate environment variables
run: |
set -euo pipefail
[ -n "${IMAGE_NAME:-}" ] || {
echo "::error::Missing required Environment variable: IMAGE_NAME"
echo "::error::Set it at Settings → Environments → release → Variables"
exit 1
}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# GoReleaser inspects git history for the changelog. Full clone
# avoids "shallow clone" warnings on large repos.
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
# buildx + qemu enable cross-arch image manifest building. The
# Dockerfiles only do COPY, so QEMU isn't strictly required for
# execution — but binfmt registration is needed so buildx
# recognises linux/arm64 and linux/riscv64 as valid platforms.
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
# Logins for the three image registries. Quay/Docker Hub are
# skipped if their token is missing, mirroring the previous
# workflow's per-registry opt-in pattern.
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to quay.io
env:
QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }}
if: env.QUAY_TOKEN != ''
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_TOKEN }}
- name: Login to docker.io
env:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
if: env.DOCKERHUB_TOKEN != ''
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4
- uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0
- uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
with:
version: latest
args: release --clean
# SLSA Build Level 3 provenance for every binary listed in
# GoReleaser's dist/checksums.txt (sha256sum -c format). Signed
# via Sigstore (Fulcio + Rekor) using the workflow's OIDC token,
# uploaded to GitHub's native Attestations API. Verified by
# consumers with `gh attestation verify <archive> --owner <org>`.
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4
with:
subject-checksums: dist/checksums.txt
# CycloneDX SBOMs on the multi-arch manifests, as cosign
# attestations. Iterates over the images GoReleaser pushed
# (read from dist/artifacts.json — its source of truth).
#
# dockers_v2 emits one "Docker Image" artifact per (registry, tag),
# where each tag is the multi-arch index manifest pushed by a single
# `docker buildx build --push --platform=...` invocation. There's no
# separate "Docker Manifest" artifact type anymore — those existed
# only for the legacy `dockers + docker_manifests` two-step flow.
- name: Generate + attest image SBOMs
env:
COSIGN_YES: "true"
run: |
set -euo pipefail
# Re-anchor each pushed manifest by digest (the tag is mutable;
# the digest is forever) and attach a CycloneDX SBOM as a
# cosign attestation. `${ref%:*}` strips the trailing `:tag`
# using the shortest-match suffix removal — only `:tag`,
# leaving registry/namespace/repo intact.
jq -r '.[] | select(.type == "Docker Image") | .name' dist/artifacts.json \
| while read -r ref; do
echo "::group::SBOM for $ref"
digest=$(docker buildx imagetools inspect "$ref" \
--format '{{ .Manifest.Digest }}')
ref_by_digest="${ref%:*}@${digest}"
syft "$ref_by_digest" -o cyclonedx-json=/tmp/sbom.json
cosign attest --predicate /tmp/sbom.json --type cyclonedx "$ref_by_digest"
rm -f /tmp/sbom.json
echo "::endgroup::"
done
# ─── 2. Helm chart: package, push as OCI, cosign-sign ────────────────────
chart:
name: Chart publish & sign
needs: goreleaser # the chart references the just-pushed images
runs-on: ubuntu-latest
timeout-minutes: 15
environment: release
permissions:
contents: read
id-token: write # OIDC for cosign keyless
env:
CHART_NAME: ${{ vars.CHART_NAME }}
CHART_REGISTRY: ${{ vars.CHART_REGISTRY }}
steps:
- name: Validate environment variables
run: |
set -euo pipefail
missing=()
[ -n "${CHART_NAME:-}" ] || missing+=("CHART_NAME")
[ -n "${CHART_REGISTRY:-}" ] || missing+=("CHART_REGISTRY")
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Missing required Environment variable(s): ${missing[*]}"
echo "::error::Set them at Settings → Environments → release → Variables"
exit 1
fi
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5
with:
version: "3.18.0"
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4
- name: Login to chart OCI registry
env:
CHART_REGISTRY_USERNAME: ${{ secrets.CHART_REGISTRY_USERNAME }}
CHART_REGISTRY_TOKEN: ${{ secrets.CHART_REGISTRY_TOKEN }}
run: |
set -euo pipefail
host="${CHART_REGISTRY%%/*}"
echo "$CHART_REGISTRY_TOKEN" | helm registry login -u "$CHART_REGISTRY_USERNAME" --password-stdin "$host"
- name: Override chart name if needed
run: |
set -euo pipefail
source_name=$(awk '/^name:/ {print $2; exit}' chart/Chart.yaml)
if [ "$source_name" != "$CHART_NAME" ]; then
sed -i "s|^name:.*|name: $CHART_NAME|" chart/Chart.yaml
echo "Renamed chart from '$source_name' to '$CHART_NAME'"
fi
- name: Copy LICENSE into chart
run: cp LICENSE chart/LICENSE
- name: Helm package
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
mkdir -p dist
helm package chart/ \
--destination dist/ \
--version "$version" \
--app-version "$version"
- name: Helm push to OCI registry
id: push
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
chart_tgz="dist/${CHART_NAME}-${version}.tgz"
out=$(helm push "$chart_tgz" "oci://${CHART_REGISTRY}" 2>&1 | tee /dev/stderr)
digest=$(printf '%s\n' "$out" | grep -oE 'sha256:[a-f0-9]+' | head -1)
if [ -z "$digest" ]; then
echo "::error::Could not extract digest from helm push output"
exit 1
fi
ref="${CHART_REGISTRY}/${CHART_NAME}@${digest}"
echo "ref=$ref" >> "$GITHUB_OUTPUT"
- name: Sign chart with cosign keyless
env:
COSIGN_YES: "true"
run: cosign sign "${{ steps.push.outputs.ref }}"