Local installers and updates
Build download files locally
Run this on the operating system you want to package:
make setup # first time
make downloadsThe build is unsigned and intended for local testing. The command prints the exact output paths:
- macOS:
src-tauri/target/<target>/release/bundle/pkg/*.pkg(primary) andsrc-tauri/target/<target>/release/bundle/dmg/*.dmg - Windows:
src-tauri/target/<target>/release/bundle/nsis/*-setup.exe - Linux: AppImage, DEB, and RPM directories under
src-tauri/target/<target>/release/bundle/
On macOS the local target uses non-interactive DMG layout mode. It does not need Cursor/Terminal automation permission to control Finder.
The DMG contains OnionGate.app. A macOS machine builds its own architecture; Windows and Linux installers should be built natively on those systems. GitHub release CI builds every supported target with signing and provenance.
The macOS installer package
Tauri has no pkg bundle target, so the installer is a post-bundle step: scripts/build-macos-pkg.sh takes the .app that Tauri produced and drives Apple's pkgbuild and productbuild. make downloads runs it automatically, and CI runs the same script on both macOS targets, so a local .pkg is built the same way a released one is.
To rebuild only the installer after a bundle already exists:
make macos-pkgThe package installs to /Applications with no option to change that, and its postinstall script registers the privileged helper and pins the sing-box sidecar to a root-owned path. It also injects the uninstaller into the bundle as Contents/Resources/uninstall.command, which is why the .dmg — built by Tauri, untouched by this script — does not carry one.
Local packages are unsigned. Set PKG_SIGNING_IDENTITY to a Developer ID Installer identity if you have one; without it productbuild still succeeds and Gatekeeper prompts on the first double-click.
The updater is unaffected. macOS updates continue to ship as .app.tar.gz payloads, and scripts/build-updater-manifest.mjs matches on that suffix, so adding a .pkg to a release changes nothing about Check for updates.
Local bundles are unsigned
make downloads deliberately passes --no-sign. Gatekeeper, Authenticode, and the Tauri updater trust chain apply only to official CI artifacts.
When to create an update
The first published version is an installation, not an update. Create an update when users already have an older signed OnionGate release and you are ready to publish a newer version.
Before tagging:
- invoke the project
release-changelogCursor skill with the next semantic version; - review and merge its version/changelog change on
mainorstaging; - run
make release-check VERSION=<version>; - tag that exact commit as
v<version>and push the tag.
Release CI creates a draft. Test the installers, checksums, SBOMs, and attestations before publishing.
How automatic updates work
The installed app contains only the updater public key. The matching private key stays offline and in GitHub Actions secrets.
When the user selects Settings → Check for updates:
- OnionGate requests
https://github.com/openhat-security/oniongate/releases/latest/download/latest.json; - the manifest selects the current OS and architecture;
- Tauri downloads the updater payload;
- the embedded public key verifies its signature;
- OnionGate installs it and relaunches.
A missing or invalid signature fails the update. OnionGate never bypasses the check.
Alpha, staging, and stable channels
Tags must point to commits on main or staging.
- While the product is alpha,
maintags use semver prereleases such asv0.2.5-alpha.1(stay on0.y.z-alpha.N; do not jump to1.0.0-alphauntil the signing gates are the active goal). Release CI marks them GitHub prereleases (never Latest). stagingalways produces a prerelease for manual testing.- A plain stable tag (for example
v1.0.0) may become Latest only after the stable-release gate.
GitHub's /releases/latest/ endpoint excludes drafts and prereleases. Therefore the built-in updater follows published non-prerelease releases only. While every downloadable build is a prerelease, /releases/latest/ (and thus latest.json) is empty on purpose — older plain v0.2.x releases are marked superseded/prerelease so they cannot become Latest again. Alpha and staging installers are downloaded from the Releases page (and verified against SHA256SUMS). Do not retag already-published plain v0.2.x tags.
Trust-root warning
Losing TAURI_SIGNING_PRIVATE_KEY or its password prevents existing installations from accepting future updates. Rotating the public key in tauri.conf.json also breaks that trust chain. Keep an encrypted offline backup and never commit either secret.
