GenerateSaaS

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:

Skips cleanly (green, no build) unless your storage upload credentials are complete and the desktop app or a package it bundles changed since the last desktop-v* tag.
Computes the next version from the 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.
Builds the per-OS installers across a matrix (Linux always; Windows and macOS when their signing secrets are set, or when unsigned builds are opted in), signing and notarizing where secrets allow. Cap: 30 minutes per platform.
Uploads the installers, the latest*.yml update feeds, and the per-installer .blockmap files to your public storage bucket under the feed URL's path prefix.
Tags desktop-v<version> and creates a GitHub release.

Secrets

The workflow skips cleanly until you configure publishing. Set these repository secrets:

SecretPurpose
STORAGE_PUBLIC_BUCKET, STORAGE_ENDPOINT, STORAGE_REGION, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEYYour 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_IDmacOS 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_NAMEWindows 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 .blockmap files (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.

PlatformSigningUnsigned defaultFirst-open prompt
macOSApple Developer ID + notarizationskippedGatekeeper: right-click then Open, or System Settings > Privacy & Security > "Open Anyway"
WindowsAzure Trusted SigningskippedSmartScreen: "More info" then "Run anyway"
Linuxnone herealways 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.

On this page