Releases are tag-driven. The workflow .github/workflows/release.yml runs the same quality gates as CI (lint, typecheck, tests + coverage, Firestore rules tests, validate-env, production build), generates an SBOM, builds release notes from merged PRs, and publishes a GitHub Release with sbom.json attached.
- After
develophas been reviewed and you are ready to ship a version (typically after the batchdevelop→mainrelease process described in CONTRIBUTING). - Use Semantic Versioning (
vMAJOR.MINOR.PATCH).
- Ensure
developis green (all required checks passing) and that the develop → main release PR has merged. - Cut the signed tag — preferred path is the Cut signed release tag GitHub Actions workflow, which uses the runner's OIDC token to sign via Sigstore (no local install needed):
- Go to Actions → Cut signed release tag → Run workflow.
- Inputs:
version=v0.3.0,ref=main(default),messageoptional. - The workflow creates a gitsign-signed annotated tag and pushes it. Pushing the tag triggers the Release workflow, which verifies the signature, signs the SBOMs via cosign, attests SLSA L2 provenance, and publishes the GitHub Release.
- Local-machine fallback (if Actions is unavailable) — see § Signed tags for one-time gitsign or GPG setup, then:
git checkout main && git pull git tag -s v0.3.0 -m "release: v0.3.0" git push origin v0.3.0
- Unsigned tags are rejected by the release workflow from v0.3.0 forward.
- Verify the GitHub Release on the Releases page — assets include
sbom.json,sbom.spdx.json,*.cosign.bundlesignature bundles, and a SLSA provenance attestation. - Update CHANGELOG.md with the released version (keep Keep a Changelog style).
From v0.3.0 forward, all release tags MUST be cryptographically signed. The release workflow (.github/workflows/release.yml) verifies the tag signature and refuses to publish unsigned tags.
Two supported signing paths — pick one and configure it once per maintainer machine:
gitsign signs git objects via Sigstore (the same trust root used by cosign for the release SBOMs). The signature is a short-lived x509 cert tied to your OIDC identity (GitHub / Google / Microsoft); there is no long-lived private key to protect or rotate.
# one-time install (macOS)
brew install sigstore/tap/gitsign
# one-time config for this repo (or use --global to apply everywhere)
git config --local commit.gpgsign true
git config --local tag.gpgsign true
git config --local gpg.x509.program gitsign
git config --local gpg.format x509When you run git tag -s …, gitsign opens an OIDC flow in your browser; pick your maintainer identity. The signature is verifiable later via:
gitsign verify --certificate-identity=<your-oidc-email> --certificate-oidc-issuer=https://github.com/login/oauth v0.3.0If you already maintain a GPG key for other projects, that works too. Upload the public key to your GitHub account so tags display the Verified badge on the Releases page.
# one-time config
git config --local user.signingkey <your-gpg-keyid>
git config --local commit.gpgsign true
git config --local tag.gpgsign trueRecord the key fingerprint in MAINTAINERS.md so reporters can pin it.
git fetch --tags --quiet
git tag -v v0.3.0Maintainers can also run the workflow from Actions → Release → Run workflow, passing a version string (e.g. v0.2.0). Ensure the checked-out ref matches the code you intend to release.
- OpenSSF Scorecard runs on a schedule and on pushes to
main; CodeQL may run via GitHub default code scanning — they complement but do not replace the release job's checks. - Container images are built in CI on
mainfor validation only (push: false); publishing signed images to a registry is optional future work.
If a release breaks production or ships an issue that warrants pulling:
- Don't delete the GitHub Release immediately. A deleted release page breaks any link a downstream consumer (or attacker scanning for missing tags) may have cached. Mark it as pre-release first via the GitHub UI to remove it from "Latest".
- Cut a follow-up release with the fix — a
vX.Y.Z+1patch is faster and clearer than reverting the tag. The original CHANGELOG entry stays; the new entry references it (e.g. "Reverts the X behavior introduced in v0.2.0; see v0.2.0 notes for context."). - If the issue is security-grade, follow the disclosure flow in
SECURITY.mdand coordinate the rollback with the disclosure timeline — public revert commit messages should not name the vulnerability before the embargoed advisory date. - If Sigstore signing failed mid-release (the workflow produced a tag + GitHub Release but the cosign step errored), re-run the release workflow via Actions → Release → Run workflow with the same version. Sigstore signing is idempotent against the same artifact hash.
- Communicate the rollback — post in
#announcementson Discord and add a note to the next release's CHANGELOG entry. Don't silently ship a fix.
Never:
git push --forceto delete a tag on the remote. Tags are public artifacts; force-deleting them invalidates SBOM signatures and breaks downstream attestation. Cut a new release instead.- Skip the CHANGELOG entry for a hotfix. Even one-line "reverts X" notes earn their keep when the next reviewer reads the history.