Desktop Auto-Update System โ
Architectural Foundation: electron-updater ยท electron-builder ยท GitHub Actions
Description: Comprehensive auto-update system documentation covering the reactive IPC update flow, GitHub/S3/generic release providers, CI/CD pipeline configuration, code signing requirements, and diagnostic runbook.
apps/desktop utilizes a unified update lifecycle powered by electron-updater. This architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
๐ Reactive Update Flow โ
The following diagram illustrates the Reactive Update Flow, bridging the Node.js Main Process with the React UI layer through a secure IPC event stream.
Lifecycle Sequence โ
- App launch โ After a 3-second initialization delay,
autoUpdater.checkForUpdatesAndNotify()is invoked. - Update detected โ If
autoDownloadistrue(default), the binary payload downloads in the background. - Progress streaming โ
download-progressevents are forwarded to the renderer via IPC in real-time. - Download complete โ The renderer surfaces a "Restart to Update" prompt to the user.
- User-initiated install โ
autoUpdater.quitAndInstall()terminates the current process and launches the updated binary.
๐ Current Provider: GitHub Releases โ
The update provider is declared in electron-builder.yml:
publish:
provider: github
owner: YOUR_GITHUB_ORG
repo: YOUR_REPO_NAMEOperational Mechanics โ
When
electron-builder--publish alwaysexecutes, it:- Compiles the application for the target platform.
- Uploads the installer(s) to a GitHub Release tagged with the version from
package.json. - Generates and uploads the platform-specific manifest:
latest.yml(Windows),latest-mac.yml(macOS), orlatest-linux.yml(Linux).
When the packaged application calls
checkForUpdates(),electron-updater:- Reads
app-update.ymlfrom the app'sresources/directory (auto-generated during build โ never manually created). - Fetches the appropriate
latest*.ymlmanifest from the configured release endpoint. - Performs a semantic version comparison and initiates the download if a newer version exists.
- Reads
๐ Release Workflow: The Deterministic Pipeline โ
To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries.
Manual Release โ
# 1. Version bump โ semver discipline
cd apps/desktop
npm version patch # or: minor, major
# 2. Compile web assets
cd ../..
pnpm build --filter=web
# 3. Synchronize, compile, and publish
cd apps/desktop
pnpm run prebuild
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml> **Treat `GH_TOKEN` as a critical secret.** It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault.
Automated Release (GitHub Actions) โ
name: Release Desktop
on:
push:
tags:
- 'desktop-v*'
jobs:
release:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm build --filter=web
- run: cd apps/desktop && pnpm run prebuild
- run: cd apps/desktop && pnpm run build
- name: Publish
run: cd apps/desktop && npx electron-builder --publish always --config electron-builder.yml
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}๐ CI/CD Environment Variables โ
| Variable | Required | Platform | Description |
|---|---|---|---|
GH_TOKEN | Yes | All | GitHub personal access token with repo scope. Authorizes release artifact uploads. |
CSC_LINK | macOS/Windows | macOS, Windows | Base64-encoded .p12 code signing certificate. Generate with: base64 -i cert.p12 | pbcopy |
CSC_KEY_PASSWORD | macOS/Windows | macOS, Windows | Passphrase for the .p12 certificate. |
APPLE_ID | macOS only | macOS | Apple ID email for notarization submission. |
APPLE_APP_SPECIFIC_PASSWORD | macOS only | macOS | App-specific password generated at appleid.apple.com. |
APPLE_TEAM_ID | macOS only | macOS | Your Apple Developer Team ID. |
WIN_CSC_LINK | Windows only | Windows | Separate Windows code signing certificate (if different from CSC_LINK). |
WIN_CSC_KEY_PASSWORD | Windows only | Windows | Passphrase for the Windows certificate. |
Configuring Secrets โ
- Navigate to Settings โ Secrets and variables โ Actions in your GitHub repository.
- Add each variable as a Repository secret.
- Reference them in workflow files as
${{ secrets.VARIABLE_NAME }}.
โ๏ธ Deployment Strategies โ
AWS S3 (Private Infrastructure) โ
For enterprise environments requiring private infrastructure, the system can be reconfigured to target an AWS S3 Bucket or a CloudFront Distribution.
Update electron-builder.yml:
publish:
provider: s3
bucket: your-bucket-name
region: ap-southeast-1
path: /desktop-releases
acl: privateAdditional environment variables:
| Variable | Description |
|---|---|
AWS_ACCESS_KEY_ID | IAM access key with S3 PutObject and GetObject permissions |
AWS_SECRET_ACCESS_KEY | IAM secret key |
Bucket structure:
your-bucket/desktop-releases/
โโโ latest.yml (Windows manifest)
โโโ latest-mac.yml (macOS manifest)
โโโ latest-linux.yml (Linux manifest)
โโโ EigenDesktop-Setup-0.2.0.exe
โโโ EigenDesktop-0.2.0.dmg
โโโ EigenDesktop-0.2.0-mac.zip
โโโ EigenDesktop-0.2.0.AppImageNOTE
The bucket must allow public read access to the manifest files (latest*.yml), or you must configure a CloudFront distribution. electron-updater performs unauthenticated GET requests to resolve the latest version.
Generic File Server (Self-Hosted) โ
For self-hosted infrastructure (Nginx, Caddy, etc.):
publish:
provider: generic
url: [https://updates.your-domain.com/desktop](https://updates.your-domain.com/desktop)Your server must host the same directory structure as the S3 layout above.
> **MIME Type Configuration**: Ensure your file server correctly serves `.yml` files with `text/yaml` and installer binaries with `application/octet-stream`. Incorrect MIME types will cause download corruption or silent update failures.
Nginx reference:
server {
listen 443 ssl;
server_name updates.your-domain.com;
location /desktop/ {
alias /var/www/desktop-releases/;
autoindex off;
add_header Cache-Control "no-cache";
# MIME types for update manifests
types {
text/yaml yml;
application/octet-stream exe dmg AppImage zip;
}
}
}๐ก๏ธ Code Signing: The Trust Boundary โ
> **Code signing is not merely a requirement โ it is the Trust Boundary established by the operating system.** macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures, `electron-updater` will **reject update payloads entirely**.
macOS โ
- Requires an Apple Developer ID Application certificate ($99/year Apple Developer Program).
- The
electron-builder.ymlis configured with:yamlmac: hardenedRuntime: true gatekeeperAssess: false entitlements: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist - You must create
apps/desktop/build/entitlements.mac.plist:xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "[http://www.apple.com/DTDs/PropertyList-1.0.dtd](http://www.apple.com/DTDs/PropertyList-1.0.dtd)"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-jit</key> <true/> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/> <key>com.apple.security.cs.allow-dyld-environment-variables</key> <true/> </dict> </plist> - Notarization is mandatory for macOS 10.15+. Provide
APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD, andAPPLE_TEAM_ID.
Windows โ
- Requires an EV Code Signing Certificate or a standard code signing certificate from a trusted CA.
- Without signing, Windows SmartScreen warns users with "Windows protected your PC" โ severely impacting adoption.
- EV certificates eliminate SmartScreen warnings immediately; standard certificates build trust reputation over time through Microsoft's telemetry.
Linux โ
- Code signing is not enforced by the OS for AppImage distribution.
- Optional GPG signing is available for package managers that support it.
๐งช Testing Updates in Development โ
NOTE
The auto-updater is intentionally disabled in development mode to prevent runtime crashes. Setting forceDevUpdateConfig requires a dev-app-update.yml file, which introduces unnecessary complexity during local development.
What Happens in Dev Mode โ
In src/main/index.ts, the setupAutoUpdaterEvents() function detects IS_DEV and returns early:
if (IS_DEV) {
autoUpdater.autoDownload = false;
return; // Skip event registration โ no update server in dev
}This means:
- No update check is performed on startup.
- No
electron-updaterevents are emitted. - The
useElectronUpdater()hook will remain inidlestatus.
How to Test Updates โ
Auto-update can only be fully validated using a packaged, signed build distributed through a real update channel:
- Publish v0.1.0 โ Package and release a signed build.
- Bump to v0.2.0 โ Increment the version in
package.json. - Publish v0.2.0 โ Package and release the updated build.
- Launch v0.1.0 โ The app should detect v0.2.0, download it, and prompt the user to restart.
For rapid iteration, use the generic provider pointing to a local Nginx or Python HTTP server:
# electron-builder.yml (temporary, for testing)
publish:
provider: generic
url: http://localhost:8080/updates# Serve the release directory locally
cd apps/desktop/release
python3 -m http.server 8080 --directory .โ ๏ธ Diagnostic Runbook โ
Issue: "Update check failed" on startup โ
| Symptom | Root Cause | Resolution |
|---|---|---|
Console logs [AutoUpdater] Startup check failed (possibly offline) | The machine is offline, or the update server (GitHub/S3/generic) is unreachable. | No action required. The error is caught in a try/catch block, logged to the console, and the application continues to function normally. The next check will occur on the next app launch. |
Issue: app-update.yml not found in production build โ
| Symptom | Root Cause | Resolution |
|---|---|---|
electron-updater throws "Cannot find app-update.yml" immediately after launch. | The publish block in electron-builder.yml is missing or misconfigured. electron-builder generates app-update.yml only when a valid provider is declared. | Verify the publish block exists in electron-builder.yml. Run electron-builder --publish never and inspect release/*/resources/app-update.yml to confirm generation. |
Issue: "Cannot update: code signature is invalid" (macOS) โ
| Symptom | Root Cause | Resolution |
|---|---|---|
| The updater downloads a new version but refuses to apply it, logging a signature validation error. | The application was not signed, or the signing certificate has expired / been revoked. | Ensure CSC_LINK and CSC_KEY_PASSWORD are correctly set in CI. Verify the packaged app with: codesign --verify --deep --strict release/mac*/Desktop.app. Re-sign and re-publish if the certificate was rotated. |
Issue: Updates work on Windows/Linux but not macOS โ
| Symptom | Root Cause | Resolution |
|---|---|---|
| Windows and Linux users receive updates, but macOS users see no update prompt. | macOS requires both a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload. | Provide all Apple credential environment variables (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID) and ensure hardenedRuntime: true is set in electron-builder.yml. Re-package and re-publish. |
Issue: S3/Generic provider returns corrupted downloads โ
| Symptom | Root Cause | Resolution |
|---|---|---|
| Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. | The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale latest*.yml files. | Verify MIME types: .yml โ text/yaml, .exe/.dmg/.AppImage/.zip โ application/octet-stream. Add Cache-Control: no-cache headers to latest*.yml responses. Invalidate CDN cache after publishing a new release. |