From 21dacdd4d9455587ca843f9f9231f6ccc9255ac9 Mon Sep 17 00:00:00 2001 From: Codechu Date: Sat, 29 Aug 2026 18:49:28 +0300 Subject: [PATCH] Write down how a release is cut, in both layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version scheme was standardised; the act of releasing was not. §5 said what a number means and LIBRARY.md said what artifacts a release produces, but nothing said how one gets out — the order, what cannot be undone, and what to check afterwards. Every trap in the new §5.1 fired on a Codechu release this month: a bump that moved one version record of four; a job that published to the index and then failed for want of a write permission, leaving a state no re-run could repair; and a release guide still instructing a step the pipeline had stopped performing. The Python half — where the version hides, generating the long description rather than maintaining it, trusted publishing, and pipx before pip for tools — is in lang/python/RELEASING.md, linked from the layer index. That index also carried a dead anchor into STANDARDS §13 "Active repositories", which has been §14 for some time. --- STANDARDS.md | 35 ++++++++++++++++++++++ lang/python/README.md | 3 +- lang/python/RELEASING.md | 65 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 lang/python/RELEASING.md diff --git a/STANDARDS.md b/STANDARDS.md index ad1e97c..d01a6db 100644 --- a/STANDARDS.md +++ b/STANDARDS.md @@ -142,6 +142,41 @@ Details: [BRAND-GUIDELINES.md](BRAND-GUIDELINES.md). | Pre-release | `-alpha.N`, `-beta.N`, `-rc.N` | | Changelog | [Keep a Changelog](https://keepachangelog.com/) format | +### 5.1 Cutting a release + +The version scheme above says what a number means. This says how one gets +out, and every line of it was paid for. + +1. **One command moves every version record, and a test asserts they + agree.** Wherever a version is written more than once, moving them by + hand moves some of them. *Incident: a release bumped the packaging + metadata and left three other records behind, so every artefact that + release produced was stamped with a version that did not produce it.* +2. **Write the changelog entry before the tag.** It is the release note; + nothing is written twice. +3. **The tag is the only trigger, and pushing it is the human signature.** + No other event publishes. Nothing else should be able to. +4. **Order the pipeline so everything that can fail runs before the + irreversible step**, and grant the job the permissions its *last* step + needs — not its first. Publication to a package index cannot be undone + or repeated. *Incident: a release job published to the index and then + failed creating the source-forge release for want of a write + permission; the dependent job behind it never ran, and no re-run could + repair the state, because the upload could not be replayed.* +5. **Verify the artefacts, not the colour of the run** — in **both** + directions. A green run can leave a half-finished release, and a red + run can sit above a perfectly good one. Open the package index entry, + the release page, and whatever else the pipeline was supposed to touch. +6. **The procedure names its tooling, and is re-read when the tooling + changes.** A documented step the pipeline no longer performs is worse + than no documentation, because it is followed. *Incident: a release + guide still instructed the operator to create the forge release by + hand, months after the workflow began creating it from the tag.* + +A failed release is not a private matter: if a version reached users in +any state, say so in the changelog entry for that version rather than in +a commit message nobody reads. + ## 6. Repository discipline (every product) | Item | Rule | diff --git a/lang/python/README.md b/lang/python/README.md index b8891fa..e8631ab 100644 --- a/lang/python/README.md +++ b/lang/python/README.md @@ -9,6 +9,7 @@ library extractions. | [LIBRARY.md](LIBRARY.md) | Starting or maintaining a `codechu-*` library published to PyPI | | [APPLICATION.md](APPLICATION.md) | Starting or maintaining a Python end-user app (e.g. `disk-cleaner`) | | [PROPRIETARY.md](PROPRIETARY.md) | Starting or maintaining a **closed-source** Python library or app (private repo, license keys, signed wheels, etc.) | +| [RELEASING.md](RELEASING.md) | Cutting a release: where the version lives, the generated long description, trusted publishing, the order that survives | These extend, never weaken, the org-wide rules in [`STANDARDS.md`](../../STANDARDS.md) and the language-agnostic @@ -22,5 +23,5 @@ project-type rules in ## Active Python libraries -See the roster in [`STANDARDS.md` §13](../../STANDARDS.md#13-active-repositories) +See the roster in [`STANDARDS.md` §14](../../STANDARDS.md#14-active-repositories) and [`README.md`](../../README.md). diff --git a/lang/python/RELEASING.md b/lang/python/RELEASING.md new file mode 100644 index 0000000..6c21b87 --- /dev/null +++ b/lang/python/RELEASING.md @@ -0,0 +1,65 @@ +# Releasing a Python package + +Extends [`STANDARDS.md` §5.1](../../STANDARDS.md#51-cutting-a-release), +which carries the language-agnostic rules. This is what §5.1 means when +the artefact is a wheel on PyPI. + +Read first: [`STANDARDS.md`](../../STANDARDS.md), then your project type. + +## 1. Where the version lives + +A Python project writes its version in more places than it looks: + +| File | Why it matters | +|---|---| +| `pyproject.toml` | what the index sees | +| `/__init__.py` | what the running tool reports, and what any artefact it produces is stamped with | +| `CITATION.cff` | what a citation resolves to | +| `.zenodo.json` | what the DOI archives | + +**One command moves all of them**, refuses to run when they disagree +beforehand, and does not tag, commit or push — the tag stays a deliberate +act. A test asserts the records agree, so a mismatch fails CI rather than +shipping. `__init__.py` is the quiet one: it is what a produced artefact +records, and an artefact stamped with a version that did not produce it +cannot be reproduced. + +## 2. The long description is generated, never maintained + +PyPI renders the description outside the repository, so every relative +link in the README 404s there. If the project ships a rewritten copy, +**generate it** and check it in CI — a copy maintained by remembering to +is a copy that ships one revision behind, and only a reader notices. + +## 3. Publish through a trusted publisher + +No API token in the repository, and no token in a secret either where the +index supports OIDC. The workflow runs on the tag, builds, runs the suite +and the offline self-check *again* inside the release, then publishes. + +Grant the job `contents: write` if any step after publication creates a +release or touches the repository. Publication is irreversible; a job +that can 403 after the upload leaves a state no re-run repairs. + +## 4. The order that survives + +``` +changelog entry # the release note, written first +bump # one command, every record +suite + self-check + any generated-file check +commit, push, let CI pass +tag vX.Y.Z, push the tag # the only trigger +verify: index, release page, anything downstream +``` + +## 5. Installation the README recommends + +A **library** is installed into a project's dependency tree: `pip install`. +A **tool** goes on the PATH and should not join anyone's dependency tree: +recommend `pipx install` first, and say what the import/command name is +when it differs from the distribution name. + +A tool that shells out to another Codechu tool declares it as a +dependency, checks for it before the stage that needs it, and records the +version it found beside the result — see +[`project-type/TOOL.md`](../../project-type/TOOL.md) §5.