Release¶
The release process. Versioning, tagging, changelog management, and (eventually) PyPI publishing.
Versioning¶
SemVer with explicit phase tags:
| Tag | Phase | What ships |
|---|---|---|
v0.1.0a0 |
Phase 1 | Scaffold, taxonomy, exceptions, CLI surface, CI matrix, docs |
v0.2.0a0 |
Phase 2 | Manifest schema, classifier with parity tests |
v0.3.0a0 |
Phase 3 | Posture state machine, receipts, hook adapter, end-to-end |
Bug-fix releases within a phase use the aN+1 suffix (v0.1.0a1, v0.1.0a2, ...). The phase number bumps on phase exit.
PyPI publish is gated on a project-level decision, not on CI alone. The package may stay source-only beyond v0.3.0a0 if there's reason to.
Phase exit gate¶
A phase ships only when every item on its exit gate is verified. The gates live in docs/history/phase-1.md (and equivalent files for later phases) and in RECEIPTS.md.
Phase 1 exit gate (recap):
- Repo public on GitHub.
- CI green on all 9 matrix cells (3.11/3.12/3.13 × Linux/macOS/Windows).
- Docs deployed to GitHub Pages.
pip install -e .works in a fresh venv.python -c "import spine_lite; print(spine_lite.__version__)"returns the target version.- The phase's headline test file passes (Phase 1:
tests/unit/test_effects.py). - CHANGELOG entry for the new version.
- CLAUDE.md ≤ 150 lines.
- All commits in Conventional Commits format.
- Receipt appended to
RECEIPTS.mdand mirrored to the operator's run-registry.
Cut a release¶
# 1. Verify the gate (run from a clean checkout)
nox -s lint typecheck test coverage docs
# 2. Bump the version in pyproject.toml and __init__.py
# (One-line change in both files; keep them in sync.)
# 3. Update CHANGELOG.md — move [Unreleased] entries to [vX.Y.Za0]
# Add the date in YYYY-MM-DD form. Add the comparison link at the bottom.
# 4. Append the phase exit receipt to RECEIPTS.md
# 5. Commit and tag
git add pyproject.toml src/spine_lite/__init__.py CHANGELOG.md RECEIPTS.md
git commit -m "release: vX.Y.Za0"
git tag -a vX.Y.Za0 -m "Phase N exit"
# 6. Push tag and branch
git push origin main
git push origin vX.Y.Za0
CI re-runs on the tag push; verify all cells green before announcing.
Build artifacts¶
uv build
ls dist/
# spine_lite-X.Y.ZaN-py3-none-any.whl
# spine_lite-X.Y.ZaN.tar.gz
Inspect the wheel's metadata and contents:
uv run python -m zipfile -l dist/spine_lite-X.Y.ZaN-py3-none-any.whl
uv run python -m wheel unpack dist/spine_lite-X.Y.ZaN-py3-none-any.whl -d /tmp/wheel-check
The wheel must contain spine_lite/py.typed. Without it, downstream type-checkers won't pick up annotations.
PyPI publish (gated on project-level approval)¶
# Test PyPI first
uv publish --publish-url https://test.pypi.org/legacy/
# Then real PyPI, only after the test install verifies clean
uv publish
The uv publish command reads credentials from UV_PUBLISH_TOKEN or ~/.pypirc. Don't put credentials in the repo or in CI secrets without project-level approval.
Rollback¶
If a release goes out broken:
- Don't
pip install --force-reinstallto fix it for users — they'll keep the broken version cached. - Yank the bad release on PyPI (
uv publish --yankor via the PyPI web UI). Yanked releases stay installable by exact-version pin but are excluded from new resolutions. - Cut a fix release with the next
aN+1suffix. - Add a CHANGELOG entry for the yanked version explaining what was wrong.
- Append a
release: yank vX.Y.Za0receipt toRECEIPTS.md.
Yanking is a one-way door at the user-experience level — once a release is out, treat it as permanent in your changelog even if you yank it from PyPI.
See also¶
- Phase 1 History — what shipped in the first release.
- Explanation / Invariants — what every release must preserve.