tsudev # Releasing tsudev.com
--- Publishing a release here is not only publishing a download. Every installed copy checks GitHub Releases on startup and **blocks until it has installed whatever is newest**. Publishing pushes a mandatory update to everyone. That is deliberate - see [`SAFETY.md`](SAFETY.md) for why a stale safety database is worth interrupting people over - but it makes the publish step the one that deserves care. --- ## Version naming Releases are named for the day they ship: | Situation | Release name | |---|---| | First release on 19 August 2026 | `tsudev-cwico-v26.8.19` | | A second release the same day | `tsudev-cwico-v26.8.19.2` | | A third | `tsudev-cwico-v26.8.19.3` | | The next day | `tsudev-cwico-v26.8.20` | Internally each maps to a semver whose patch field carries the day and the release counter (`26.8.1901`), because Cargo, the MSI bundler and the updater all require three components - and the updater *compares* that number to decide whether a user is out of date. Never write a version by hand. `tools/version.py` owns the mapping: ```bash tools/version.py current # what the project claims right now tools/version.py next # the next release name for today tools/version.py check # do all three manifests agree? ``` --- ## Cutting a release ### 1. Confirm the tree is green ```bash cargo fmt --all -- --check cargo clippy --all-targets -- -D warnings cargo test npm --prefix ui run build python3 tools/check_docs.py --with-tests python3 tools/test_version.py ``` ### 2. Set the version ```bash tools/version.py set "$(tools/version.py next)" tools/version.py check ``` This rewrites `Cargo.toml`, `app/src-tauri/tauri.conf.json` and `ui/package.json`. Nothing else should carry a version. ### 3. Write the changelog entry Add a section to `CHANGELOG.md` under the new release name. Its body becomes the release notes users read **inside the mandatory update screen**, so write it for them rather than for other developers: what changed about what the tool will and will not remove, and anything they should know before it installs. The build fails if the section is missing. A blocking screen with no explanation is worse than no blocking screen - check what it will look like: ```bash tools/changelog_section.py "$(tools/version.py current | awk '{print $1}')" ``` ### 4. Commit and tag ```bash git add -A git commit -m "Release $(tools/version.py current | awk '{print $1}')" git tag "$(tools/version.py current | awk '{print $1}')" git push origin main --tags ``` The workflow refuses to build a tag whose version disagrees with the manifests, so a forgotten step 2 fails in CI rather than shipping a build that reports the previous version - which, since the updater compares exactly that number, would tell every user they were already current. ### 5. Check the draft The workflow drafts a release; it does not publish one. The updater endpoint resolves `releases/latest`, and **GitHub does not count drafts as latest**, so nothing reaches users yet. Before publishing, confirm on the draft: - [ ] `latest.json` is attached - without it no update is ever delivered - [ ] The MSI and NSIS installers are attached and their sizes look sane - [ ] The release notes read the way you want them to, in a blocking dialog - [ ] **The signature verifies against the key this build carries:** ```bash gh run download -n installers -D /tmp/release python3 tools/verify_update_signature.py /tmp/release ``` This is the check worth not skipping. A payload signed by the wrong key - a rotation applied to the repository secrets but not to `tauri.conf.json` - produces a release that every installed copy shows as a mandatory update and then refuses to install. Users are stuck behind a wall with a broken button, and the only way out is downloading a fresh installer by hand. - [ ] Ideally: install the MSI on a Windows machine and run it once ### 6. Publish Publishing is the moment every installed copy starts blocking on this version. There is no staged rollout - press it when you are confident. ### 7. Update winget The build attaches `winget-installer-manifest.yaml` with the real SHA256 and ProductCode. Copy it over `packaging/winget/manifests/t/tsudev/cwico//tsudev.cwico.installer.yaml`, update the other two manifests in that directory, and open a pull request against [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs). --- ## Checking packaging without releasing ```bash gh workflow run release.yml ``` A manual run builds the installers and uploads them as artefacts without drafting a release or touching any user. Use it after changing anything about packaging, the bundler configuration or the build hooks - `cargo build` does not exercise those, so CI's Windows job will not catch a break there. --- ## If a release turns out to be broken Because updates are mandatory, a broken release reaches everyone who opens the app. In order of preference: 1. **Fix forward.** Cut the next release the same day - `tsudev-cwico-v26.8.19.2` - and publish it. Users who have not yet updated go straight to the fixed build; users who did get the fix on next start. This is almost always the right answer, and it is what the same-day counter exists for. 2. **Unpublish**, if the release is minutes old and the damage is contained. Reverting it to a draft removes it from `releases/latest`, so copies that have not yet checked stop being told to update. Copies that already installed it are not helped. 3. **Never delete a published release's assets** while leaving the release itself up. Installed copies then get a `latest.json` pointing at a file that no longer exists, and the mandatory gate becomes a wall with a broken button behind it. The blast radius of a bad release is the reason the workflow drafts rather than publishes, and the reason step 5 asks you to install it once by hand.