Skip to content

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 status shows nothing pending). All feature work for the release is on main.
  • CI green. Latest main gh run list --branch=main --limit=1 --json conclusion --jq '.[0].conclusion' returns success.
  • Tooling. gh (GitHub CLI) version 2.40 or later authenticated against Gad360; git 2.40 or later with the GPG signing subkey loaded; the MAINTAINER PyPI trusted-publisher binding is live (verified at pypi.org/manage/account/publishing/).
  • Version anchors aligned. The version string declared at pyproject.toml matches the planned tag without the v prefix. Verify:
grep '^version' pyproject.toml

Expected output (for a v0.1.0 release):

version = "0.1.0"
  • CHANGELOG draft staged. A [Unreleased] section in CHANGELOG.md carries 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.toml version bumped to the new version (only when the prior commit on main already 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.yml re-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.md carries the SemVer + signing + deprecation policy this runbook honours.