Releasing
Cut a desktop release from GitHub Actions - the secrets it needs, what it signs and publishes, and where the installers and update feed land.
.github/workflows/desktop-release.yml builds, signs, notarizes, and publishes the app. It runs on workflow_dispatch only - a release happens when you start one from the Actions tab, never because something merged - and then:
desktop-v* tag.bump input: auto (the default) is minor when any feat landed since that tag, else patch. A major is never automatic - dispatch with bump: major. The first release seeds from apps/desktop/package.json.latest*.yml update feeds, and the per-installer .blockmap files to your public storage bucket under the feed URL's path prefix.desktop-v<version> and creates a GitHub release.Secrets
The workflow skips cleanly until you configure publishing. Set these repository secrets:
| Secret | Purpose |
|---|---|
STORAGE_PUBLIC_BUCKET, STORAGE_ENDPOINT, STORAGE_REGION, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY | Your app's public storage account, reused from the app. Installers and the update feed publish here. All five are required together - releases skip until the full set is present. |
MAC_CSC_LINK, MAC_CSC_KEY_PASSWORD, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID | macOS signing (the code-signing cert as a base64 CSC_LINK plus its password) and notarization. macOS builds when all five are set, or when DESKTOP_ALLOW_UNSIGNED is true. |
AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SIGNING_ENDPOINT, AZURE_SIGNING_ACCOUNT_NAME, AZURE_SIGNING_PROFILE_NAME | Windows signing via Azure Trusted Signing (AZURE_SIGNING_PUBLISHER_NAME optional on top). Windows builds when all six are set, or when DESKTOP_ALLOW_UNSIGNED is true. |
Publish to STORAGE_PUBLIC_BUCKET, never STORAGE_PRIVATE_BUCKET. Installers and the update feed must be publicly readable (electron-updater fetches them anonymously), so private user uploads stay in the separate private bucket. The installed app never holds storage credentials; it only talks to your API.
What lands in the bucket
Point config.desktop.autoUpdate.url at the public URL where the feed lives (your STORAGE_PUBLIC_URL plus a path prefix, or a custom domain). electron-builder bakes that URL into the packaged app and the release derives the upload prefix from its path, so the feed, the installers, and the download links always land where the app looks.
- The versioned installers plus their
.blockmapfiles (for delta downloads). latest-mac.yml/latest.yml/latest-linux.yml- the feeds electron-updater polls per OS.- Stable, always-latest aliases named from
config.desktop.protocol:<protocol>.dmg,<protocol>-Setup.exe,<protocol>.AppImage.
Unsigned builds (opt-in)
Each platform ships signed when its signing secrets are set and is otherwise skipped, so a release never produces an unsigned installer by accident.
| Platform | Signing | Unsigned default | First-open prompt |
|---|---|---|---|
| macOS | Apple Developer ID + notarization | skipped | Gatekeeper: right-click then Open, or System Settings > Privacy & Security > "Open Anyway" |
| Windows | Azure Trusted Signing | skipped | SmartScreen: "More info" then "Run anyway" |
| Linux | none here | always builds (AppImage, deb) | - |
To test Windows or macOS before you have certificates, set the repository variable DESKTOP_ALLOW_UNSIGNED to true; the release then also builds the unsigned installers.
Unsigned macOS builds cannot auto-update - treat them as testing-only. Switching to a signed build later is purely adding the signing secrets, with no code change.
With AI on there is no extra binary to sign: the agent runtime is forked from the app's own Electron binary, so the app's signature already seals it. The version is injected at build time from your tag history, so apps/desktop/package.json is never edited on release. To build installers by hand instead, see Development and builds.
End-to-end tests
Run the desktop Playwright harness that drives the built Electron app against a local backend, and the services and system libraries it needs.
Updates and downloads
Background auto-updates via electron-updater, the per-OS update feed, and the /download page plus the always-latest installer links behind it.