|
| 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