Files
truenas-truecloud-patch/docs/releasing.md
T
flan ea00bd0685
CI / shell (shellcheck + syntax) (push) Successful in 10s
CI / python 3.11 (push) Failing after 19s
CI / python 3.12 (push) Failing after 25s
CI / python 3.13 (push) Failing after 17s
Repoint forge references from git.onetick.ninja to git.arch.fyi
2026-08-03 20:03:16 +00:00

99 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Development and releasing
> Part of [truenas-truecloud-patch](../README.md).
## Development
Parts of this project were written with AI assistance (Claude). All of it is
reviewed and tested before release; the test suite and CI exist in large part to
make that review meaningful. Bugs are mine.
```bash
pip install pytest ruff
ruff check patch tests tools
pytest tests
```
CI runs shellcheck, `bash -n`, ruff, and pytest on Python 3.11–3.13. Two checks
are worth calling out, because nothing else would catch what they catch:
- The tests **`compile()` the `*_BLOCK` strings** in `patch/apply.sh`. Those are
Python source appended into live `middlewared` modules — a syntax error there
breaks the box at boot, and they're string literals, so nothing else type-checks
them.
- CI asserts **every script declares the same version**, and that it matches the
newest CHANGELOG entry. `VERSION=` had silently drifted to three different
values across the scripts before anything checked.
The project is hosted on **Gitea** (`git.arch.fyi/flan/truenas-truecloud-patch`)
and mirrored to GitHub. Both run the same workflows — Gitea reads
`.github/workflows/` too — so a change is checked twice, on two independent runners.
---
## Releasing
**Every release interrupts every user.** An update alert fires on each installed
box (see [Update alerts](../README.md#update-alerts)), so a release that exists only to fix the
last release teaches people to dismiss the alert — and one day that alert will be
carrying a security fix. This project cut twelve releases in a single day once.
Never again, and not by good intentions: by a gate.
### The rule
> A stable `vX.Y.Z` may only be published if a `vX.Y.Z-rcN` tag points at the
> **same commit**.
Release candidates are **invisible to users**: `update.sh` and the update alert both
take the newest plain `vX.Y.Z` tag, so an `-rc` is never offered as an update. All
the debugging therefore happens across `rc1`, `rc2`, `rc3` — at nobody's expense —
instead of across `v0.5.0`, `v0.5.1`, `v0.5.2`, at everybody's.
"The candidate passed, then I pushed one more little fix" is refused **by name**.
That is not hypothetical; it is exactly how v0.5.1 happened.
### Day to day
You don't touch the release machinery. Write your changes under `## Unreleased` in
`CHANGELOG.md` and push to `main`. `main` is a work surface — it is allowed to be
mid-thought. Releasing is a separate, deliberate act.
### Cutting a release
```bash
bash release.sh 0.6.0 --check # what would ship? what is the next rc?
bash release.sh 0.6.0 --rc # promotes `## Unreleased` -> v0.6.0, stamps every
# VERSION=, tags v0.6.0-rc1, pushes. Users see nothing.
# ... install it on a real box. Exercise it. Break it. ...
# Found a bug? Fix it on main, then `bash release.sh 0.6.0 --rc` again -> rc2.
bash release.sh 0.6.0 --promote # publishes v0.6.0. REFUSED unless an rc points here.
```
### The gates, and where they live
The logic is Python so it can be unit-tested; CI is the enforcement boundary
because it is the only actor holding the token that publishes. `release.sh` runs the
**same** code locally so you fail in 200 ms instead of after a push.
| gate | enforces | where |
| --- | --- | --- |
| [`release_notes.py check`](../tools/release_notes.py) | every script's `VERSION=` matches the tag; the CHANGELOG section exists and is non-empty; **nothing is stranded under `## Unreleased`** | `release.sh` + CI |
| [`release_gate.py`](../tools/release_gate.py) | **an rc points at this exact commit** | `release.sh` + CI |
| the full suite | ruff, pytest, shellcheck, `bash -n` — re-run against the *tagged* commit | CI |
A release's body **is** its `CHANGELOG.md` section — there is no second place to
write release notes, and therefore no second place for them to go stale. Releases
are published on both forges.
### Why the alert doesn't nag
A release whose CHANGELOG contains only a `### Docs` section changed no code, and
raises **no alert**. Candidates raise no alert either. So the only thing that ever
interrupts a user is a real, complete change — which is the entire point.
---