Skip to content

Commit e910254

Browse files
docs: add RELEASING.md (#56)
* 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. * docs: reframe rc-first as convention, not invariant The release workflow dispatches by tag name alone; a maintainer can push vX.Y.Z directly and the workflow will publish to PyPI without consulting any prior -rc tag. The intro paragraph read as if the rc-first flow were automation-enforced, which it isn't. Reframed the intro to make clear that rc-first is a maintainer-side convention carried by the pre-release checklist, not a property of the workflow.
1 parent 472126d commit e910254

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

RELEASING.md

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

0 commit comments

Comments
 (0)