Skip to content

Commit fa74c5a

Browse files
docs: add RELEASING.md, bump workflow actions to Node 24 (#13)
* docs: add RELEASING.md, bump workflow actions to Node 24 docs/RELEASING.md captures the release flow as observed during the v0.4.0-rc0 smoke test: prerequisites, the four version-bump locations, the rc → final cycle with copy-pasteable commands, the workflow gate table, common failure modes, and post-release verification. Mirrors the forbin-mcp pattern but tailored to this project's TestPyPI staging track. Bump every pinned action to its latest major to clear the Node.js 20 deprecation warnings GitHub flagged on the v0.4.0-rc0 run: actions/checkout v4 -> v6 astral-sh/setup-uv v4 -> v8 actions/upload-artifact v4 -> v7 actions/download-artifact v4 -> v8 Each major versioned independently per its maintainer's release cadence. Node.js 20 is forced off runners in June 2026 and removed in September 2026; this gets us off well before then. * ci: pin astral-sh/setup-uv to v8.1.0 (full version) The previous bump used @v8 but astral-sh/setup-uv doesn't publish a moving major tag — only full SemVer tags (v8.1.0, v8.0.0, etc.). The test job failed with "Unable to resolve action astral-sh/setup-uv@v8, unable to find version v8". Pinning to v8.1.0 (the latest released tag) fixes resolution. The other three actions (actions/checkout, upload-artifact, download-artifact) all DO publish moving major tags, so @v6/@v7/@v8 remain valid for those. * docs: address review feedback on RELEASING.md - Drop "annotated" qualifier from the opener — every example uses a lightweight tag and tag-triggered workflows fire on either kind. - Remove the stale Future work bullet about bumping action versions; the same PR already does that. * chore: add uv-lock pre-commit hook The RELEASING.md doc claimed pre-commit regenerates uv.lock when pyproject.toml changes, but no hook was actually configured to do that. The behavior worked incidentally because the pyright hook runs `uv run`, which triggered uv to re-sync as a side effect. Adding astral-sh/uv-pre-commit's uv-lock hook makes it explicit so the docs claim is accurate and the lockfile sync no longer depends on pyright happening to run uv first.
1 parent 144ed53 commit fa74c5a

4 files changed

Lines changed: 192 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ jobs:
2121
runs-on: ubuntu-latest
2222
steps:
2323
- name: Checkout
24-
uses: actions/checkout@v4
24+
uses: actions/checkout@v6
2525
with:
2626
# Conformance fixtures live in the openarmature-spec submodule.
2727
submodules: recursive
2828

2929
- name: Install uv
30-
uses: astral-sh/setup-uv@v4
30+
uses: astral-sh/setup-uv@v8.1.0
3131
with:
3232
enable-cache: true
3333

.github/workflows/release.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ jobs:
2323
runs-on: ubuntu-latest
2424
steps:
2525
- name: Checkout
26-
uses: actions/checkout@v4
26+
uses: actions/checkout@v6
2727
with:
2828
# Conformance fixtures live in the openarmature-spec submodule.
2929
submodules: recursive
3030

3131
- name: Install uv
32-
uses: astral-sh/setup-uv@v4
32+
uses: astral-sh/setup-uv@v8.1.0
3333
with:
3434
enable-cache: true
3535

@@ -81,12 +81,12 @@ jobs:
8181
runs-on: ubuntu-latest
8282
steps:
8383
- name: Checkout
84-
uses: actions/checkout@v4
84+
uses: actions/checkout@v6
8585
with:
8686
submodules: recursive
8787

8888
- name: Install uv
89-
uses: astral-sh/setup-uv@v4
89+
uses: astral-sh/setup-uv@v8.1.0
9090
with:
9191
enable-cache: true
9292

@@ -97,7 +97,7 @@ jobs:
9797
run: uv build
9898

9999
- name: Upload dist artifact
100-
uses: actions/upload-artifact@v4
100+
uses: actions/upload-artifact@v7
101101
with:
102102
name: dist
103103
path: dist/
@@ -114,7 +114,7 @@ jobs:
114114
id-token: write
115115
steps:
116116
- name: Download dist artifact
117-
uses: actions/download-artifact@v4
117+
uses: actions/download-artifact@v8
118118
with:
119119
name: dist
120120
path: dist/
@@ -140,7 +140,7 @@ jobs:
140140
id-token: write
141141
steps:
142142
- name: Download dist artifact
143-
uses: actions/download-artifact@v4
143+
uses: actions/download-artifact@v8
144144
with:
145145
name: dist
146146
path: dist/
@@ -159,10 +159,10 @@ jobs:
159159
contents: write
160160
steps:
161161
- name: Checkout
162-
uses: actions/checkout@v4
162+
uses: actions/checkout@v6
163163

164164
- name: Download dist artifact
165-
uses: actions/download-artifact@v4
165+
uses: actions/download-artifact@v8
166166
with:
167167
name: dist
168168
path: dist/

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ repos:
1616
args: [--fix]
1717
- id: ruff-format
1818

19+
- repo: https://github.com/astral-sh/uv-pre-commit
20+
rev: "0.11.10"
21+
hooks:
22+
- id: uv-lock
23+
1924
- repo: local
2025
hooks:
2126
- id: pyright

docs/RELEASING.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Releasing openarmature
2+
3+
The release pipeline is tag-driven. Pushing a tag triggers
4+
`.github/workflows/release.yml`, which builds the package and either uploads
5+
to TestPyPI (pre-release tags) or PyPI plus a GitHub Release (final tags).
6+
7+
## Prerequisites (one-time, already done)
8+
9+
- Trusted Publisher pending entries on test.pypi.org and pypi.org for project
10+
`openarmature`, owner `LunarCommand`, repo `openarmature-python`, workflow
11+
`release.yml`. Environments: `testpypi` and `pypi`.
12+
- GitHub repo Environments: `testpypi` (no protection) and `pypi` (required
13+
reviewer). The `pypi` reviewer protection is what makes the publish-pypi job
14+
pause for a manual approval click on the Actions tab before any real-PyPI
15+
upload — keep it on.
16+
17+
## Where the version lives
18+
19+
Four places stay in sync. A version bump touches all of them; pre-commit
20+
regenerates `uv.lock` automatically when you stage `pyproject.toml`.
21+
22+
| File | Field | Notes |
23+
|---|---|---|
24+
| `pyproject.toml` | `[project] version` | Source of truth for the build |
25+
| `src/openarmature/__init__.py` | `__version__` | Read at import time |
26+
| `tests/test_smoke.py` | `assert __version__ == ...` | Drift guard |
27+
| `uv.lock` | `[[package]] openarmature version` | Regenerated by pre-commit |
28+
29+
`__spec_version__` and `tool.openarmature.spec_version` are separate pins for
30+
the spec submodule; per the phased-rollout strategy, they're bumped only at
31+
release prep, not per-phase.
32+
33+
## Release flow
34+
35+
### 1. Bump to an RC version
36+
37+
On main, in a small "release: prep" PR:
38+
39+
```bash
40+
# Pick the version you intend to ship; suffix `rcN` for the first RC.
41+
vim pyproject.toml # version = "0.5.0rc1"
42+
vim src/openarmature/__init__.py # __version__ = "0.5.0rc1"
43+
vim tests/test_smoke.py # match
44+
# uv.lock auto-updates via pre-commit when you stage pyproject.toml
45+
git add pyproject.toml src/openarmature/__init__.py tests/test_smoke.py uv.lock
46+
git commit -m "release: prep 0.5.0-rc1"
47+
git push origin <branch>
48+
gh pr create --title "release: prep 0.5.0-rc1" ...
49+
```
50+
51+
If this is the same release cycle as a previous RC, also bump the spec pins
52+
on the same PR if Phase 6's spec target requires it (`spec_version` in
53+
`pyproject.toml`, `__spec_version__` in `__init__.py`).
54+
55+
Merge the PR.
56+
57+
### 2. Tag the RC
58+
59+
```bash
60+
git checkout main && git pull --ff-only
61+
git tag v0.5.0-rc1
62+
git push origin v0.5.0-rc1
63+
```
64+
65+
The Release workflow fires. Job order: `test → build → publish-testpypi`.
66+
`publish-pypi` and `github-release` skip because the tag contains `-`.
67+
68+
Watch the run: `gh run watch $(gh run list --workflow=release.yml --limit=1 --json databaseId --jq '.[0].databaseId') --exit-status`
69+
70+
### 3. Verify the RC install
71+
72+
In a fresh venv:
73+
74+
```bash
75+
python3 -m venv /tmp/oa-verify
76+
/tmp/oa-verify/bin/pip install \
77+
--index-url https://test.pypi.org/simple/ \
78+
--extra-index-url https://pypi.org/simple/ \
79+
openarmature==0.5.0rc1
80+
/tmp/oa-verify/bin/python -c "
81+
import openarmature
82+
from openarmature.graph import GraphBuilder, State, SubscribedObserver
83+
print(f'version: {openarmature.__version__}')
84+
print(f'spec_version: {openarmature.__spec_version__}')
85+
print('imports ok')
86+
"
87+
rm -rf /tmp/oa-verify
88+
```
89+
90+
`--extra-index-url` is required: TestPyPI doesn't host all transitive deps
91+
(pydantic, etc.), so pip needs to fall back to real PyPI for them.
92+
93+
If anything's wrong (broken install, wrong reported version, missing deps,
94+
README rendering issue on TestPyPI), fix on main and bump to `0.5.0rc2`. RC
95+
slots are one-shot — TestPyPI refuses re-uploads of the same version.
96+
97+
### 4. Tag the real release
98+
99+
When the RC verifies clean, drop the suffix on a new "release:" PR:
100+
101+
```bash
102+
vim pyproject.toml # version = "0.5.0"
103+
vim src/openarmature/__init__.py # __version__ = "0.5.0"
104+
vim tests/test_smoke.py # match
105+
git add ...
106+
git commit -m "release: 0.5.0"
107+
gh pr create --title "release: 0.5.0" ...
108+
```
109+
110+
Merge it. Then tag from main:
111+
112+
```bash
113+
git checkout main && git pull --ff-only
114+
git tag v0.5.0
115+
git push origin v0.5.0
116+
```
117+
118+
Job order: `test → build → publish-pypi → github-release`. The
119+
`publish-pypi` job pauses at the `pypi` environment with a "waiting for
120+
approval" notice. Click approve on the Actions tab; PyPI receives the
121+
upload; then `github-release` creates the public GitHub Release with
122+
auto-generated notes.
123+
124+
## What the workflow gates on
125+
126+
| Tag pattern | publish-testpypi | publish-pypi | github-release |
127+
|---|:---:|:---:|:---:|
128+
| `v0.5.0` | skip | run | run |
129+
| `v0.5.0-rc1`, `v0.5.0-rc2`, ... | run | skip | skip |
130+
| `v0.5.0-beta1`, `v0.5.0-alpha1`, ... | skip | skip | skip |
131+
132+
The `-rc` suffix is the only pre-release form that actually publishes
133+
(to TestPyPI). Any other suffix is a no-op — failsafe against accidental
134+
uploads from misnamed tags. Real PyPI requires a clean SemVer tag with
135+
no suffix at all.
136+
137+
The workflow also runs a "Verify pyproject.toml version matches tag"
138+
step before the build. PEP 440 normalization is applied via
139+
`packaging.version.Version`, so `0.5.0-rc1``0.5.0rc1`.
140+
141+
## Common failure modes
142+
143+
**OIDC trust error on first publish.** The pending publisher on TestPyPI
144+
or PyPI hasn't been set up correctly. Re-check the four fields (project,
145+
owner, repo, workflow filename, environment) on the index's "Add a new
146+
pending publisher" form.
147+
148+
**"version mismatch" failure in the test job.** `pyproject.toml` doesn't
149+
match the pushed tag after PEP 440 normalization. Fix `pyproject.toml`,
150+
make a new commit, and tag again with a fresh RC number — TestPyPI/PyPI
151+
won't accept a re-upload at the existing version.
152+
153+
**publish-pypi never runs.** Check the tag name. Any `-` in the tag (rc,
154+
beta, alpha, dev) skips real-PyPI by design. Real release requires a
155+
clean `vX.Y.Z` tag with no suffix.
156+
157+
**Approval gate stuck on `pypi` environment.** That's working as
158+
intended. Click "Review deployments" on the Actions run, approve the
159+
`pypi` environment.
160+
161+
## Verifying after release
162+
163+
- **PyPI**: https://pypi.org/project/openarmature/
164+
- **GitHub Release**: Releases tab on the repo; auto-generated notes
165+
list every commit since the previous tag.
166+
- Smoke install in a fresh venv:
167+
```bash
168+
pip install openarmature==<version>
169+
python -c "import openarmature; print(openarmature.__version__)"
170+
```
171+
172+
## Future work
173+
174+
- A first-class `drain` deadline parameter (spec proposal in flight) will
175+
change the engine API; release notes for that version should call it
176+
out as a behavioral change.

0 commit comments

Comments
 (0)