Files
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

4.1 KiB
Raw Permalink Blame History

Development and releasing

Part of truenas-truecloud-patch.

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.

pip install pytest ruff
ruff check patch tests tools
pytest tests

CI runs shellcheck, bash -n, ruff, and pytest on Python 3.113.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), 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 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 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 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.