9.1 KiB
Cutting a release
The release.yml workflow builds and publishes on every pushed plain
vX.Y.Z tag. Pushing a tag with a suffix (vX.Y.Z-rc1) publishes
nothing: the updaters ignore suffixes, so an rc would reach every user
as a stable release. The workflow asserts that the pushed tag matches
Cargo.toml version and fails fast on mismatch, so the order
below matters: bump Cargo before you tag.
Version source
The version lives in Cargo.toml only. tauri.conf.json has no
version field, so the app, the Windows file properties (written by
build.rs) and the bundles all inherit the Cargo version. Never add a
version to tauri.conf.json.
Signing key and secrets
Every release is signed with one minisign key pair, the updater key. Installed copies verify each update against the public key they were built with, so losing the private key means no installed copy can ever update again.
- The public key is
plugins.updater.pubkeyintauri.conf.json. Both release workflows refuse to build while it is empty. - The private key and its password are the repository secrets
TAURI_SIGNING_PRIVATE_KEYandTAURI_SIGNING_PRIVATE_KEY_PASSWORD. Only the workflow steps that check or sign see them; dependency installs and compilation run without them. A signing step fails hard when the key is empty, so no unsigned build is published. - Keep an offline backup of the private key file and its password (for example an encrypted copy on removable media and the password in a password manager). GitHub secrets cannot be read back, so the backup is the only copy that survives a lost or rotated secret.
- Generate the pair once with
npx tauri signer generate. Rotating it strands every installed copy on the old key, so do it only if the private key leaks. build.rsrefuses to build whenTAURI_SIGNING_PRIVATE_KEYis set and the effectivepubkeyis empty, so a local build with the key in the environment cannot produce a release that never verifies updates.
Steps for a new version
-
Bump
Cargo.tomlversion(X.Y.Z). -
Run
cargo buildlocally to refreshCargo.lock. -
Commit:
chore: bump version to X.Y.Z. -
Tag and push:
git tag -a vX.Y.Z -m "vX.Y.Z" git push origin main git push origin vX.Y.Z -
Watch the "Release" workflow run. It creates a non-draft, non-prerelease GitHub Release marked latest, with auto-generated notes and the assets listed below, then fails if
/releases/latestdoes not return the new tag.
Release assets
A release carries exactly these files and nothing else:
| Asset | Built by | Read by |
|---|---|---|
claude-code-usage-bubble.exe |
Windows job, npm run tauri build -- --no-bundle |
Legacy Win32 clients (GitHub API) and Tauri Windows clients (latest.json) |
claude-code-usage-bubble.exe.sig |
tauri signer sign on the exe |
Tauri Windows clients |
claude-code-usage-bubble_X.Y.Z_amd64.AppImage |
Linux job, tauri bundle |
Tauri AppImage clients |
claude-code-usage-bubble_X.Y.Z_amd64.AppImage.sig |
the bundler, with the signing key | Tauri AppImage clients |
claude-code-usage-bubble_X.Y.Z_amd64.deb |
Linux job, tauri bundle |
Users installing by hand (.deb installs check for updates but do not apply them) |
latest.json |
scripts/make-latest-json.js in the publish job |
Tauri clients, through the updater endpoint in tauri.conf.json |
latest.json holds version, notes, pub_date and exactly two
platforms, windows-x86_64 and linux-x86_64, each with the asset's
release-download URL and the content of its .sig. There is no
Linux ARM64 or macOS entry.
Rename rule
The bundler names the Linux outputs after productName
("Claude Code Usage Bubble"), and GitHub rewrites spaces in asset
names, which would break the latest.json URLs. The Linux job
therefore copies the bundles to space-free kebab names before upload:
claude-code-usage-bubble_<version>_amd64.AppImage, its .sig, and
claude-code-usage-bubble_<version>_amd64.deb. The signature covers
the file content, not its name, so renaming keeps it valid. The
bundler also signs the deb, but that signature is not published,
because no updater entry uses it.
Legacy-client asset contract
Clients from v0.5.0 and earlier read /releases/latest through the
GitHub API, which skips drafts and prereleases. Checked against their
updater code at v0.4.0, v0.4.1 and v0.5.0, they need:
- A non-draft, non-prerelease release marked latest in this repo.
- A
tag_nameof plainvX.Y.Z, higher than the running version (suffixes are ignored when comparing). The first Tauri release must therefore be above v0.5.0. - An asset named
claude-code-usage-bubble.exe(matched case-insensitively), else the first asset ending in.exe. The release must carry exactly one.exeasset, under that name. The.exe.sig, AppImage, deb andlatest.jsondo not end in.exe. - The exe uploaded unmodified, so the
digest(sha256:<hex>) that GitHub computes matches the downloaded body they hash.tauri signer signwrites a separate.sigand leaves the exe bytes untouched. - A new exe that starts from any of their hand-over modes: v0.4.0 runs
it with
--updated-to Xonly, v0.4.1 and v0.5.0 with--wait-pid <pid> --updated-to X, and v0.1.9 and earlier with no arguments. The Tauri exe accepts all three, and its startup cleanup deletes the<exe>.old.<pid>,updates/update.exeandupdates/updater-helper-*.exefiles they leave behind.
scripts/make-latest-json.js enforces the asset rules before
gh release create runs: it fails on a missing platform entry or
signature, on any .exe other than claude-code-usage-bubble.exe
(in any case), on any asset name with a space, and on any file outside
the exact set above. Tauri clients read only latest.json, whose URLs
must equal the asset names exactly.
Testing without a real tag (dry run)
A dry run creates a draft release, which does not show up on the public Releases feed and does not reach any client, legacy or Tauri.
- Pick a throwaway tag with a suffix, like
v0.0.0-test. The suffix keeps the tag push from starting a publishing run; never use a plainvX.Y.Ztag for a dry run, because pushing it publishes a release marked latest. - On a scratch branch, set
Cargo.tomlversionto0.0.0-test, runcargo buildlocally to refreshCargo.lock, and commit both files. The release jobs build with--locked, so a staleCargo.lockfails them before any draft exists. The tag check also runs for drafts, so the tag must point at a commit whoseCargo.tomlversion matches it. - Tag that commit and push the tag, then start the "Release" workflow
from the Actions tab (
workflow_dispatch) with that tag as input. - Check the draft: it must hold exactly the six assets in the table
above (with
0.0.0-testas the version), andlatest.jsonmust list onlywindows-x86_64andlinux-x86_64, with URLs equal to the asset names. - Delete the draft release, the tag and the scratch branch.
Rollback to the Win32 build
If a Tauri release is broken in the field, publish the Win32 sources
again under a higher version with the "Release legacy" workflow
(release-legacy.yml):
- Pick a version
X.Y.Zhigher than every release in the field, so every client sees it as an update. - Start "Release legacy" from the Actions tab with
refset to a ref holding the Win32 sources (for examplev0.5.0),versionset toX.Y.Z,tagset tovX.Y.Z, anddraftleft attrue. - The workflow patches the
Cargo.tomlversion at that ref, builds withcargo build --release --locked, signs the exe with the same updater key and writes a Windows-onlylatest.json. Check the draft: it holdsclaude-code-usage-bubble.exe, its.sigandlatest.json. - Publish it: either edit the draft into a release marked latest, or
delete the draft and re-run the workflow with
draftset tofalse, which publishes it as latest.
Legacy clients take the exe through the GitHub API, Tauri Windows
clients through the signed latest.json, and Tauri Linux clients find
no entry for their platform, which they report as up to date. The
Win32 app accepts the --wait-pid <pid> --updated-to X relaunch the
Tauri updater uses.
Versioning
Semver-ish: bump patch for fixes, minor for features, major for
breaking changes (e.g. settings.json schema change). The in-app
updater compares Version { major, minor, patch } lexicographically,
so a higher tuple wins.
Residual risk
The hand-over from a legacy Win32 release to the Tauri exe has been
checked only by reading the updater code at v0.4.0, v0.4.1 and v0.5.0;
it has never run end to end before a release. Static reading cannot
prove runtime behaviour such as antivirus or SmartScreen reactions to
a self-replaced, larger, unsigned exe, WebView2 start-up from a
detached relaunch, or the v0.4.0 helper timing. The first Tauri
release is the first real run. Mitigations: the WebView2 guard, the
--diagnose log, update-error.log, and the release-legacy.yml
rollback, which rebuilds the Win32 sources at a given ref under a
higher version as a Windows-only release. That rollback is itself
unproven end to end.