Skip to content

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 โ€‹

  1. App launch โ†’ After a 3-second initialization delay, autoUpdater.checkForUpdatesAndNotify() is invoked.
  2. Update detected โ†’ If autoDownload is true (default), the binary payload downloads in the background.
  3. Progress streaming โ†’ download-progress events are forwarded to the renderer via IPC in real-time.
  4. Download complete โ†’ The renderer surfaces a "Restart to Update" prompt to the user.
  5. 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:

yaml
publish:
  provider: github
  owner: YOUR_GITHUB_ORG
  repo: YOUR_REPO_NAME

Operational Mechanics โ€‹

  1. When electron-builder --publish always executes, 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), or latest-linux.yml (Linux).
  2. When the packaged application calls checkForUpdates(), electron-updater:

    • Reads app-update.yml from the app's resources/ directory (auto-generated during build โ€” never manually created).
    • Fetches the appropriate latest*.yml manifest from the configured release endpoint.
    • Performs a semantic version comparison and initiates the download if a newer version exists.

๐Ÿš€ Release Workflow: The Deterministic Pipeline โ€‹

To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries.

Manual Release โ€‹

bash
# 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) โ€‹

yaml
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 โ€‹

VariableRequiredPlatformDescription
GH_TOKENYesAllGitHub personal access token with repo scope. Authorizes release artifact uploads.
CSC_LINKmacOS/WindowsmacOS, WindowsBase64-encoded .p12 code signing certificate. Generate with: base64 -i cert.p12 | pbcopy
CSC_KEY_PASSWORDmacOS/WindowsmacOS, WindowsPassphrase for the .p12 certificate.
APPLE_IDmacOS onlymacOSApple ID email for notarization submission.
APPLE_APP_SPECIFIC_PASSWORDmacOS onlymacOSApp-specific password generated at appleid.apple.com.
APPLE_TEAM_IDmacOS onlymacOSYour Apple Developer Team ID.
WIN_CSC_LINKWindows onlyWindowsSeparate Windows code signing certificate (if different from CSC_LINK).
WIN_CSC_KEY_PASSWORDWindows onlyWindowsPassphrase for the Windows certificate.

Configuring Secrets โ€‹

  1. Navigate to Settings โ†’ Secrets and variables โ†’ Actions in your GitHub repository.
  2. Add each variable as a Repository secret.
  3. 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:

yaml
publish:
  provider: s3
  bucket: your-bucket-name
  region: ap-southeast-1
  path: /desktop-releases
  acl: private

Additional environment variables:

VariableDescription
AWS_ACCESS_KEY_IDIAM access key with S3 PutObject and GetObject permissions
AWS_SECRET_ACCESS_KEYIAM secret key

Bucket structure:

text
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.AppImage

NOTE

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.):

yaml
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:

nginx
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.yml is configured with:
    yaml
    mac:
      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, and APPLE_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:

typescript
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-updater events are emitted.
  • The useElectronUpdater() hook will remain in idle status.

How to Test Updates โ€‹

Auto-update can only be fully validated using a packaged, signed build distributed through a real update channel:

  1. Publish v0.1.0 โ†’ Package and release a signed build.
  2. Bump to v0.2.0 โ†’ Increment the version in package.json.
  3. Publish v0.2.0 โ†’ Package and release the updated build.
  4. 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:

yaml
# electron-builder.yml (temporary, for testing)
publish:
  provider: generic
  url: http://localhost:8080/updates
bash
# Serve the release directory locally
cd apps/desktop/release
python3 -m http.server 8080 --directory .

โš ๏ธ Diagnostic Runbook โ€‹

Issue: "Update check failed" on startup โ€‹

SymptomRoot CauseResolution
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 โ€‹

SymptomRoot CauseResolution
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) โ€‹

SymptomRoot CauseResolution
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 โ€‹

SymptomRoot CauseResolution
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 โ€‹

SymptomRoot CauseResolution
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.