Skip to content

Documentation updates and added cargo deny and dependabot (#24) #48

Documentation updates and added cargo deny and dependabot (#24)

Documentation updates and added cargo deny and dependabot (#24) #48

Workflow file for this run

# Copyright (c) 2025 Erick Bourgeois, firestoned
# SPDX-License-Identifier: Apache-2.0
name: Build
on:
pull_request:
branches:
- main
paths:
- 'src/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'Dockerfile*'
- '.github/workflows/build.yaml'
- '.github/actions/**'
push:
branches:
- main
release:
types:
- published
# Defaults for jobs that do not declare their own permissions block.
# Jobs with special needs (docker, attest, sign, upload) override at job level.
permissions:
contents: read
id-token: write
security-events: write
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# Override the cross-compilation linkers set in .cargo/config.toml.
# config.toml specifies the Homebrew cross-compilers needed when a macOS
# developer runs `cargo build --target x86_64-unknown-linux-gnu` locally.
# On Linux CI runners the native target IS x86_64/aarch64-unknown-linux-gnu,
# so cargo would try to invoke those cross-compilers — which are not
# installed. Setting these vars to `cc` restores the default system linker.
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: cc
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: cc
jobs:
# ─────────────────────────────────────────────────────────────────────────────
# License headers — always runs (PRs are pre-filtered by path at the trigger).
# ─────────────────────────────────────────────────────────────────────────────
license-check:
name: ⚖️ Verify SPDX License Headers
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Check license headers
uses: firestoned/github-actions/security/license-check@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
copyright-holder: "Erick Bourgeois, firestoned"
license-id: "Apache-2.0"
# ─────────────────────────────────────────────────────────────────────────────
# Commit signatures — verify-mode differs by event type.
# ─────────────────────────────────────────────────────────────────────────────
verify-commits:
name: 🔐 Verify Signed Commits
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Verify commits (PR)
if: github.event_name == 'pull_request'
uses: firestoned/github-actions/security/verify-signed-commits@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
base-ref: origin/${{ github.base_ref }}
verify-mode: pr
- name: Verify commits (push to main)
if: github.event_name == 'push'
uses: firestoned/github-actions/security/verify-signed-commits@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
verify-mode: push
- name: Verify commits (release)
if: github.event_name == 'release'
uses: firestoned/github-actions/security/verify-signed-commits@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
verify-mode: release
# ─────────────────────────────────────────────────────────────────────────────
# PR only: formatting + clippy.
# ─────────────────────────────────────────────────────────────────────────────
format:
name: 🎨 Check Formatting
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [license-check, verify-commits]
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly)
with:
components: rustfmt
- name: Check formatting
run: cargo fmt -- --check
clippy:
name: 📎 Clippy
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [format]
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly)
with:
components: clippy
- name: Cache cargo dependencies
uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
# ─────────────────────────────────────────────────────────────────────────────
# Extract version — all events; downstream build jobs depend on this.
# format/clippy are independent PR quality checks and do not gate the build.
# ─────────────────────────────────────────────────────────────────────────────
extract-version:
name: 🏷️ Extract Version Information
runs-on: ubuntu-latest
needs: [license-check, verify-commits]
outputs:
version: ${{ steps.version-chainguard.outputs.version }}
tag_name: ${{ steps.version-chainguard.outputs.tag-name }}
image-tag-chainguard: ${{ steps.version-chainguard.outputs.image-tag }}
image-tag-distroless: ${{ steps.version-distroless.outputs.image-tag }}
image-repository-chainguard: ${{ steps.version-chainguard.outputs.image-repository }}
image-repository-distroless: ${{ steps.version-distroless.outputs.image-repository }}
short-sha: ${{ steps.version-chainguard.outputs.short-sha }}
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Extract version for Chainguard
id: version-chainguard
uses: ./.github/actions/extract-version
with:
repository: ${{ github.repository }}
workflow-type: ${{ github.event_name == 'pull_request' && 'pr' || github.event_name == 'push' && 'main' || 'release' }}
pr-number: ${{ github.event.pull_request.number }}
release-tag: ${{ github.event.release.tag_name }}
image-tag-suffix: ""
- name: Extract version for Distroless
id: version-distroless
uses: ./.github/actions/extract-version
with:
repository: ${{ github.repository }}
workflow-type: ${{ github.event_name == 'pull_request' && 'pr' || github.event_name == 'push' && 'main' || 'release' }}
pr-number: ${{ github.event.pull_request.number }}
release-tag: ${{ github.event.release.tag_name }}
image-tag-suffix: "-distroless"
# ─────────────────────────────────────────────────────────────────────────────
# Build binary — all events.
# On release the Cargo.toml version is bumped to match the release tag.
# ─────────────────────────────────────────────────────────────────────────────
build:
name: 🦀 Build - ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.runner }}
needs: [extract-version]
strategy:
fail-fast: false
matrix:
platform:
- name: Linux x86_64
runner: ubuntu-24.04
artifact_name: 5spot-linux-amd64
binary_name: 5spot
- name: Linux ARM64
runner: ubuntu-24.04-arm
artifact_name: 5spot-linux-arm64
binary_name: 5spot
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Update Cargo.toml version
if: github.event_name == 'release'
run: |
perl -i -pe 's/^version = ".*"/version = "${{ needs.extract-version.outputs.version }}"/' Cargo.toml
echo "Updated Cargo.toml to version ${{ needs.extract-version.outputs.version }}"
grep '^version = ' Cargo.toml
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly)
- name: Cache cargo dependencies
uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
- name: Build binary
run: cargo build --release
- name: Install cargo-cyclonedx
run: cargo install cargo-cyclonedx --locked --quiet
- name: Generate SBOM
run: |
cargo cyclonedx --format json
find . -maxdepth 1 -name "*.cdx.json" -exec mv {} target/release/ \;
# PR and push artifacts expire after 1 day — they are not release assets.
- name: Upload artifacts (PR / push)
if: github.event_name != 'release'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.platform.artifact_name }}
path: |
target/release/${{ matrix.platform.binary_name }}
target/release/*.cdx.json
retention-days: 1
# Release artifacts keep the default retention so re-runs remain possible.
- name: Upload artifacts (release)
if: github.event_name == 'release'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.platform.artifact_name }}
path: |
target/release/${{ matrix.platform.binary_name }}
target/release/*.cdx.json
# ─────────────────────────────────────────────────────────────────────────────
# Tests — all events; running unconditionally lets docker depend on it simply.
# ─────────────────────────────────────────────────────────────────────────────
test:
name: 🧪 Test
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly)
- name: Download x86_64 build artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: 5spot-linux-amd64
path: target/release/
- name: Cache cargo dependencies
uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
- name: Run tests
run: cargo test --all --verbose
# ─────────────────────────────────────────────────────────────────────────────
# Docker build and push — all events.
#
# Metadata tags vary by event (three separate metadata steps, only one runs
# per invocation; empty outputs from skipped steps are filtered out by
# docker/build-push-action). On release, Cosign signing and SBOM generation
# steps are also enabled.
# ─────────────────────────────────────────────────────────────────────────────
docker:
name: 🐳 Build and Push Docker Image - ${{ matrix.variant.name }}
runs-on: ubuntu-latest
needs: [build, extract-version, test]
permissions:
contents: read
packages: write
id-token: write
outputs:
digest-chainguard: ${{ steps.export-digest.outputs.digest-chainguard }}
digest-distroless: ${{ steps.export-digest.outputs.digest-distroless }}
strategy:
fail-fast: false
matrix:
variant:
- name: Chainguard
dockerfile: Dockerfile.chainguard
suffix: ""
description: "5-Spot Chainguard Zero-CVE (glibc, FIPS-ready)"
image-tag: ${{ needs.extract-version.outputs.image-tag-chainguard }}
image-repository: ${{ needs.extract-version.outputs.image-repository-chainguard }}
- name: Distroless
dockerfile: Dockerfile
suffix: "-distroless"
description: "5-Spot Google Distroless (glibc)"
image-tag: ${{ needs.extract-version.outputs.image-tag-distroless }}
image-repository: ${{ needs.extract-version.outputs.image-repository-distroless }}
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Prepare Docker binaries
uses: ./.github/actions/prepare-docker-binaries
- name: Setup Docker environment
uses: firestoned/github-actions/docker/setup-docker@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Only one of the three metadata steps below runs per job instance.
# docker/build-push-action concatenates all tags/labels outputs and
# silently drops the empty lines produced by the two skipped steps.
- name: Extract metadata (PR)
id: meta-pr
if: github.event_name == 'pull_request'
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ghcr.io/${{ matrix.variant.image-repository }}
tags: |
type=raw,value=${{ matrix.variant.image-tag }}
type=raw,value=sha-${{ needs.extract-version.outputs.short-sha }}
flavor: |
latest=false
- name: Extract metadata (push to main)
id: meta-main
if: github.event_name == 'push'
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ghcr.io/${{ matrix.variant.image-repository }}
tags: |
type=raw,value=${{ matrix.variant.image-tag }}
type=raw,value=main-{{date 'YYYY-MM-DD'}}
type=raw,value=sha-${{ needs.extract-version.outputs.short-sha }}
type=raw,value=latest
flavor: |
latest=false
- name: Extract metadata (release)
id: meta-release
if: github.event_name == 'release'
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ghcr.io/${{ matrix.variant.image-repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=sha-${{ needs.extract-version.outputs.short-sha }}${{ matrix.variant.suffix }}
flavor: |
latest=false
prefix=v
suffix=${{ matrix.variant.suffix }}
- name: Build and push Docker image (${{ matrix.variant.name }})
id: docker_build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ${{ matrix.variant.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ steps.meta-pr.outputs.tags }}
${{ steps.meta-main.outputs.tags }}
${{ steps.meta-release.outputs.tags }}
labels: |
${{ steps.meta-pr.outputs.labels }}
${{ steps.meta-main.outputs.labels }}
${{ steps.meta-release.outputs.labels }}
org.opencontainers.image.description=${{ matrix.variant.description }}
build-args: |
${{ github.event_name == 'release' && format('VERSION={0}', needs.extract-version.outputs.version) || '' }}
cache-from: type=gha,scope=${{ matrix.variant.name }}
cache-to: type=gha,mode=max,scope=${{ matrix.variant.name }}
sbom: true
provenance: true
# Push to main and release: sign the image with Cosign (keyless, Sigstore).
# PR images are ephemeral and not deployed, so signing them is skipped.
# Sign by digest (not tag) to guarantee we sign exactly what was pushed.
- name: Install Cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
- name: Sign container image with Cosign
if: github.event_name != 'pull_request'
run: |
cosign sign --yes \
ghcr.io/${{ matrix.variant.image-repository }}@${{ steps.docker_build.outputs.digest }}
# Release only: generate a CycloneDX SBOM for the published image.
- name: Generate Docker SBOM for ${{ matrix.variant.name }}
if: github.event_name == 'release'
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
image: ghcr.io/${{ matrix.variant.image-repository }}:${{ matrix.variant.image-tag }}
format: cyclonedx-json
output-file: docker-sbom-${{ matrix.variant.name }}.json
upload-release-assets: false
- name: Upload Docker SBOM as artifact
if: github.event_name == 'release'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: docker-sbom-${{ matrix.variant.name }}
path: docker-sbom-${{ matrix.variant.name }}.json
- name: Export image digest
id: export-digest
run: |
case "${{ matrix.variant.name }}" in
Chainguard) echo "digest-chainguard=${{ steps.docker_build.outputs.digest }}" >> "$GITHUB_OUTPUT" ;;
Distroless) echo "digest-distroless=${{ steps.docker_build.outputs.digest }}" >> "$GITHUB_OUTPUT" ;;
esac
# ─────────────────────────────────────────────────────────────────────────────
# GitHub Artifact Attestation — all events.
# ─────────────────────────────────────────────────────────────────────────────
attest:
name: 🔏 Attest Docker Image - ${{ matrix.variant.name }}
runs-on: ubuntu-latest
needs: [docker, extract-version]
permissions:
id-token: write
attestations: write
packages: write
strategy:
fail-fast: false
matrix:
variant:
- name: Chainguard
image-repository: ${{ needs.extract-version.outputs.image-repository-chainguard }}
digest: ${{ needs.docker.outputs.digest-chainguard }}
- name: Distroless
image-repository: ${{ needs.extract-version.outputs.image-repository-distroless }}
digest: ${{ needs.docker.outputs.digest-distroless }}
steps:
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Attest ${{ matrix.variant.name }} image provenance
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-name: ghcr.io/${{ matrix.variant.image-repository }}
subject-digest: ${{ matrix.variant.digest }}
push-to-registry: true
# ─────────────────────────────────────────────────────────────────────────────
# Security scan — all events.
# Gitleaks secret scan only runs on PRs; release uploads the audit report.
# ─────────────────────────────────────────────────────────────────────────────
security:
name: 🛡️ Security Vulnerability Scan
runs-on: ubuntu-latest
needs: [verify-commits]
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly)
- name: Run security scan (PR / push)
if: github.event_name != 'release'
uses: firestoned/github-actions/rust/security-scan@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
cargo-audit-version: "0.22.0"
- name: Run security scan (release)
if: github.event_name == 'release'
uses: firestoned/github-actions/rust/security-scan@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
cargo-audit-version: "0.22.0"
upload-artifact-name: cargo-audit-report-release
- name: Run gitleaks (secret scanning)
if: github.event_name == 'pull_request'
run: make gitleaks
# ─────────────────────────────────────────────────────────────────────────────
# SAST (Semgrep OSS) — PR + push to main.
# Runs the community Rust / security-audit / secrets / OWASP Top Ten rulesets
# and uploads SARIF to the Code Scanning dashboard. No token required — this
# is the free OSS Community Edition; findings appear under the `semgrep` tool
# category in the Security tab.
# ─────────────────────────────────────────────────────────────────────────────
semgrep-sast:
name: 🔎 SAST (Semgrep)
if: github.event_name != 'release'
runs-on: ubuntu-latest
needs: [verify-commits]
permissions:
contents: read
security-events: write
container:
image: returntocorp/semgrep
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Run Semgrep
run: |
semgrep scan \
--config=p/rust \
--config=p/security-audit \
--config=p/secrets \
--config=p/owasp-top-ten \
--sarif --output=semgrep.sarif \
--metrics=off \
--error || true
- name: Upload Semgrep SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a # v3.35.2
with:
sarif_file: semgrep.sarif
category: semgrep
# ─────────────────────────────────────────────────────────────────────────────
# IaC security scan (Trivy config) — PR + push to main.
# Scans Kubernetes manifests under deploy/ and the Dockerfiles for
# misconfigurations. Uploads SARIF to the Code Scanning dashboard.
# ─────────────────────────────────────────────────────────────────────────────
iac-scan:
name: 🏗️ IaC Security Scan (Trivy config)
if: github.event_name != 'release'
runs-on: ubuntu-latest
needs: [verify-commits]
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Run Trivy config scan
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
scan-type: config
scan-ref: .
format: sarif
output: trivy-iac.sarif
severity: CRITICAL,HIGH,MEDIUM
exit-code: '0'
- name: Upload Trivy IaC SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a # v3.35.2
with:
sarif_file: trivy-iac.sarif
category: trivy-iac
# ─────────────────────────────────────────────────────────────────────────────
# cargo-deny — PR + push to main.
# Enforces the license allow-list, RustSec advisories (same DB as
# cargo-audit), and forbids dependencies from unknown registries or git
# URLs. Configured via deny.toml at repo root.
# ─────────────────────────────────────────────────────────────────────────────
cargo-deny:
name: 🧪 cargo-deny (License + Advisory + Sources)
if: github.event_name != 'release'
runs-on: ubuntu-latest
needs: [verify-commits]
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # v2.0.17
with:
log-level: warn
command: check
arguments: --all-features
# ─────────────────────────────────────────────────────────────────────────────
# Container security scan — all events.
# ─────────────────────────────────────────────────────────────────────────────
trivy:
name: 🔭 Container Security Scan - ${{ matrix.variant.name }}
runs-on: ubuntu-latest
needs: [docker, extract-version]
permissions:
contents: read
packages: read
id-token: write
security-events: write
strategy:
fail-fast: false
matrix:
variant:
- name: Chainguard
image-tag: ${{ needs.extract-version.outputs.image-tag-chainguard }}
image-repository: ${{ needs.extract-version.outputs.image-repository-chainguard }}
- name: Distroless
image-tag: ${{ needs.extract-version.outputs.image-tag-distroless }}
image-repository: ${{ needs.extract-version.outputs.image-repository-distroless }}
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run Trivy scan on ${{ matrix.variant.name }} image
uses: firestoned/github-actions/security/trivy-scan@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
image-ref: ghcr.io/${{ matrix.variant.image-repository }}:${{ matrix.variant.image-tag }}
sarif-category: trivy-${{ github.event_name }}-${{ matrix.variant.name }}
upload-artifact-name: ${{ github.event_name == 'release' && format('trivy-release-{0}', matrix.variant.name) || format('trivy-main-{0}-{1}', matrix.variant.name, github.run_number) }}
output-filename: trivy-${{ github.event_name }}-${{ matrix.variant.name }}.json
# ─────────────────────────────────────────────────────────────────────────────
# Push-to-main and release: sign binaries with Cosign and emit GitHub
# artifact attestations. Skipped on PRs.
# ─────────────────────────────────────────────────────────────────────────────
sign-artifacts:
name: ✍️ Sign Binary Artifacts
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: [build, extract-version]
permissions:
id-token: write
attestations: write
contents: read
strategy:
fail-fast: false
matrix:
platform:
- artifact_name: 5spot-linux-amd64
binary_name: 5spot
- artifact_name: 5spot-linux-arm64
binary_name: 5spot
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Download binary artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ matrix.platform.artifact_name }}
path: ./artifacts/${{ matrix.platform.artifact_name }}
- name: Create tarball for signing
run: |
cd artifacts/${{ matrix.platform.artifact_name }}
chmod +x ${{ matrix.platform.binary_name }}
tar czf ${{ matrix.platform.artifact_name }}.tar.gz ${{ matrix.platform.binary_name }}
ls -lh ${{ matrix.platform.artifact_name }}.tar.gz
- name: Sign binary tarball with Cosign
uses: firestoned/github-actions/security/cosign-sign@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
with:
artifact-path: artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz
- name: Attest binary tarball provenance
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.platform.artifact_name }}-signed
path: |
artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz
artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz.bundle
# ─────────────────────────────────────────────────────────────────────────────
# Push-to-main and release: compute SHA-256 hashes of signed tarballs for SLSA.
# ─────────────────────────────────────────────────────────────────────────────
generate-provenance-subjects:
name: 🔬 Generate SLSA Provenance Subjects
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: [sign-artifacts, extract-version]
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Download signed artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: 5spot-*-signed
path: ./artifacts
merge-multiple: true
- name: Generate hashes for SLSA provenance
id: hash
run: |
cd artifacts
sha256sum *.tar.gz > checksums.txt
cat checksums.txt
echo "hashes=$(cat checksums.txt | base64 -w0)" >> "$GITHUB_OUTPUT"
# ─────────────────────────────────────────────────────────────────────────────
# Push-to-main and release: SLSA Level 3 provenance via the official generator.
# `upload-assets` only applies on release (there is no GitHub Release object
# on a push-to-main to attach assets to); on push the provenance lands as a
# workflow artifact, verifiable with slsa-verifier or `gh attestation verify`.
# ─────────────────────────────────────────────────────────────────────────────
slsa-provenance:
name: 🏛️ Generate SLSA Provenance
if: github.event_name != 'pull_request'
needs: [generate-provenance-subjects, extract-version]
permissions:
actions: read
id-token: write
contents: write
# MUST remain on a semver tag, not a SHA: SLSA Level 3 verification
# requires the generator ref to be one of the approved released versions.
# SHA-pinning this line would break slsa-verifier attestation validation.
# See https://github.com/slsa-framework/slsa-github-generator#verification-of-provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: "${{ needs.generate-provenance-subjects.outputs.hashes }}"
upload-assets: ${{ github.event_name == 'release' }}
provenance-name: "${{ needs.extract-version.outputs.version }}.intoto.jsonl"
# ─────────────────────────────────────────────────────────────────────────────
# Release only: regenerate and package the CRD deploy manifests.
# ─────────────────────────────────────────────────────────────────────────────
package-deploy-manifests:
name: 📦 Package Deployment Manifests
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: [extract-version]
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly)
- name: Cache cargo dependencies
uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
- name: Regenerate CRDs
run: cargo run --quiet --bin crdgen > deploy/crds/scheduledmachine.yaml
- name: Package deploy manifests
run: tar czf deploy-manifests.tar.gz deploy/
- name: Upload deploy manifests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: deploy-manifests
path: deploy-manifests.tar.gz
# ─────────────────────────────────────────────────────────────────────────────
# Release only: collate all artifacts and upload to the GitHub Release.
# ─────────────────────────────────────────────────────────────────────────────
upload-release-assets:
name: 🚀 Upload Release Assets
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: [build, docker, attest, sign-artifacts, slsa-provenance, package-deploy-manifests]
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: ./artifacts
pattern: "{5spot-*,docker-sbom-*,*.intoto.jsonl,deploy-manifests}"
- name: 🧹 Organize release artifacts
run: |
cd artifacts
mkdir -p release sboms signatures provenance
# Copy signed tarballs and signature bundles
for dir in 5spot-*-signed; do
if [ -d "$dir" ]; then
platform="${dir%-signed}"
if compgen -G "$dir/*.tar.gz" > /dev/null; then
cp "$dir"/*.tar.gz release/
fi
if compgen -G "$dir/*.tar.gz.bundle" > /dev/null; then
cp "$dir"/*.tar.gz.bundle signatures/
fi
fi
done
# Collect binary SBOMs (Linux builds only)
for dir in 5spot-linux-amd64 5spot-linux-arm64; do
if [ -d "$dir" ] && compgen -G "$dir/*.cdx.json" > /dev/null; then
cp "$dir"/*.cdx.json "sboms/${dir}-sbom.json"
fi
done
# Copy Docker SBOMs
for variant in Chainguard Distroless; do
if [ -d "docker-sbom-${variant}" ] && [ -f "docker-sbom-${variant}/docker-sbom-${variant}.json" ]; then
cp "docker-sbom-${variant}/docker-sbom-${variant}.json" "sboms/docker-sbom-${variant}.json"
fi
done
# Copy SLSA provenance
find . -name "*.intoto.jsonl" -type f -exec cp {} provenance/ \;
# Copy deploy manifests
if [ -f "deploy-manifests/deploy-manifests.tar.gz" ]; then
cp deploy-manifests/deploy-manifests.tar.gz release/
fi
# Generate SHA256 checksums
cd release && sha256sum * > checksums.sha256
cd ../sboms && sha256sum *.json >> ../release/checksums.sha256
cd ../signatures && sha256sum *.bundle >> ../release/checksums.sha256
cd ../provenance && compgen -G "*.intoto.jsonl" > /dev/null && sha256sum *.intoto.jsonl >> ../release/checksums.sha256 || true
echo "Release artifacts:"
ls -lh ../release/
- name: Upload release assets
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: |
artifacts/release/*.tar.gz
artifacts/sboms/*.json
artifacts/signatures/*.bundle
artifacts/provenance/*.intoto.jsonl
artifacts/release/checksums.sha256