99 lines
4.1 KiB
Markdown
99 lines
4.1 KiB
Markdown
# 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.
|
||
|
||
---
|
||
|