Skip to content

Commit 9f81f21

Browse files
docs: add RELEASING.md
The release workflow already implements the rc-first publish flow (``-rc`` tags route to TestPyPI; plain ``vX.Y.Z`` tags route to PyPI + GitHub Release), but the human-facing process wasn't written down. This is the maintainer-side companion to release.yml. Covers: the two-tag rc-then-real path, the pre-release checklist (CHANGELOG current, docs sweep, version pin, branch state, CI green), how to verify an rc from TestPyPI in a fresh venv (with the ``--extra-index-url`` workaround for transitive deps), how to tag the real release, how to iterate on rcs, and the yank-and-patch rollback path for a broken real release. Lives at repo root alongside CHANGELOG.md and AGENTS.md, matching the existing maintainer-doc convention. Not part of the mkdocs nav.
1 parent 472126d commit 9f81f21

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

RELEASING.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Releasing
2+
3+
How to cut a release of `openarmature`. For maintainers.
4+
5+
Every release goes through TestPyPI before PyPI. The release workflow
6+
(`.github/workflows/release.yml`) is tag-driven: tag pushes are the only
7+
trigger; nothing else publishes.
8+
9+
## The release path: rc first, then real
10+
11+
A release happens in two tag steps:
12+
13+
1. **`vX.Y.Z-rc1`** publishes to **TestPyPI only**. No PyPI upload, no
14+
GitHub Release. Use the rc to verify the artifact installs, the
15+
examples still run, and the docs reflect what shipped.
16+
2. **`vX.Y.Z`** publishes to **PyPI** and creates a **GitHub Release**
17+
with auto-generated notes. Fires only after the rc is good.
18+
19+
Tag dispatch is by name:
20+
21+
- Tags containing `-rc` route to TestPyPI.
22+
- Tags with no `-` suffix route to PyPI + GitHub Release.
23+
- Any other suffix (`-beta`, `-alpha`, `-dev`, ...) is a no-op by design;
24+
a typo in the rc suffix cannot accidentally hit PyPI.
25+
26+
The workflow validates that `pyproject.toml`'s `version` field matches
27+
the tag (modulo PEP 440 normalization: `0.7.0-rc1``0.7.0rc1`).
28+
Mismatches fail the test job before any publishing step runs.
29+
30+
## Pre-release checklist
31+
32+
Run through this before tagging the first rc. Everything here is human
33+
judgment; the workflow can't catch a stale doc reference or a missing
34+
changelog entry.
35+
36+
- [ ] **`CHANGELOG.md` is current.** Every commit since the previous
37+
release that changed user-visible behavior is reflected in the
38+
upcoming version's section. The date matches the day the rc tag
39+
is pushed (refresh it again at the real-release step).
40+
- [ ] **Docs sweep for stale references.** For each behavior change in
41+
the upcoming release, grep the docs for the old wording, file
42+
paths, and flag descriptions; reconcile in the same PR as the
43+
version bump. Common spots: `README.md`, `docs/concepts/*`,
44+
`docs/getting-started/index.md`, `docs/reference/index.md`,
45+
in-code help text.
46+
- [ ] **`pyproject.toml` version pinned.** Set `project.version` to the
47+
target version, e.g. `0.7.0`. The PEP 440 form (`0.7.0rc1` or
48+
`0.7.0`) and the tag form (`v0.7.0-rc1` or `v0.7.0`) are
49+
normalized to the same value, so either is accepted in the
50+
`pyproject` field; the tag uses the dash form for readability.
51+
- [ ] **Branch state.** On `main`, clean working tree, latest pulled.
52+
Release tags should point at commits already on `main`.
53+
- [ ] **CI is green on `main`.** The release workflow's `test` job
54+
re-runs the suite, but a red `main` is a sign to investigate
55+
before tagging anything.
56+
57+
Land the version bump + changelog refresh as one commit before tagging.
58+
Convention: `chore(release): vX.Y.Z`.
59+
60+
## Tagging the rc
61+
62+
After the prep commit is on `main`:
63+
64+
```bash
65+
git tag v0.7.0-rc1
66+
git push origin v0.7.0-rc1
67+
```
68+
69+
Watch the workflow run at <https://github.com/LunarCommand/openarmature-python/actions>.
70+
On success, the artifact lands at <https://test.pypi.org/project/openarmature/>.
71+
72+
The `testpypi` GitHub Environment owns the OIDC trust for the upload;
73+
no secrets to plumb manually.
74+
75+
## Verifying the rc
76+
77+
Install from TestPyPI into a fresh venv and exercise the package the
78+
way a downstream user would.
79+
80+
```bash
81+
python -m venv /tmp/oa-rc-verify
82+
source /tmp/oa-rc-verify/bin/activate
83+
84+
# --extra-index-url is required: TestPyPI does not mirror the
85+
# transitive dependency graph, so dependencies pull from real PyPI.
86+
pip install \
87+
--index-url https://test.pypi.org/simple/ \
88+
--extra-index-url https://pypi.org/simple/ \
89+
'openarmature==0.7.0rc1'
90+
91+
python -c "import openarmature; print(openarmature.__version__)"
92+
```
93+
94+
Minimum smoke set:
95+
96+
- [ ] Version string matches the rc tag.
97+
- [ ] At least one example runs to completion against a real LLM
98+
endpoint (`examples/00-hello-world/main.py` is the quickest).
99+
- [ ] The optional `[otel]` extra installs cleanly and
100+
`import openarmature.observability.otel` succeeds.
101+
- [ ] If any docs changed in this release, the live docs site
102+
(<https://openarmature.ai/>) builds without warnings.
103+
104+
If any of these fail, see *Iterating on an rc* below. Do not proceed
105+
to the real-release tag with an unverified rc.
106+
107+
## Tagging the real release
108+
109+
After the rc is verified, the real release is one tag away:
110+
111+
```bash
112+
git tag v0.7.0
113+
git push origin v0.7.0
114+
```
115+
116+
The workflow runs the same test job, builds the artifact, publishes to
117+
PyPI through the `pypi` GitHub Environment, and creates a GitHub
118+
Release with notes auto-generated from commits since the previous tag.
119+
120+
The `pypi` environment is the right place to attach a **required
121+
reviewers** protection rule so the publish step pauses for explicit
122+
manual approval before any real-PyPI upload. Configure it in repo
123+
settings under *Environments → pypi → Required reviewers*.
124+
125+
After the workflow finishes:
126+
127+
- [ ] The new version appears on <https://pypi.org/project/openarmature/>.
128+
- [ ] A GitHub Release exists at
129+
<https://github.com/LunarCommand/openarmature-python/releases>
130+
with the wheel and sdist attached.
131+
- [ ] `pip install openarmature` in a fresh venv resolves the new
132+
version.
133+
134+
## Iterating on an rc
135+
136+
If the rc reveals an issue, **never move the existing rc tag**. PyPI
137+
and TestPyPI treat versions as immutable; bump the rc counter instead.
138+
139+
```bash
140+
# Fix the bug, commit it.
141+
git tag v0.7.0-rc2
142+
git push origin v0.7.0-rc2
143+
```
144+
145+
Repeat verification against the new rc. Two or three rc iterations is
146+
fine. If the same issue keeps recurring, that's a signal to step back
147+
and address the design rather than spin more rc tags.
148+
149+
## Rollback
150+
151+
PyPI does not allow re-uploading the same version. If a real release
152+
ships and turns out to be broken:
153+
154+
1. **Yank the version** via the PyPI web UI
155+
(<https://pypi.org/manage/project/openarmature/release/X.Y.Z/>).
156+
Yanking marks the version as not-installable-by-default; existing
157+
pinned dependencies still resolve, but a fresh install skips it.
158+
2. **Cut a patch.** Fix the bug, run through the full rc → real cycle
159+
for `X.Y.(Z+1)`. The yanked version stays in place as a historical
160+
record; the new patch supersedes it.
161+
162+
Do not try to delete a release. Yanking + patching is the supported
163+
path and what downstream tooling expects.
164+
165+
## Reference
166+
167+
- `.github/workflows/release.yml` — the release workflow; authoritative
168+
on what happens when each tag shape is pushed.
169+
- `CHANGELOG.md` — release notes go here, in
170+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
171+
- `pyproject.toml``project.version` must match the tag (normalized
172+
per PEP 440).
173+
- GitHub Environments — `testpypi` and `pypi` own the OIDC trust to
174+
the respective indexes. Configure required-reviewer rules on `pypi`
175+
for an extra approval gate before real publishes.

0 commit comments

Comments
 (0)