mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:31:30 +00:00
69d4ada385
Two GitHub-Actions defaults were producing ugly titles on every tag: 1. The Actions-tab workflow run title was auto-generated as `<commit-subject> #<run-number>` because release.yml had no `run-name:`. The v2.0.69 push showed up as "chore: rename Go module path to github.com/certctl-io/certctl #73" instead of the obvious "Release v2.0.69". 2. The Releases-page title was auto-generated by softprops/action-gh-release@v2 because the action's `with:` block had no `name:` field — it falls back to the most recent commit subject in that case, producing the same noise on the Releases page. Fixes: - Add `run-name: Release ${{ github.ref_name }}` at the workflow top. `github.ref_name` resolves to the tag (e.g., `v2.0.69`) since the only trigger is `on: push: tags: ['v*']`. Actions tab now shows "Release v2.0.69". - Add `name: ${{ github.ref_name }}` to the softprops/action-gh-release@v2 step's `with:` block. Releases page now shows "v2.0.69" as the title instead of the commit subject. Affects v2.0.70+. The v2.0.69 workflow run + release page that's already in flight retain the bad titles (the workflow file is read at trigger time); the v2.0.69 Releases-page title can be manually edited via the GitHub UI ("Edit release" → set title to `v2.0.69` → Update release). The Actions-tab run name for #73 is immutable post-trigger. This same pattern likely affects ci.yml + the other workflows but the operator-facing surface is the Release workflow's titles, so leaving the CI workflows alone for now (they run continuously on master and nobody clicks individual run titles).
424 lines
17 KiB
YAML
424 lines
17 KiB
YAML
name: Release
|
||
|
||
# Override the auto-generated run name (which would otherwise default to
|
||
# the most recent commit subject + a #NN run number) so the Actions tab
|
||
# shows "Release v2.0.69" instead of "chore: rename Go module path... #73".
|
||
# `github.ref_name` resolves to the tag name (e.g., `v2.0.69`) for tag-triggered
|
||
# workflows, which is the only trigger we set below.
|
||
run-name: Release ${{ github.ref_name }}
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*'
|
||
|
||
env:
|
||
REGISTRY: ghcr.io
|
||
# Keep in lock-step with .github/workflows/ci.yml (M-3).
|
||
GO_VERSION: '1.25.9'
|
||
IMAGE_NAMESPACE: certctl-io
|
||
|
||
jobs:
|
||
# ----------------------------------------------------------------------
|
||
# build-binaries (M-3): matrix build every (binary × OS × arch) tuple.
|
||
# For each tuple we produce: the binary, a SPDX-JSON SBOM, a keyless
|
||
# Cosign signature + certificate bundle, and a single-line sha256sum
|
||
# file. All artefacts are uploaded to a workflow-scoped artifact; the
|
||
# aggregate-checksums job fans them back in for release upload.
|
||
# ----------------------------------------------------------------------
|
||
build-binaries:
|
||
name: Build ${{ matrix.binary }} (${{ matrix.os }}/${{ matrix.arch }})
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
id-token: write # Cosign keyless OIDC identity token
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
binary: [agent, server, cli, mcp-server]
|
||
os: [linux, darwin]
|
||
arch: [amd64, arm64]
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Set up Go
|
||
uses: actions/setup-go@v5
|
||
with:
|
||
go-version: ${{ env.GO_VERSION }}
|
||
|
||
- name: Extract version from tag
|
||
id: version
|
||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Install govulncheck
|
||
# Bundle D / Audit L-008: release.yml previously had no vulnerability
|
||
# scan, so a release tag could in principle ship a binary with a
|
||
# known CVE in transitive deps that ci.yml's govulncheck would have
|
||
# caught on master. Pre-build scan blocks the release if anything
|
||
# surfaced post-merge. Pinned to the same major as ci.yml.
|
||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||
|
||
- name: Run govulncheck (release gate)
|
||
# govulncheck distinguishes called-vs-uncalled vulnerable functions.
|
||
# Default exit code (0 unless an actual call site lands in a vuln
|
||
# function) is the right gate for release; deferred-call advisories
|
||
# are tracked separately on master via L-021. If a release-time
|
||
# scan surfaces a NEW called-vuln, the release is blocked until the
|
||
# bump lands on master and a new tag is cut.
|
||
run: govulncheck ./...
|
||
|
||
- name: Build binary
|
||
id: build
|
||
env:
|
||
GOOS: ${{ matrix.os }}
|
||
GOARCH: ${{ matrix.arch }}
|
||
CGO_ENABLED: '0'
|
||
VERSION: ${{ steps.version.outputs.VERSION }}
|
||
run: |
|
||
set -euo pipefail
|
||
OUTPUT_NAME="certctl-${{ matrix.binary }}-${{ matrix.os }}-${{ matrix.arch }}"
|
||
mkdir -p dist
|
||
go build \
|
||
-trimpath \
|
||
-ldflags="-w -s -X main.Version=${VERSION}" \
|
||
-o "dist/${OUTPUT_NAME}" \
|
||
"./cmd/${{ matrix.binary }}"
|
||
ls -lh "dist/${OUTPUT_NAME}"
|
||
echo "output_name=${OUTPUT_NAME}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Generate SBOM (SPDX-JSON)
|
||
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||
with:
|
||
file: dist/${{ steps.build.outputs.output_name }}
|
||
format: spdx-json
|
||
output-file: dist/${{ steps.build.outputs.output_name }}.sbom.spdx.json
|
||
upload-artifact: false
|
||
upload-release-assets: false
|
||
|
||
- name: Install Cosign
|
||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||
|
||
- name: Keyless-sign binary with Cosign
|
||
env:
|
||
OUTPUT_NAME: ${{ steps.build.outputs.output_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
# Cosign v3.0 (shipped by cosign-installer@v4.1.1 default
|
||
# cosign-release=v3.0.5) removed --output-signature/--output-certificate
|
||
# on sign-blob. The replacement is --bundle, which emits a unified
|
||
# Sigstore bundle (signature + cert chain + Rekor inclusion proof) as
|
||
# a single .sigstore.json artefact. M-11.
|
||
cosign sign-blob \
|
||
--yes \
|
||
--bundle "dist/${OUTPUT_NAME}.sigstore.json" \
|
||
"dist/${OUTPUT_NAME}"
|
||
|
||
- name: Compute SHA-256 sidecar
|
||
env:
|
||
OUTPUT_NAME: ${{ steps.build.outputs.output_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
cd dist
|
||
sha256sum "${OUTPUT_NAME}" > "${OUTPUT_NAME}.sha256"
|
||
cat "${OUTPUT_NAME}.sha256"
|
||
|
||
- name: Upload build artefacts
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: binary-${{ steps.build.outputs.output_name }}
|
||
path: |
|
||
dist/${{ steps.build.outputs.output_name }}
|
||
dist/${{ steps.build.outputs.output_name }}.sigstore.json
|
||
dist/${{ steps.build.outputs.output_name }}.sbom.spdx.json
|
||
dist/${{ steps.build.outputs.output_name }}.sha256
|
||
if-no-files-found: error
|
||
retention-days: 7
|
||
|
||
# ----------------------------------------------------------------------
|
||
# aggregate-checksums (M-3): fan in every matrix artefact, produce a
|
||
# single checksums.txt (sha256sum format, compatible with `sha256sum
|
||
# -c`), sign it with Cosign, upload everything to the GitHub Release,
|
||
# and emit a base64-encoded hash manifest for the SLSA generator.
|
||
# ----------------------------------------------------------------------
|
||
aggregate-checksums:
|
||
name: Aggregate checksums & sign
|
||
runs-on: ubuntu-latest
|
||
needs: [build-binaries]
|
||
permissions:
|
||
contents: write
|
||
id-token: write # Cosign keyless OIDC identity token
|
||
outputs:
|
||
hashes: ${{ steps.hashes.outputs.hashes }}
|
||
steps:
|
||
- name: Download binary artefacts
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
pattern: binary-*
|
||
path: artifacts
|
||
merge-multiple: true
|
||
|
||
- name: Aggregate SHA-256 sums
|
||
id: hashes
|
||
run: |
|
||
set -euo pipefail
|
||
cd artifacts
|
||
: > checksums.txt
|
||
for f in certctl-*; do
|
||
case "$f" in
|
||
*.sigstore.json|*.sbom.spdx.json|*.sha256|checksums.txt)
|
||
continue ;;
|
||
esac
|
||
sha256sum "$f" >> checksums.txt
|
||
done
|
||
echo "=== checksums.txt ==="
|
||
cat checksums.txt
|
||
# base64 hashes (single line, no wrapping) for SLSA generator.
|
||
HASHES=$(base64 -w0 < checksums.txt)
|
||
echo "hashes=${HASHES}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Install Cosign
|
||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||
|
||
- name: Keyless-sign checksums.txt
|
||
run: |
|
||
set -euo pipefail
|
||
cd artifacts
|
||
# Cosign v3.0 --bundle replaces the removed v2 flag pair
|
||
# --output-signature / --output-certificate. See M-11.
|
||
cosign sign-blob \
|
||
--yes \
|
||
--bundle checksums.txt.sigstore.json \
|
||
checksums.txt
|
||
|
||
- name: Upload artefacts to GitHub Release
|
||
uses: softprops/action-gh-release@v2
|
||
if: startsWith(github.ref, 'refs/tags/')
|
||
with:
|
||
files: |
|
||
artifacts/certctl-*
|
||
artifacts/checksums.txt
|
||
artifacts/checksums.txt.sigstore.json
|
||
|
||
# ----------------------------------------------------------------------
|
||
# provenance-binaries (M-3): SLSA Level 3 provenance for every binary.
|
||
# The SLSA generic generator reusable workflow runs in a hermetic
|
||
# workflow run, producing multiple.intoto.jsonl from the base64 hash
|
||
# manifest and uploading it as a release asset.
|
||
# ----------------------------------------------------------------------
|
||
provenance-binaries:
|
||
name: SLSA provenance (binaries)
|
||
needs: [aggregate-checksums]
|
||
permissions:
|
||
actions: read
|
||
id-token: write
|
||
contents: write
|
||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
|
||
with:
|
||
base64-subjects: "${{ needs.aggregate-checksums.outputs.hashes }}"
|
||
upload-assets: true
|
||
provenance-name: multiple.intoto.jsonl
|
||
|
||
# ----------------------------------------------------------------------
|
||
# build-and-push-docker: push container images to GHCR with native
|
||
# SLSA L3 provenance (mode=max) and SBOM attestations emitted by
|
||
# docker/build-push-action@v6, plus a keyless Cosign signature on the
|
||
# image digest for identity-bound verification. The M-4 proxy-propagation
|
||
# build-args block is retained verbatim — M-3 only adds supply-chain
|
||
# steps; it never touches M-4 wiring.
|
||
# ----------------------------------------------------------------------
|
||
build-and-push-docker:
|
||
name: Build & Push Docker Images
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
packages: write
|
||
id-token: write # Cosign keyless OIDC identity token
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Log in to GitHub Container Registry
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: ${{ env.REGISTRY }}
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Extract version from tag
|
||
id: version
|
||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
- name: Install Cosign
|
||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||
|
||
- name: Build and push server image
|
||
id: server-push
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
context: .
|
||
file: ./Dockerfile
|
||
push: true
|
||
tags: |
|
||
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-server:${{ steps.version.outputs.VERSION }}
|
||
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-server:latest
|
||
# Proxy propagation (M-4, Issue #9) — forwards runner-level proxy
|
||
# secrets into the Docker build so self-hosted runners behind
|
||
# corporate proxies can reach public registries. GitHub-hosted
|
||
# runners don't need proxies, so the secrets are optional and
|
||
# resolve to empty strings when unset — byte-identical to the
|
||
# pre-fix behaviour for the public-runner path.
|
||
build-args: |
|
||
HTTP_PROXY=${{ secrets.HTTP_PROXY }}
|
||
HTTPS_PROXY=${{ secrets.HTTPS_PROXY }}
|
||
NO_PROXY=${{ secrets.NO_PROXY }}
|
||
# Supply-chain hardening (M-3): emit native SLSA L3 provenance
|
||
# and SBOM attestations bound to the image manifest.
|
||
provenance: mode=max
|
||
sbom: true
|
||
cache-from: type=gha
|
||
cache-to: type=gha,mode=max
|
||
|
||
- name: Keyless-sign server image with Cosign
|
||
env:
|
||
DIGEST: ${{ steps.server-push.outputs.digest }}
|
||
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-server
|
||
run: |
|
||
set -euo pipefail
|
||
cosign sign --yes "${IMAGE}@${DIGEST}"
|
||
|
||
- name: Build and push agent image
|
||
id: agent-push
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
context: .
|
||
file: ./Dockerfile.agent
|
||
push: true
|
||
tags: |
|
||
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-agent:${{ steps.version.outputs.VERSION }}
|
||
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-agent:latest
|
||
# Proxy propagation (M-4, Issue #9) — see server-image step for
|
||
# rationale. Empty secrets resolve to empty build args, leaving
|
||
# the un-proxied code path byte-identical to the pre-fix tree.
|
||
build-args: |
|
||
HTTP_PROXY=${{ secrets.HTTP_PROXY }}
|
||
HTTPS_PROXY=${{ secrets.HTTPS_PROXY }}
|
||
NO_PROXY=${{ secrets.NO_PROXY }}
|
||
# Supply-chain hardening (M-3): emit native SLSA L3 provenance
|
||
# and SBOM attestations bound to the image manifest.
|
||
provenance: mode=max
|
||
sbom: true
|
||
cache-from: type=gha
|
||
cache-to: type=gha,mode=max
|
||
|
||
- name: Keyless-sign agent image with Cosign
|
||
env:
|
||
DIGEST: ${{ steps.agent-push.outputs.digest }}
|
||
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/certctl-agent
|
||
run: |
|
||
set -euo pipefail
|
||
cosign sign --yes "${IMAGE}@${DIGEST}"
|
||
|
||
# ----------------------------------------------------------------------
|
||
# create-release: stamp the release body. The actual asset uploads are
|
||
# handled by aggregate-checksums (binaries, SBOMs, sigs, certs,
|
||
# checksums.txt + signature) and the SLSA generator (multiple.intoto.jsonl).
|
||
# ----------------------------------------------------------------------
|
||
create-release:
|
||
name: Create Release Notes
|
||
runs-on: ubuntu-latest
|
||
needs: [build-binaries, aggregate-checksums, provenance-binaries, build-and-push-docker]
|
||
permissions:
|
||
contents: write
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Extract version from tag
|
||
id: version
|
||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Create release with notes
|
||
# generate_release_notes: true asks GitHub to auto-generate the
|
||
# "What's Changed" section from PRs+commits between this tag and the
|
||
# previous one. The hardcoded body below appends a per-release
|
||
# supply-chain verification block (Cosign / SLSA / SBOM steps with the
|
||
# current version baked into the commands) plus a single link to the
|
||
# README's Quick Start section for install/upgrade instructions.
|
||
# We deliberately do NOT duplicate install instructions here — the
|
||
# README is the source of truth for those, and inlining them in every
|
||
# release page produces the kind of "every release looks identical"
|
||
# noise that gives operators no signal about what actually changed.
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
# Pin the release title to the tag name. softprops/action-gh-release@v2
|
||
# falls back to the most recent commit subject when `name:` is omitted,
|
||
# which produces ugly titles like "chore: rename Go module path..." on
|
||
# the Releases page. `github.ref_name` evaluates to the tag (`v2.0.69`).
|
||
name: ${{ github.ref_name }}
|
||
generate_release_notes: true
|
||
body: |
|
||
> **Install / upgrade:** see the [Quick Start section in the README](https://github.com/certctl-io/certctl/blob/master/README.md#quick-start) for Docker Compose, agent install, Helm, and binary download instructions.
|
||
|
||
## Verifying this release
|
||
|
||
Every binary, `checksums.txt`, and container image is signed with Cosign
|
||
keyless OIDC. Each binary ships with a SPDX-JSON SBOM. Binaries are covered
|
||
by SLSA Level 3 provenance; container images carry native SLSA L3 provenance
|
||
and SBOM attestations (docker/build-push-action `provenance: mode=max`,
|
||
`sbom: true`) in addition to a Cosign signature on the digest.
|
||
|
||
**1. Verify SHA-256 checksums:**
|
||
|
||
```bash
|
||
sha256sum -c checksums.txt
|
||
```
|
||
|
||
**2. Verify the Cosign signature on checksums.txt (keyless OIDC):**
|
||
|
||
```bash
|
||
cosign verify-blob \
|
||
--bundle checksums.txt.sigstore.json \
|
||
--certificate-identity-regexp '^https://github\.com/certctl-io/certctl/\.github/workflows/release\.yml@refs/tags/' \
|
||
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
|
||
checksums.txt
|
||
```
|
||
|
||
Replace `checksums.txt` with any individual binary name to verify that
|
||
artefact directly (each binary ships with its own `.sigstore.json`
|
||
bundle, e.g. `cosign verify-blob --bundle certctl-agent-linux-amd64.sigstore.json …`).
|
||
|
||
**3. Verify SLSA Level 3 provenance (binaries):**
|
||
|
||
```bash
|
||
slsa-verifier verify-artifact \
|
||
--provenance-path multiple.intoto.jsonl \
|
||
--source-uri github.com/certctl-io/certctl \
|
||
--source-tag ${{ steps.version.outputs.VERSION }} \
|
||
certctl-agent-linux-amd64
|
||
```
|
||
|
||
**4. Verify container image signature and attestations:**
|
||
|
||
```bash
|
||
IMAGE=ghcr.io/certctl-io/certctl-server:${{ steps.version.outputs.VERSION }}
|
||
cosign verify \
|
||
--certificate-identity-regexp '^https://github\.com/certctl-io/certctl/\.github/workflows/release\.yml@refs/tags/' \
|
||
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
|
||
"$IMAGE"
|
||
|
||
# SBOM attestation (SPDX-JSON) emitted by docker/build-push-action
|
||
cosign verify-attestation --type spdxjson \
|
||
--certificate-identity-regexp '^https://github\.com/certctl-io/certctl/' \
|
||
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
|
||
"$IMAGE"
|
||
|
||
# SLSA provenance attestation (mode=max)
|
||
cosign verify-attestation --type slsaprovenance \
|
||
--certificate-identity-regexp '^https://github\.com/certctl-io/certctl/' \
|
||
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
|
||
"$IMAGE"
|
||
```
|