Releasing pal
A release is a v* tag. .github/workflows/release.yml bundles it for
every platform onto a draft GitHub release; publishing the draft by hand
is what makes it public and what the in-app updater sees.
Steps
make release VERSION=x.y.zsets the version inapp/src-tauri/tauri.conf.json,app/src-tauri/Cargo.toml,app/package.json,core/Cargo.tomland the two lockfiles. Review the diff, commit it.- Tag and push the tag, as the target prints:
git tag -a vx.y.z -m vx.y.z && git push origin vx.y.z. The workflow refuses a tag that does not matchtauri.conf.json's version. - Three jobs run (
macos-latestfor aarch64 and, cross-compiled, x86_64;ubuntu-24.04for x86_64) and upload to one draft release named after the tag, notes generated from the commits since the previous tag. ~15 to 25 min cold; the release profile is LTO with one codegen unit. - Check the draft on GitHub:
pal_x.y.z_aarch64.dmg,pal_x.y.z_x64.dmgpal_x.y.z_amd64.AppImage,pal_x.y.z_amd64.deb- with the updater key set:
pal_x.y.z_aarch64.app.tar.gz(+.sig),..._x64.app.tar.gz(+.sig),..._amd64.AppImage.sig, andlatest.json - the notes; edit them, the draft is yours until published. Install one dmg and the AppImage somewhere real before publishing: ad-hoc signed, so on macOS it is right-click, Open the first time.
- Publish the draft.
https://github.com/zcag/pal/releases/latest/download/latest.jsonnow resolves to this release's manifest, and every running pal finds it on its next check.
The updater
tauri-plugin-updater (app/src-tauri/src/updater.rs) checks the manifest
above once 20 s after startup and then daily while general.check_updates
is on (default), and on demand through the check_updates command ({ available, version?, notes? }), which the tray's "Check for updates…" will
call. Today a found update is a log line (updater\tavailable\t<version>);
download and install are not wired. Debug builds skip the periodic check.
latest.json is written by tauri-action from the .sig files of every
job (uploadUpdaterJson), platform keys darwin-aarch64, darwin-x86_64,
linux-x86_64, each with the asset URL and its minisign signature. The
signature is checked against plugins.updater.pubkey in tauri.conf.json
at update time, so the key pair is load-bearing: lose the private key and
no installed pal can update to a build signed with a new one (they would
need a manual reinstall).
The Linux updater only handles AppImages; a .deb install sees the update
and cannot apply it (the plugin's limitation, not ours).
Secrets
Repository secrets (Settings, Secrets and variables, Actions):
| secret | what | required |
|---|---|---|
TAURI_SIGNING_PRIVATE_KEY |
the minisign private key, file contents (tauri signer generate) |
no: without it the release builds with createUpdaterArtifacts off and has no latest.json, so the updater never sees it |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
its password | empty for the current key |
GITHUB_TOKEN |
provided by Actions | yes |
The matching public key is committed in tauri.conf.json. A new pair
(cd app && npx tauri signer generate -w <path>) means replacing both, and
every installed pal older than that release updating by hand once.
Local npm run tauri build needs the key in the environment for the same
reason (TAURI_SIGNING_PRIVATE_KEY=<contents or path>); without one, build
with the updater artifacts off:
npm run tauri build -- --config '{"bundle":{"createUpdaterArtifacts":false}}'.
macOS signing, later
Today: ad-hoc (bundle.macOS.signingIdentity: "-", hardenedRuntime: false).
Gatekeeper shows the "unidentified developer" dialog once per install; no
notarisation. For a signed and notarised build:
- A Developer ID Application certificate (Apple Developer Program). Export
it as
.p12; base64 of the file isAPPLE_CERTIFICATE, its passwordAPPLE_CERTIFICATE_PASSWORD, and its name (Developer ID Application: Name (TEAMID))APPLE_SIGNING_IDENTITY. - Notarisation credentials:
APPLE_ID,APPLE_PASSWORD(an app-specific password),APPLE_TEAM_ID. - In
tauri.conf.json:signingIdentityto the Developer ID name (theAPPLE_SIGNING_IDENTITYenv only applies when the config leaves it unset),hardenedRuntime: true, and anentitlementsplist. The bun sidecar is a JIT (JavaScriptCore), so hardened runtime needs at leastcom.apple.security.cs.allow-jit; Bun's own signing notes (docs forbun build --compile, "Codesigning on macOS") also listcom.apple.security.cs.allow-unsigned-executable-memoryandcom.apple.security.cs.disable-executable-page-protection. Start with all three and drop what a notarised build still runs without. - Uncomment the
APPLE_*lines inrelease.yml. tauri-bundler then signs the sidecar and the app inside out, submits to notarytool and staples.
Until then, the dmg is what README.md says: ad-hoc signed, right-click
Open on first launch.
CI
ci.yml runs on every push and pull request on the same two runners:
clippy with -D warnings, cargo test --workspace (tests that need a
pasteboard, an unlocked keychain or the fixture corpus are #[ignore]d and
run by hand with --ignored), a cargo build of the app crate (build.rs,
tauri-build, no bundle), tsc and vitest in app/, tsc and bun test
in host/ (the apps test is macOS-only), the declaration build and a
npm pack --dry-run in sdk/. Caches: cargo (rust-cache), npm
(setup-node), bun's package cache and the fetched bun sidecar
(app/src-tauri/binaries, keyed on fetch-bun.sh).
Rendered from docs/releasing.md in the pal repo, 2026-09-16.