# Releasing

> **Conformance:** L0 / L1 / L2 (see `README.md`)
> **Audience:** Human engineers AND AI coding agents acting as release maintainers.

> **Scope.** Cross-project release-engineering standard for any
> Python project that publishes to PyPI (or a private index). The
> rules are written for the `mgf-common` shape but apply to every
> versioned, distributed package. They turn the "push to PyPI"
> ritual from tribal knowledge into a discipline that survives
> handoff between maintainers, sessions, and agents.
>
> Filed in response to two concrete incidents on 2026-05-02:
> (a) a maintainer-side false-positive unblock claim caused by
> reading a local `dist/` wheel as if it were the PyPI artifact
> (resolved in commit `5bef66f`); (b) FEEDBACK PAPER-12 — a
> consumer reading `PUBLIC_API.md` from a checkout believed
> features were shipped that were only on master HEAD. Both
> failures share one root cause: **the published artifact is the
> contract; nothing else is.**

---

## Rules box (read this first)

| ID | Rule |
|---|---|
| **REL-01** | **Every PyPI release MUST have a corresponding annotated git tag** of the form `v<version>` on the exact commit that built the published wheel. Tags are how the project answers "what was master at when 0.X.Y was cut?" months later. |
| **REL-02** | **A version string MUST NOT be reused across different wheel contents.** Once `0.X.Y` is published, the local `dist/` artifact for that version is read-only. Never rebuild the same version string from a later commit — a rebuilt local wheel that *looks* like 0.X.Y but ships extra commits is the canonical maintainer-side footgun. Bump the version, build, ship. |
| **REL-03** | **`PUBLIC_API.md` and `PUBLIC_API.json` reflect master HEAD, not the published surface.** They MAY be ahead of PyPI during a release window. Consumers verify what they actually have with `pip show mgf-common` + the `__version__` field in `PUBLIC_API.json`. The doc carries a banner stating this. (Closes PAPER-12.) |
| **REL-04** | **PyPI tokens MUST NOT be passed via chat, shell history, or environment variables in unmanaged sessions.** Local: `~/.pypirc` with `chmod 600` and a project-scoped token. CI: PyPI Trusted Publisher (OIDC, no token at all — see [docs.pypi.org/trusted-publishers](https://docs.pypi.org/trusted-publishers/)). |
| **REL-05** | **Releases are one-shot and irreversible.** Once `twine upload` succeeds, the published version is frozen forever — PyPI rejects re-upload of the same version (even after `pypi yank`). Bug fixes ship as a new release, never as a re-upload. |
| **REL-06** | **The release flow is strictly ordered.** Out-of-order is a footgun. The order is: bump version → regenerate `PUBLIC_API.json` → build → run full test suite (green) → twine check → commit → tag → push tag → upload. Do not invert. |
| **REL-07** | **A release commit MUST close every FEEDBACK entry it ships.** Flip status from `OPEN` / `ACCEPTED` / `SCHEDULED` to `✅ SHIPPED` with a one-paragraph retrospective in the entry body. The CHANGELOG is the audit trail; the FEEDBACK entry is the design record. |
| **REL-08** | **Pre-release smoke MUST exercise the actually-built wheel.** Run `pip install dist/<package>-<version>-*.whl` into an ephemeral venv and import every public top-level name. A passing test suite against the source tree does NOT prove the wheel installs and imports cleanly. |
| **REL-09** | **Every FEEDBACK entry flipped to `✅ SHIPPED` (or to `DEFERRED` with a sharpened rationale, or to `❌ REJECTED`) MUST produce a consumer-facing notification message.** A short, copy-pasteable paragraph naming the version it ships in, the consumer's next move, and any caveats. The CHANGELOG's *Migration* section is the source content; the notification is the *delivery mechanism* — without this step, the consumer who filed the entry never learns it was addressed. The maintainer (human or AI agent) drafts the notification at close-loop time and hands it back to whoever filed the original entry. |

---

## 1. Mental model — PyPI is the contract

A published wheel is the *only* artifact every consumer can
observe. Master, `dist/`, `PUBLIC_API.md`, your reference
consumer's vendored copy — none of these are authoritative. They
can all drift from PyPI, and when they drift they lie.

### 1.1 The asymmetry that makes the published artifact authoritative

When a consumer runs `pip install mgf-common==0.X.Y`:

- They get the bytes that `twine upload` sent to PyPI on whatever
  date 0.X.Y was cut.
- They do NOT get whatever is on master right now.
- They do NOT get whatever is in the maintainer's `dist/`
  directory right now.
- They CANNOT get a re-uploaded 0.X.Y with extra commits — PyPI
  enforces immutability.

Every claim of the form "this is in 0.X.Y" must be verifiable
against that specific frozen artifact. The right verification
question is **"is this in the wheel PyPI serves for 0.X.Y?"** —
not "is this in master?", not "is this in PUBLIC_API.md?".

### 1.2 The two anti-patterns this rule kills

**Anti-pattern A: trusting `dist/`.**
> "I unzipped `dist/mgf_common-0.X.Y-py3-none-any.whl` and the
> module is there, so it's published."

False. Local builds can reuse a version string; the local wheel
may contain commits that landed after the PyPI upload. The
maintainer-side false-positive on 2026-05-02 was exactly this
shape — local dist had VersionedFileStore, PyPI 0.12.0 didn't.

**Anti-pattern B: trusting `PUBLIC_API.md`.**
> "I'm reading `PUBLIC_API.md` from the maintainer's master and
> it lists `default_log_path`, so it's available."

False. `PUBLIC_API.md` reflects master HEAD. PAPER-12 documents
this consumer-side trap. The banner at the top of
`PUBLIC_API.md` (shipped post-PAPER-12) tells readers exactly
this.

### 1.3 How to verify "is X in version Y?"

Three increasingly authoritative checks:

```bash
# (1) The lightweight check — is the version even on PyPI?
curl -sSL 'https://pypi.org/pypi/<package>/json' | jq '.releases | keys'

# (2) Per-version metadata + upload time + digest:
curl -sSL 'https://pypi.org/pypi/<package>/<version>/json' | \
  jq '.urls[] | {filename, sha256: .digests.sha256, upload_time}'

# (3) Cross-check the upload time against the commit that ships X:
git log --pretty='%H %cI %s' -- <path/to/X> | head
```

If the upload time of `<version>` is BEFORE the commit that ships
X, then X is NOT in `<version>` — regardless of what's on master.

When this answer matters to a downstream decision, run all three.
A grep against a local `dist/` wheel is not a substitute.

---

## 2. The release flow

The order is a discipline. Each step builds on the previous
step's invariants.

```
1. Bump version             — pyproject.toml::project.version + __version__
                              (CI lints they match — rule AP-13)
2. Regenerate PUBLIC_API.json — uv run python tools/generate_public_api_json.py
3. Build                    — uv build  (wheel + sdist into dist/)
4. Test                     — uv run pytest -x -q   (must be green)
5. twine check              — uv run --with twine twine check dist/<version>*
6. Commit                   — git add … && git commit
7. Tag                      — git tag -a v<version> -m "<description>"
8. Push tag                 — git push --tags
9. Upload to PyPI           — uv run --with twine twine upload dist/<version>*
                              (uses ~/.pypirc — see §4)
10. Verify on PyPI          — curl https://pypi.org/pypi/<package>/<version>/json
                              (digest + upload time appear)
```

A release that lands at step 9 but fails any earlier step is a
broken release in disguise. Run them in order, fix at the step
that broke, restart from there if you have to.

### 2.1 Version-bump checklist

Two locations, kept in sync by AP-13's CI lint:

```toml
# pyproject.toml
[project]
version = "0.X.Y"
```

```python
# src/<package>/__init__.py
__version__ = "0.X.Y"
```

The `tools/generate_public_api_json.py` script reads
`__version__` from the package — running it after the bump
embeds the new version into `PUBLIC_API.json`'s top-level
`__version__` field. If the field shows the OLD version after
regeneration, the bump didn't take. Re-check.

### 2.2 Pre-publish smoke (rule REL-08)

A green test suite proves the source tree works. It does not
prove the *built wheel* installs cleanly into a fresh venv —
packaging metadata bugs (missing `package_data`, broken
`requires-python`, wrong `[project.optional-dependencies]`) only
surface against the wheel itself.

```bash
# Cheap smoke that catches every packaging bug short of
# functional regression:
python -m venv /tmp/smoke && /tmp/smoke/bin/pip install \
  dist/<package>-<version>-py3-none-any.whl
/tmp/smoke/bin/python -c "import <package>; print(<package>.__version__)"
rm -rf /tmp/smoke
```

If this fails, the wheel is broken. Do NOT upload.

---

## 3. Tag discipline (rule REL-01)

Git tags are how the project answers "what was master at when
0.X.Y was cut?" three months later, when memory is gone and the
maintainer has switched contexts twice.

### 3.1 The tag format

Annotated tags only. Lightweight tags don't carry author /
date / message metadata and are easy to lose to garbage
collection. Use:

```bash
git tag -a v0.X.Y -m "v0.X.Y — <one-line summary>

PyPI: https://pypi.org/project/<package>/0.X.Y/
SHA256 (wheel): <prefix>...
Uploaded: <UTC timestamp>

Closes FEEDBACK PAPER-NN."
git push --tags
```

The tag message embeds the PyPI artifact metadata so future
inspection (`git show v0.X.Y`) answers "what published?" without
hitting PyPI.

### 3.2 What the tag enables

| Question | One-liner |
|---|---|
| What's in master that isn't in the latest published version? | `git diff v<latest>..HEAD --stat` |
| What did 0.X.Y change relative to its predecessor? | `git log --oneline v<prev>..v<latest>` |
| Was symbol X published in 0.X.Y or a later release? | `git log --all -- <path/to/X.py>` then check which tag's commit is an ancestor |
| Reproduce the build exactly | `git checkout v0.X.Y && uv build` |

Without tags, every one of these is a manual cross-reference
against PyPI upload times.

### 3.3 Retroactive tagging

If a release shipped without a tag (the case for early
`mgf-common` releases), tag retroactively against the commit
that built the wheel. Identify the commit by:

1. `curl https://pypi.org/pypi/<package>/<version>/json` — note
   the upload time.
2. `git log --pretty='%H %cI'` — find the commit immediately
   *before* the upload time.
3. Verify that commit's tree contains the version bump in
   `pyproject.toml` and `__version__`.
4. `git tag -a v<version> <commit-sha> -m "..."`.

A retroactive tag is a small lie if the published wheel was
built from a commit that drifted further than expected — but in
practice, the bump-build-upload sequence happens within minutes,
so the tag is accurate.

---

## 4. Token storage (rule REL-04)

PyPI authentication is the place where shortcuts compound into
breaches. The token is a credential that authorizes uploads to
your project; if it leaks, anyone with the leaked token can
publish a malicious 0.X.(Y+1) that every consumer's `pip
install -U` will pull.

### 4.1 Local: `~/.pypirc` (the minimum)

```ini
[pypi]
  username = __token__
  password = pypi-AgE...your-project-scoped-token...
```

Then:

```bash
chmod 600 ~/.pypirc
```

`twine upload dist/*` reads `~/.pypirc` automatically. No env
vars, no command-line arguments, no shell history exposure.

**Token scoping.** Issue a project-scoped token (limited to one
project's uploads), not an account-wide token. Generate at
[pypi.org/manage/account/token/](https://pypi.org/manage/account/token/).

### 4.2 CI: Trusted Publishers (the right answer)

[PyPI Trusted Publishers](https://docs.pypi.org/trusted-publishers/)
use OIDC to authenticate the CI job directly — no token stored
anywhere. GitHub Actions:

```yaml
# .github/workflows/release.yml
jobs:
  publish:
    permissions:
      id-token: write   # required for Trusted Publisher OIDC
    steps:
      - uses: pypa/gh-action-pypi-publish@release/v1
```

Configure on PyPI side: project → "Manage" → "Publishing" → add
the GitHub repo + workflow filename. After that, no token is
needed; the OIDC handshake authenticates each push.

This is the strategic-review P2 ("operational maturity")
endpoint — once CI lands, every release goes through Trusted
Publisher and `~/.pypirc` becomes a developer-only concern.

### 4.3 The anti-patterns to avoid

| Anti-pattern | Why it's wrong | What to do instead |
|---|---|---|
| Token pasted in chat / Slack / email | Lands in transcripts, search indexes, model context | Store in `~/.pypirc` and reference by file |
| Token in environment variable on shared shell | History captures it; `ps` may surface it | `~/.pypirc` (chmod 600) |
| Token committed to a `.env` file | Even a "private" repo is one accidental fork from public | `~/.pypirc` outside the repo |
| Account-wide token | Compromise affects every project on the account | Project-scoped token |
| Re-using a token after a leak | The leaked token authenticates forever until revoked | Revoke immediately, issue fresh, never reuse |

If a token leaks (was pasted, sent over an unencrypted channel,
appeared in a model session), revoke at
[pypi.org/manage/account/token/](https://pypi.org/manage/account/token/)
within minutes and issue a fresh one.

---

## 5. Local `dist/` hygiene (rule REL-02)

The `dist/` directory is a build cache. Treat it like one.

### 5.1 The reuse-version footgun

```bash
$ ls dist/
mgf_common-0.12.0-py3-none-any.whl   # ← uploaded to PyPI on 2026-05-01
mgf_common-0.12.0.tar.gz              # ← uploaded to PyPI on 2026-05-01

# Maintainer merges another feature, runs uv build...
$ uv build
$ ls dist/
mgf_common-0.12.0-py3-none-any.whl   # ← OVERWRITTEN with a different artifact
                                     #   that PyPI will never accept
```

The local wheel now claims to be 0.12.0 but its contents differ
from PyPI's 0.12.0. Anyone (including future-you) who treats the
local wheel as authoritative will be wrong.

### 5.2 The discipline

Before each release build, decide which version is being built
and bump first. After publish, treat the published-version
wheels in `dist/` as artifacts of a frozen release — don't
overwrite them.

For local development, build with a `+dev` suffix or to a
separate directory:

```bash
# Local-only dev build, doesn't collide with release artifacts:
uv build --out-dir dist-dev/

# Or use a +dev local version:
sed -i 's/^version = "0.X.Y"$/version = "0.X.Y+dev"/' pyproject.toml
uv build
```

Either way, the rule is: **once a version is on PyPI, the
matching `dist/` artifact is read-only.** If you need to inspect
"what shipped?", `pip download mgf-common==0.X.Y` into a clean
directory pulls the authoritative artifact.

### 5.3 Cleaning between releases

```bash
rm -rf dist/   # before each release build
uv build
```

The `dist/` directory is regenerated by every build. Wiping it
before a release ensures only the artifacts of the current
version are present, eliminating the "which 0.X.Y is this?"
ambiguity.

---

## 6. Recovery

Once `twine upload` succeeds, the version is on PyPI forever.
You cannot:

- Re-upload the same version with different bytes (PyPI rejects).
- Delete the version (PyPI doesn't support delete-and-republish).

You CAN:

- **Yank.** Marks the version as "do not install by default" but
  keeps it available for explicit pins. Use this for critical
  bugs / security issues. `twine yank <package> <version>` (or
  via the PyPI web UI).
- **Ship a new version.** Bump the patch / minor / major
  appropriately, fix the issue, ship `0.X.(Y+1)`. The yanked
  version stays on the index but new installs land on the new
  version.

### 6.1 What "broken release" warrants a yank

| Failure | Yank? |
|---|---|
| Broken wheel — won't install / won't import | Yes (security-class — silent breakage) |
| Wrong contents — published earlier than intended | Yes |
| Critical functional regression | Yes |
| Minor functional bug | Usually no — ship a fix in the next release |
| Test-only or doc-only mistake | No |

### 6.2 Token leak

The recovery sequence:

1. Revoke the leaked token at
   [pypi.org/manage/account/token/](https://pypi.org/manage/account/token/).
2. Issue a fresh project-scoped token.
3. Update `~/.pypirc` with the new token.
4. Audit recent uploads (`pypi.org/manage/project/<package>/`) —
   verify every release is intentional. If you see one you
   didn't make, that's an active compromise; rotate ALL tokens
   on the account, audit account email + 2FA, contact PyPI
   security.

A leaked token in the model context window or transcript counts
as leaked. Revoke. The leaked token is the credential, not the
bytes-on-disk.

---

## 7. Consumer notification on closure (rule REL-09)

The CHANGELOG is the audit trail. The FEEDBACK retrospective is
the design record. The cutover guide (added v0.18.0; closes
PAPER-21) is the consumer-action artifact. Neither of the first
two *delivers* the news to the consumer who filed the entry;
REL-09 closes that gap by producing a notification paragraph
that points at the cutover guide.

### 7.0 Cutover-guide pattern (since v0.18.0)

Each release that closes ≥1 FEEDBACK entry produces an in-tree
markdown file at `docs/cutover/v<X.Y.Z>.md`. The cutover guide
contains one § per closed entry with:

- What landed (one-line summary).
- The consumer's migration steps (concrete commands, code
  diffs where relevant).
- What the rule does NOT require (drop-it-if-N/A clarifications).
- Caveats (things that *didn't* change, follow-ups).

REL-09 paragraphs become **pointers into the cutover guide**
("see v0.X.Y cutover guide §<entry>") rather than re-stating
the migration text. Single source of truth per release;
eliminates per-paragraph drift; composes cleanly with REL-07
("a release commit MUST close every FEEDBACK entry it ships")
— the cutover guide is the artifact REL-07 produces; REL-09
paragraphs are its delivery mechanism.

The first cutover guide is
[`docs/cutover/v0.18.0.md`](../cutover/v0.18.0.md). Its shape
is the canonical pattern; subsequent releases follow it.

Approach A (in-tree markdown per release) is what's shipping.
Approach B (auto-generate from FEEDBACK.md frontmatter at
release time) is the eventual evolution; deferred until 2-3
manually-authored cutover guides surface the right shape.

### 7.1 What the rule asks for

After every status flip on a FEEDBACK entry —
`OPEN`/`ACCEPTED`/`SCHEDULED` → `✅ SHIPPED`, `OPEN` →
`DEFERRED` with sharpened rationale, or `OPEN` → `❌ REJECTED`
— produce a short paragraph in chat (or in whatever channel
relays back to the filer) that is **copy-pasteable into the
filer's next session**. The paragraph names:

  - The PAPER-NN (or VAULT-NN, ASPIRE-NN) identifier.
  - The new status.
  - The version it ships in (if SHIPPED) or the trigger that
    would unblock it (if DEFERRED).
  - The consumer's concrete next move — bump pin, run audit,
    rewrite imports, etc.
  - Any caveats: things that *didn't* change, things still
    deferred, follow-up entries to file if X surfaces.

This is *distinct from* the maintainer-facing recap that every
release commit already produces. Maintainer recap → "what
shipped"; consumer notification → "what you do about it".

### 7.2 Template (cutover-guide era — since v0.18.0)

When a cutover guide exists (any release that closes ≥1
entry), the notification paragraph is short and points into it:

```
PAPER-NN <STATUS> in mgf-common v<X.Y.Z>. See the cutover guide
§<PAPER-NN> for the migration steps:
https://codeberg.org/magogi-admin/mgf_common/src/branch/master/docs/cutover/v<X.Y.Z>.md#<paper-nn>

Pin bump: mgf-common = ">=0.X,<0.(X+1)".

Caveats: <one-line caveat or "none">.
```

For DEFERRED entries (no cutover-guide entry):

```
PAPER-NN DEFERRED. Reopen criteria: <trigger>. <reference to
gating evidence — strategic-review priority, second-consumer
signal, etc.>.
```

### 7.2.1 Template (pre-cutover-guide — for historical reference)

Before v0.18.0, REL-09 paragraphs carried the full migration
content inline. This shape is still valid for releases without
a cutover guide (e.g., a single-entry close where a separate
guide would be overkill):

```
PAPER-NN <STATUS> in mgf-common v<X.Y.Z>. <one-line summary of what changed>.

What to do <once 0.X.Y is on PyPI>:

  1. <concrete step 1>
  2. <concrete step 2>
  3. <concrete step 3>

Notes:
  - <caveat 1>
  - <follow-up to file if N surfaces>
```

For DEFERRED entries, drop steps 1–3 and replace with:

```
What to do: <nothing actionable / continue with X / file evidence
if Y surfaces>. <reference to the gating evidence — a
strategic-review priority, a second-consumer signal, etc.>
```

### 7.3 Worked example — PAPER-08, closed in v0.17.0

```
PAPER-08 SHIPPED in mgf-common v0.17.0. The maintainer-side gap
that let your `_default_log_path` import slip through pre-PAPER-07
is now mechanical: `mgf-common catch-up` grows a sixth audit row
that AST-walks src/ and flags any `from mgf.common._x import …`,
`from mgf.common.X import _y`, or `import mgf.common._x`.

What to do once 0.17.0 is on PyPI:

  1. Bump your pin: `mgf-common>=0.17,<0.18` in pyproject.toml.
  2. Re-run `mgf-common catch-up` in your project root. Expect a
     new "closed-box leaks (private mgf.common imports)" row.
  3. If clean (✅): you're done. If anything surfaces (⚠️): the
     warning names file:line and, when the symbol's known, the
     public replacement. Migrate by hand — `catch-up --apply`
     does NOT auto-rewrite imports.

Notes:
  - tests/ is exempt — `_internal_test_helpers/` style imports
    are legitimate and won't surface.
  - The `_PRIVATE_TO_PUBLIC` mapping in catch-up is currently
    `_default_log_path` and `_crash_dir`. If you find any other
    private imports without a public replacement listed, file a
    PAPER-NN — it means the underlying private symbol may need
    its own PAPER-07-shaped promotion.
```

### 7.4 Why this is a release-engineering rule, not a soft norm

The notification step rots the moment it depends on maintainer
memory. Codified as REL-09:

- It surfaces in the **§8 self-review checklist** so the
  maintainer cannot mark a release "done" with the close-loop
  open.
- AI agents acting as maintainers (the `mgf-common` /
  `mgf-apiprobe` / consumer-side relay shape) read this
  document and produce the notification automatically.
- The next maintainer (or future you) doesn't have to invent
  the practice from scratch.

### 7.5 What the rule does NOT require

- A specific delivery channel. Email, chat, GitHub PR
  comment, Codeberg issue note — whatever the filer used to
  submit. The rule is "produce the paragraph"; routing is
  context-dependent.
- A separate document or audit log. The CHANGELOG carries
  the migration content; FEEDBACK carries the design record;
  the notification is just the message that *unbundles them
  for the consumer*.
- Re-stating everything. The notification is short — 5–10
  lines. Anyone who wants the full picture follows the
  CHANGELOG link.

---

## 8. Self-review checklist

Walk this list before pushing a tag (= triggering the publish
flow):

- [ ] **REL-01** — annotated `v<version>` tag points at the
  exact commit that built the wheel.
- [ ] **REL-02** — version was bumped before the build; no
  reused version string in `dist/`.
- [ ] **REL-03** — `PUBLIC_API.md` banner still present;
  `PUBLIC_API.json::__version__` matches the tag.
- [ ] **REL-04** — token in `~/.pypirc` (chmod 600) or PyPI
  Trusted Publisher OIDC. Not in chat, not in env vars, not
  in the workflow file.
- [ ] **REL-05** — version isn't already on PyPI. (`pip show`
  / PyPI release page.)
- [ ] **REL-06** — flow ran in order: bump → regen JSON →
  build → test → twine check → commit → tag → push → upload.
- [ ] **REL-07** — every FEEDBACK entry shipped in this release
  has its status flipped to `✅ SHIPPED` with a retrospective.
- [ ] **REL-08** — the freshly-built wheel installed into an
  ephemeral venv and imported cleanly with `__version__`
  matching the tag.
- [ ] **REL-09** — for every closed FEEDBACK entry, a
  consumer-facing notification paragraph is drafted and ready
  to relay. CHANGELOG migration content + delivery message.

If any box is unchecked, fix before tagging — not after.

---

## Cross-references

- [`WORKFLOWS.md`](WORKFLOWS.md) — the *how* of release
  discipline. Every REL-* rule in this document is enforced
  mechanically by a corresponding WF-* rule + CI step. When CI
  is wired per WORKFLOWS, a maintainer literally cannot skip a
  REL-* rule by accident — the workflow is the only path.
- [`WORKFLOWS_PATTERNS.md`](WORKFLOWS_PATTERNS.md) — paste-ready
  Woodpecker snippets implementing the publish flow.
- [`API_DESIGN.md`](API_DESIGN.md) — the public-surface contract
  the release ships. AP-13 (`__version__` ↔ pyproject sync) and
  AP-10 (`PUBLIC_API.md` contract list) are the static-side
  invariants this doc operationalises.
- [`../public/PUBLIC_API.md`](../../PUBLIC_API.md) — carries the
  banner from REL-03 and PAPER-12.
- [`../../FEEDBACK.md`](../../FEEDBACK.md) — PAPER-12 is the
  consumer-side incident this standard addresses.
- [PyPI Trusted Publishers](https://docs.pypi.org/trusted-publishers/) — the CI path for REL-04.
- [SemVer 0.x clause](https://semver.org/#spec-item-4) — the
  versioning regime this doc presumes.
