Release-cycle runbook¶
This runbook drives a v0.N.0 release of apothem from a clean
working tree to ten distribution surfaces verified 200 OK. The
procedure is written for the maintainer wielding gh, git, and the
release-tier shell; it terminates with a verified Release page, a
filed CHANGELOG, and an install-path smoke confirming each Tier-3
surface answers the canonical install command.
The release shape is single-tag, multi-surface. One signed
annotated tag on main triggers the CI release workflow; the
workflow fans out to ten packaging surfaces (GitHub Releases for
darwin / linux / windows; PyPI; Homebrew; Scoop; winget; AUR;
Debian; RPM); the [Released] CHANGELOG section is authored
alongside the tag commit; the operator runs the install-path smoke
after the workflow completes.
1. Prerequisites¶
The maintainer confirms the following before tagging:
- Working tree. Clean (
git statusshows nothing pending). All feature work for the release is onmain. - CI green. Latest
maingh run list --branch=main --limit=1 --json conclusion --jq '.[0].conclusion'returnssuccess. - Tooling.
gh(GitHub CLI) version 2.40 or later authenticated againstGad360;git2.40 or later with the GPG signing subkey loaded; theMAINTAINERPyPI trusted-publisher binding is live (verified atpypi.org/manage/account/publishing/). - Version anchors aligned. The version string declared at
pyproject.tomlmatches the planned tag without thevprefix. Verify:
grep '^version' pyproject.toml
Expected output (for a v0.1.0 release):
version = "0.1.0"
- CHANGELOG draft staged. A
[Unreleased]section inCHANGELOG.mdcarries the curated headline features; the maintainer will rename it to[Released — YYYY-MM-DD]in the same commit that lands the tag.
2. Tag and trigger¶
2.1 Author the release commit¶
The release commit lands two changes:
CHANGELOG.md[Unreleased]section renamed to[Released — YYYY-MM-DD]with the actual UTC date of the release.pyproject.tomlversion bumped to the new version (only when the prior commit onmainalready carries the previous-version pin and a fresh bump is required; many releases author this commit during the version-bump cycle days before tag time).
Example commit:
git checkout main
git pull --ff-only origin main
# Author CHANGELOG and pyproject.toml edits via Edit tool, then:
git add CHANGELOG.md pyproject.toml
git commit -S -m "chore(release): cut v0.1.0"
git push origin main
The -S flag signs the commit with the GPG key. The push triggers
the standard CI matrix (lint, test, build); wait for green before
tagging.
2.2 Sign and push the tag¶
git tag -a -s v0.1.0 -m "v0.1.0 — initial release"
git push origin v0.1.0
The -a -s combination produces an annotated, GPG-signed tag. The
push triggers .github/workflows/release.yml, which is the
fan-out engine.
2.3 Confirm the workflow fanned out¶
gh run watch $(gh run list --workflow=release.yml --limit=1 --json databaseId --jq '.[0].databaseId')
The workflow runs to completion. Each surface's job emits a
log section; the maintainer scans for 200 OK upload confirmations
on every surface.
3. The ten distribution surfaces¶
The release fans out to ten surfaces in parallel. Each surface has a verification command answering "did the release land here?":
| # | Surface | Artifact | Verification |
|---|---|---|---|
| 1 | GitHub Releases (darwin) | apothem-v0.1.0-darwin.tar.gz |
gh release view v0.1.0 --json assets --jq '.assets[].name' includes darwin.tar.gz |
| 2 | GitHub Releases (linux) | apothem-v0.1.0-linux.tar.gz |
same as row 1; asset name includes linux.tar.gz |
| 3 | GitHub Releases (windows) | apothem-v0.1.0-windows.zip |
same as row 1; asset name includes windows.zip |
| 4 | PyPI | apothem-0.1.0-py3-none-any.whl + sdist |
pip index versions apothem lists 0.1.0 (or pip install apothem==0.1.0 succeeds) |
| 5 | Homebrew | Gad360/apothem/Formula/apothem.rb |
brew tap gad360/apothem https://github.com/Gad360/apothem.git && brew install gad360/apothem/apothem exits 0 |
| 6 | Scoop | Gad360/apothem/bucket/apothem.json |
scoop bucket add apothem https://github.com/Gad360/apothem && scoop install apothem/apothem exits 0 |
| 7 | winget | manifests/g/Gad360/apothem/0.1.0/*.yaml (PR landed) |
winget install Gad360.apothem --version 0.1.0 exits 0 |
| 8 | AUR | Gad360/aur-apothem/PKGBUILD |
yay -S apothem (or paru -S apothem) exits 0 |
| 9 | Debian | apothem_0.1.0-1_amd64.deb |
apt install ./apothem_0.1.0-1_amd64.deb exits 0 (downloaded from the GH Release) |
| 10 | RPM | apothem-0.1.0-1.noarch.rpm |
dnf install ./apothem-0.1.0-1.noarch.rpm exits 0 (downloaded from the GH Release) |
Some surfaces (Homebrew, Scoop, winget, AUR) update via PR to a sibling repository or to the upstream registry rather than a direct write. The CI workflow's per-surface job opens the PR; the maintainer reviews and merges; then the surface goes live. The PR links land in the workflow's job summary.
4. CHANGELOG discipline¶
The CHANGELOG.md carries one section per release under the
Keep-a-Changelog convention. The release commit's CHANGELOG edit:
- Renames
[Unreleased]to[Released — YYYY-MM-DD](the actual UTC date the tag was pushed). - Re-creates a fresh empty
[Unreleased]section above it for the next cycle. - Confirms the six sub-sections — Added, Changed, Deprecated, Removed, Fixed, Security — are populated per the spec's curation rules. Empty sub-sections are dropped from the rendered file (Keep-a-Changelog allows this).
- Captures capabilities, not implementation. Domain language only; no plan-internal references; no commit hashes; no plan or phase identifiers.
A CHANGELOG section that survived the maintainer's review reads as the release-notes anchor for the announcement narrative.
5. Install-path smoke¶
After the release workflow completes and every surface PR is merged, the maintainer runs the smoke. The smoke is the canonical verification that the user-facing install commands answer.
5.1 macOS (Homebrew)¶
brew tap gad360/apothem https://github.com/Gad360/apothem.git
brew install apothem
apothem --version
Expected output: apothem 0.1.0.
5.2 macOS / Linux (PyPI)¶
pip install apothem==0.1.0
apothem --version
Expected output: apothem 0.1.0.
5.3 Linux (Debian)¶
curl -fsSLO https://github.com/Gad360/apothem/releases/download/v0.1.0/apothem_0.1.0-1_amd64.deb
sudo apt install ./apothem_0.1.0-1_amd64.deb
apothem --version
Expected output: apothem 0.1.0.
5.4 Linux (RPM)¶
curl -fsSLO https://github.com/Gad360/apothem/releases/download/v0.1.0/apothem-0.1.0-1.noarch.rpm
sudo dnf install ./apothem-0.1.0-1.noarch.rpm
apothem --version
Expected output: apothem 0.1.0.
5.5 Linux (AUR)¶
yay -S apothem
apothem --version
Expected output: apothem 0.1.0.
5.6 Windows (Scoop)¶
scoop bucket add apothem https://github.com/Gad360/apothem
scoop install apothem
apothem --version
Expected output: apothem 0.1.0.
5.7 Windows (winget)¶
winget install Gad360.apothem --version 0.1.0
apothem --version
Expected output: apothem 0.1.0.
5.8 Tarball (any POSIX)¶
curl -fsSL https://github.com/Gad360/apothem/releases/download/v0.1.0/apothem-v0.1.0-linux.tar.gz | tar -xz
./apothem-v0.1.0-linux/apothem --version
Expected output: apothem 0.1.0.
The smoke produces a one-line PASS / FAIL row per surface; record the rows in the maintainer's release log.
6. Failure recovery¶
The release workflow has six documented failure modes:
| Failure | Diagnostic | Recovery |
|---|---|---|
| Tag push rejected (signing failure) | git push origin v0.1.0 returns error: failed to push some refs with a GPG-related message |
Verify the GPG subkey is loaded (gpg --list-secret-keys --keyid-format=long); re-tag locally with -s after fixing the keyring; re-push |
release.yml job fails on a single surface |
gh run view <run-id> --log names the failing surface job |
Re-run the failed surface job alone via gh run rerun <run-id> --failed; if the failure repeats, the surface needs a per-surface fix at its packaging manifest |
| PyPI upload rejected (trusted-publisher binding) | The PyPI job emits 403 Forbidden |
Verify the trusted-publisher binding at pypi.org/manage/account/publishing/ lists the new release tag's commit SHA as authorized |
| Homebrew formula PR rejected | The Homebrew CI emits a checksum mismatch | The release tarball SHA-256 does not match the formula's recorded checksum; regenerate via shasum -a 256 apothem-v0.1.0-darwin.tar.gz; update the formula PR |
| winget PR rejected | The winget CI emits a manifest schema error | Validate the manifest locally via winget validate manifests/g/Gad360/apothem/0.1.0/; fix; push the manifest update |
| Tag visible but assets missing | gh release view v0.1.0 returns a stub release |
Re-run the release workflow's asset-upload job; if the stub persists, delete the release page (gh release delete v0.1.0 --cleanup-tag) and re-tag from the same SHA |
If a recovery cycle fails to land a surface within one diagnostic
pass, the surface is logged as a known-failure in the release notes
with a link to the open issue; downstream users are pointed to the
working surfaces while the broken one is fixed in a v0.N.1
patch release.
7. Cross-references¶
- Spec source. Specification §2.4 enumerates the ten Tier-3 packaging surfaces; §2.6 governs CHANGELOG format; §3 ratifies the per-decision audit trail (D-18 through D-23 for the packaging surfaces, D-27/D-28 for CHANGELOG framing).
- Recovery flow. The release-recovery runbook (
docs/runbooks/ release-recovery.md) governs the cross-stage failure path during a release cutover, not the per-surface release failures named in §6. - Pages enablement. The Pages-build job inside
release.ymlre-validates the doc-site renders at the custom domain; the Pages-enablement runbook (docs/runbooks/pages-enablement.md) is the upstream procedure that established the custom domain in the first place. - Versioning policy.
docs/release-engineering-policy.mdcarries the SemVer + signing + deprecation policy this runbook honours.