| 1 | # Release mode |
| 2 | |
| 3 | For projects that ship builds to people. Snowbound's working example is |
| 4 | [`tools/release.py`](https://shale.paperclover.net/snowbound/tree/-/tools/release.py) |
| 5 | with [`tools/RELEASE.md`](https://shale.paperclover.net/snowbound/tree/-/tools/RELEASE.md). |
| 6 | It publishes every desktop platform to a folder the app's updater reads. |
| 7 | |
| 8 | ## The release script |
| 9 | |
| 10 | 1. **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. |
| 13 | 2. **Refuse a dirty tree.** Publish exactly the commit main points at. |
| 14 | 3. **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. |
| 18 | 4. **Run the gate on that commit.** |
| 19 | 5. **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. |
| 22 | 6. **Sign every file and the manifest.** |
| 23 | 7. **Write the build folder under a temporary name, then rename it.** A |
| 24 | published folder is immutable and never deleted. |
| 25 | 8. **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 | |
| 30 | The manifest lists every commit since the previous published build. `feat:` |
| 31 | counts as a feature and `fix:` as a fix; anything else is "other". A commit |
| 32 | whose body is a bulleted list contributes one entry per bullet. The updater sums |
| 33 | these across every build the user skipped, and shows "3 features, 5 bug fixes, |
| 34 | and 2 other changes" with the titles. So the commit convention in the main |
| 35 | skill is the release-notes format: one `fix:` commit per fix, and bullets on |
| 36 | batch 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. |