This runbook is for maintainers cutting a new public release. D2d ships desktop installers through GitHub Actions and ships an in-app auto-updater, so a release has two halves: build/publish on CI, then auto-deliver to installed desktop users.
If you are reading this for the first time, walk through One-time setup before your first release. After that, Cutting a release is the loop you repeat.
Desktop only. The release workflow and the auto-updater cover Windows, macOS, and Linux. Android and iOS builds are not part of this flow — the Google Play Store and Apple App Store deliver mobile updates themselves when you upload a new versioned build there. See Mobile releases.
- You write changelog entries under
## [Unreleased]while developing. - You run
npm run release -- X.Y.Z. The script bumps every version field, moves the Unreleased entries under## [X.Y.Z] - YYYY-MM-DD, and prints what to commit and tag. - You commit and push the
vX.Y.Ztag. GitHub Actions runs.github/workflows/release.yml. - The workflow verifies the tag matches every version field, builds on
Windows / Linux / macOS, signs each installer with the updater key, and
uploads everything to a draft GitHub release along with
latest.json. - You review the draft release and click Publish. As soon as it is published, every installed desktop app (running an updater-enabled build) sees the new version on its next startup and prompts the user to install.
You only need to do this once per repository. Subsequent releases reuse the keypair and secrets.
The updater verifies download artifacts with a minisign signature. Generate the keypair locally:
npx tauri signer generate -w ~/.tauri/d2d-updater.keyPick a strong password when prompted. The command prints a public key and writes the private key to the file you specified.
Back up the private key file and password somewhere safe. Losing them means every future release will be signed with a new key, which breaks auto-update for every existing install — those users will have to re-install manually.
Replace the PUT_YOUR_TAURI_UPDATER_PUBLIC_KEY_HERE placeholder in
src-tauri/tauri.conf.json (the
plugins.updater.pubkey field) with the public key the command printed.
Commit and push the change on main before cutting your first release.
bundle.createUpdaterArtifactsmust staytrue. Tauri 2 defaults this flag tofalse, which makes the bundler skip producing.sigfiles andlatest.jsonregardless of whether the signing key is set. It is already set totrueinsrc-tauri/tauri.conf.json— do not remove it, or the workflow will succeed but the auto-updater will have no manifest to fetch.
The release workflow reads two secrets:
| Secret name | Value |
|---|---|
TAURI_SIGNING_PRIVATE_KEY |
Contents of the private key file from step 1. |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
The password you chose for the key. |
Using gh:
gh secret set TAURI_SIGNING_PRIVATE_KEY < ~/.tauri/d2d-updater.key
gh secret set TAURI_SIGNING_PRIVATE_KEY_PASSWORD
# (paste the password when prompted)Or add them manually under Settings → Secrets and variables → Actions.
GITHUB_TOKEN is provided automatically by Actions; you do not need to add
it.
- You are on
mainwith a clean working tree. -
## [Unreleased]inCHANGELOG.mdhas the entries you want to ship. The bump script refuses to run if this section is empty. - CI is green on
main.
git checkout main
git pull
npm run release -- 0.2.0The script:
- Validates
0.2.0is valid semver and strictly greater than the current version. - Refuses to run if the working tree is dirty, or if a
v0.2.0tag already exists locally or onorigin. - Updates
package.json,src-tauri/tauri.conf.json,src-tauri/Cargo.toml, and theinowio-d2dentry insrc-tauri/Cargo.lock. - Renames
## [Unreleased]inCHANGELOG.mdto## [0.2.0] - YYYY-MM-DDand inserts a fresh empty## [Unreleased]block above it. - Does not commit or tag — you do that explicitly so the commit message stays under your control.
git add -A
git commit -m "chore(release): v0.2.0"
git tag v0.2.0
git push && git push origin v0.2.0The git push origin v0.2.0 is what fires the release workflow.
If
mainis a protected branch (direct pushes rejected): push the bump commit on arelease/v0.2.0branch, open a PR intomain, and merge it. Then tag the resulting commit onmainand push the tag:git checkout main && git pull git tag v0.2.0 git push origin v0.2.0Tag the canonical commit on
main, not the branch commit — a squash-merge changes the hash, and a tag on the branch would dangle.
Open the Actions tab on GitHub and find the Release run for your tag.
verify-versionruns first. It fails fast if any version field disagrees with the tag (which should never happen if you used the bump script).- Three platform jobs run in parallel (
windows-latest,ubuntu-22.04,macos-latest). The macOS job builds a universal binary. The first run on each platform takes ~15 minutes; later runs are cached bySwatinem/rust-cache.
When all three platforms succeed, a draft release appears on the
releases page with installers,
.sig files, and latest.json attached.
- Open the draft release.
- Skim the artifact list. For a healthy updater-enabled release you should
see ~16 assets:
- 6 installers —
.msi,*-setup.exe,.dmg,.AppImage,.deb,.rpm - 5
.sigfiles — one per installer except the.dmg *_universal.app.tar.gzand its.sig— the macOS update artifactlatest.json— the updater manifest- 2 source-code archives (auto-added by GitHub)
- 6 installers —
- Critical sanity check: if
latest.jsonor any.sigfiles are missing, do not publish. See Updater artifacts didn't generate. - Edit the release notes if you want richer formatting than the changelog.
- Click Publish release.
Within seconds the GitHub Releases "latest" URL points to your new release. Installed desktop apps see it on next startup.
Symptom: the draft release has installers but no latest.json and no .sig
files. The build log shows Signature not found for the updater JSON. Skipping upload....
Root cause is almost always one of these, in order of frequency:
bundle.createUpdaterArtifactsisfalseor missing insrc-tauri/tauri.conf.json.TAURI_SIGNING_PRIVATE_KEY_PASSWORDdoesn't match the password used when the keypair was generated. Signing fails silently andtauri-actioncarries on without the.sigfiles.- Either signing secret is missing or malformed (line endings mangled by a copy-paste, etc.).
Quick local check for #2 — try signing a throwaway file with the key:
echo test > t.txt
npx tauri signer sign -f ~/.tauri/d2d-updater.key -p "<YOUR-PASSWORD>" t.txtIf that produces t.txt.sig, your password matches the key.
After fixing the cause: delete the draft release, delete the tag locally and
on origin, re-tag main, and push the tag again.
git tag -d v0.2.0
git push origin --delete v0.2.0
git checkout main && git pull
git tag v0.2.0
git push origin v0.2.0The draft release contains partial artifacts. Either:
- Re-run from the Actions UI — open the failed job and click "Re-run failed jobs". The cache from successful platforms makes this fast.
- Re-dispatch the workflow via
Actions → Release → Run workflowand pass the existing tag (e.g.v0.2.0).
Delete the draft release, fix the problem in code, bump to the next patch
version (0.2.1), and re-release. Do not re-use the same tag.
Tags containing a hyphen ship as GitHub prereleases automatically:
npm run release -- 0.2.0-rc.1
# ... commit ...
git tag v0.2.0-rc.1
git push origin v0.2.0-rc.1GitHub's releases/latest redirect only points to non-prerelease releases,
so the auto-updater on production installs does not pick up RC builds.
RC testers download the prerelease installers manually from the releases
page.
Android and iOS are released outside this workflow:
- The auto-updater is compiled out of mobile builds (see the target-gated
dependencies in
src-tauri/Cargo.toml), and the in-app "Check for updates" button is hidden on mobile. - Build the mobile artifacts locally with
npm run tauri android build(and the iOS equivalent once iOS is set up), then upload the versioned build to the Google Play Console / App Store Connect. - The stores handle delivering the update to users. Keep the version in sync
by running
npm run release -- X.Y.Zfirst, so all platforms share one version and changelog.
Only rotate the updater key if you believe it is compromised. Rotating breaks auto-update for every existing install — those users have to re-install manually.
To rotate:
- Generate a new keypair (same command as step 1 of one-time setup).
- Update the
plugins.updater.pubkeyfield insrc-tauri/tauri.conf.jsononmain. - Replace both GitHub secrets (
TAURI_SIGNING_PRIVATE_KEYandTAURI_SIGNING_PRIVATE_KEY_PASSWORD). - In the release notes, instruct existing users to download and re-install manually.
| OS | Installer | Auto-update target? |
|---|---|---|
| Windows | *.exe (NSIS) |
Yes — recommended for users. |
| Windows | *.msi |
No — provided for IT/MDM use. |
| macOS | *.dmg (universal) |
Yes. |
| Linux | *.AppImage |
Yes. |
| Linux | *.deb |
No — manual re-install only. |
| Linux | *.rpm |
No — manual re-install only. |
.sig files alongside each installer let users verify integrity against the
updater public key out-of-band if they need to.