Skip to content

release

release #4

Workflow file for this run

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (C) 2026 RS-Key contributors
# Cut a release from a pushed `v*` tag.
#
# `build` builds the 8 firmware flavors reproducibly via `nix build`, then GATES
# on a bit-identical rebuild of all eight (a non-reproducible image fails the job
# before anything is published), generates a CycloneDX SBOM, hashes everything
# into SHA256SUMS, signs that with keyless cosign (sigstore/Fulcio via OIDC — no
# private key), attests GitHub build provenance for every .uf2, and publishes the
# GitHub Release.
#
# The provenance is a GitHub attestation (Sigstore-signed, in the attestation API
# + the public Rekor log), NOT a release asset — so it stays compatible with
# immutable releases. A consumer verifies which workflow / commit / runner built
# each .uf2 with `gh attestation verify` (docs/supply-chain.md).
#
# The .uf2 images are UNSIGNED for secure boot — cosign + the SLSA provenance
# attest the BUILD, not the boot seal. On a secure-boot device, seal an image
# with your own key before flashing (docs/production.md). Verifying a download:
# docs/releases.md and docs/supply-chain.md.
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "tag to (re)build a release for, e.g. v0.1.0"
required: true
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 150
permissions:
contents: write # create the release + upload assets
id-token: write # keyless cosign + the attestation's Fulcio OIDC token
attestations: write # GitHub build-provenance attestation (immutable-safe)
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
ref: ${{ github.event.inputs.tag || github.ref }}
- uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
- uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2
with:
primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'flake.lock') }}
restore-keys: cargo-${{ runner.os }}-
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: resolve tag
id: tag
run: |
tag="${{ github.event.inputs.tag || github.ref_name }}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
- name: build the 8 reproducible firmware flavors
run: |
tag="${{ steps.tag.outputs.tag }}"
mkdir -p dist
for pkg in firmware firmware-pqc firmware-fips firmware-fips-pqc \
firmware-no-touch firmware-no-touch-pqc \
firmware-no-touch-fips firmware-no-touch-fips-pqc; do
echo "::group::nix build .#$pkg"
out="$(nix build ".#$pkg" --no-link --print-out-paths)"
label="${pkg#firmware}"; label="${label#-}"
[ -z "$label" ] && label="default"
cp "$out/$pkg.uf2" "dist/rs-key-${tag}-${label}.uf2"
echo "::endgroup::"
done
ls -l dist
- name: reproducibility gate — rebuild all 8, require bit-identical
run: |
# `nix build --rebuild` recompiles the derivation already in the store
# and fails with a hash mismatch if the output is not bit-identical.
# A non-reproducible flavor fails here, before the release is created.
for pkg in firmware firmware-pqc firmware-fips firmware-fips-pqc \
firmware-no-touch firmware-no-touch-pqc \
firmware-no-touch-fips firmware-no-touch-fips-pqc; do
echo "::group::nix build .#$pkg --rebuild"
nix build ".#$pkg" --rebuild --no-link
echo "::endgroup::"
done
echo "all 8 flavors rebuilt bit-identical"
- name: generate the CycloneDX SBOM
run: |
tag="${{ steps.tag.outputs.tag }}"
# Scoped to the firmware crate (the shipped artifact) and its build
# target; cargo-cyclonedx writes firmware/firmware.cdx.json (plus a
# per-crate file for every workspace member, which we ignore).
nix develop -c cargo cyclonedx \
--manifest-path firmware/Cargo.toml \
--target thumbv8m.main-none-eabihf \
--format json
sbom="firmware/firmware.cdx.json"
[ -f "$sbom" ] || sbom="$(ls firmware/*.cdx.json 2>/dev/null | head -1)"
if [ -z "$sbom" ] || [ ! -f "$sbom" ]; then echo "no SBOM produced" >&2; exit 1; fi
cp "$sbom" "dist/rs-key-${tag}-sbom.cdx.json"
- name: checksums
run: |
cd dist
sha256sum ./*.uf2 ./*.cdx.json > SHA256SUMS
cat SHA256SUMS
- name: attest build provenance
# GitHub-native build provenance for every .uf2 — signed keyless via
# Sigstore/Fulcio against this run's OIDC identity, recorded in the GitHub
# attestation API + the public Rekor log (NOT a release asset, so it is
# compatible with immutable releases). Verify with `gh attestation verify`.
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: dist/*.uf2
- name: sign SHA256SUMS (keyless cosign)
env:
COSIGN_YES: "true"
run: |
cosign sign-blob \
--bundle dist/SHA256SUMS.cosign.bundle \
dist/SHA256SUMS
- name: extract release notes from CHANGELOG
run: |
ver="${{ steps.tag.outputs.version }}"
awk -v v="$ver" '
$0 ~ "^## \\[" v "\\]" { f = 1; print; next }
/^## \[/ { if (f) exit }
f { print }
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "RS-Key ${{ steps.tag.outputs.tag }}" > release-notes.md
fi
{
echo
echo "---"
echo "Verify a download: https://github.com/${{ github.repository }}/blob/${{ steps.tag.outputs.tag }}/docs/supply-chain.md"
} >> release-notes.md
cat release-notes.md
- name: create the GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="${{ steps.tag.outputs.tag }}"
gh release create "$tag" \
--title "RS-Key $tag" \
--notes-file release-notes.md \
dist/*