1# Release mode
2
3For projects that ship builds to people. Snowbound's working example is
4[`tools/release.py`](https://shale.paperclover.net/snowbound/tree/-/tools/release.py)
5with [`tools/RELEASE.md`](https://shale.paperclover.net/snowbound/tree/-/tools/RELEASE.md).
6It publishes every desktop platform to a folder the app's updater reads.
7
8## The release script
9
101. **Run from a dedicated release checkout** that is moved to main before each
11 run. Agents' edits then can't abort it. Snowbound's first releases ran from
12 the shared tree and aborted twice.
132. **Refuse a dirty tree.** Publish exactly the commit main points at.
143. **Derive the version from that commit.** Snowbound uses the commit's date
15 in the owner's timezone plus how many commits landed that day:
16 `2026-09-29-r4`. The version is deterministic and needs no tags. A folder
17 that already exists for that commit makes the run a no-op.
184. **Run the gate on that commit.**
195. **Build each platform with the version compiled in.** Builds without a
20 version are dev builds and never update themselves. Split out debug
21 symbols, and ship them beside each archive.
226. **Sign every file and the manifest.**
237. **Write the build folder under a temporary name, then rename it.** A
24 published folder is immutable and never deleted.
258. **Update the history file, then the latest pointer, each by rename.** The
26 latest pointer only ever moves a platform forward.
27
28## Release notes from commits
29
30The manifest lists every commit since the previous published build. `feat:`
31counts as a feature and `fix:` as a fix; anything else is "other". A commit
32whose body is a bulleted list contributes one entry per bullet. The updater sums
33these across every build the user skipped, and shows "3 features, 5 bug fixes,
34and 2 other changes" with the titles. So the commit convention in the main
35skill is the release-notes format: one `fix:` commit per fix, and bullets on
36batch commits.
37
38## Around a release
39
40- **Hold pushes to main while a release runs.** Snowbound's release stopped
41 itself when main moved under it.
42- **Run releases detached, as a visible task.** A two-hour limit on background
43 commands once killed a release that had been queued behind other builds.
44- **Before the first publish, test the updater end to end**: install the old
45 build, publish the new one, update. Snowbound's first release shipped an
46 updater that rejected every download.
47- **Once the updater exists, the owner's installed build changes only through
48 it** (see `ux-testing`, on the owner's machine).
49- **After publishing:**
50 - close the issues whose fixes are in this release, naming the version;
51 - say which release first contains each fix ("am i actually updated?").
52- **Add release tooling as the project grows.** Snowbound later added signing
53 keys published with the README, debug-symbol archives, a crash reporter, and
54 a separate web deploy. Each was added when someone needed it, not up front.