Files
trackgo-fe/apps/desktop/docs/AUTO_UPDATER.md
T

11 KiB

Auto-Update System

This document covers the Electron auto-update system powered by electron-updater, including the release workflow, provider configuration, CI/CD requirements, and code signing.


Table of Contents


Architecture Overview

The auto-update flow involves three layers:

┌─────────────────────────────────────────────────────────┐
│  Main Process (src/main/index.ts)                       │
│                                                         │
│  autoUpdater.checkForUpdatesAndNotify()                 │
│       ↓                                                 │
│  Events: checking → available → progress → downloaded   │
│       ↓                                                 │
│  sendToRenderer('updater:*', data)                      │
├─────────────────────────────────────────────────────────┤
│  Preload (src/preload/index.ts)                         │
│                                                         │
│  contextBridge: onUpdateAvailable, onDownloadProgress,  │
│                 onUpdateDownloaded, checkForUpdates,     │
│                 installUpdate                           │
├─────────────────────────────────────────────────────────┤
│  Renderer / React (apps/web)                            │
│                                                         │
│  useElectronUpdater() hook                              │
│  → status, progress, updateInfo, errorMessage           │
│  → checkForUpdates(), installUpdate()                   │
└─────────────────────────────────────────────────────────┘

Lifecycle

  1. App starts → After a 3-second delay, autoUpdater.checkForUpdatesAndNotify() is called.
  2. Update available → If autoDownload is true (default), downloads automatically.
  3. Download progressdownload-progress events are forwarded to the renderer.
  4. Update downloaded → The renderer shows a "Restart to Update" prompt.
  5. User clicks installautoUpdater.quitAndInstall() restarts the app with the new version.

Current Provider: GitHub Releases

The update provider is configured in electron-builder.yml:

publish:
  provider: github
  owner: YOUR_GITHUB_ORG
  repo: YOUR_REPO_NAME

How it Works

  1. When you run electron-builder --publish always, it:

    • Builds the app for your target platform.
    • Uploads the installer(s) to a GitHub Release tagged with the version from package.json.
    • Generates and uploads latest.yml (Windows), latest-mac.yml (macOS), or latest-linux.yml (Linux).
  2. When the packaged app calls checkForUpdates(), electron-updater:

    • Reads app-update.yml from the app's resources/ directory (auto-generated during build).
    • Fetches the appropriate latest*.yml from the configured GitHub release.
    • Compares versions and downloads the update if a newer version exists.

app-update.yml

This file is automatically generated by electron-builder during the build process. It contains the provider configuration and is placed in the packaged app's resources/ directory. You do NOT need to create or manage this file manually.


Release Workflow

Manual Release

# 1. Bump the version
cd apps/desktop
npm version patch  # or minor, major

# 2. Build the web app
cd ../..
pnpm build --filter=web

# 3. Build & publish the Electron app
cd apps/desktop
pnpm run prebuild
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml

Automated Release (GitHub Actions)

A typical CI workflow:

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. Used by electron-builder to create/upload releases.
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 Password for the .p12 certificate.
APPLE_ID macOS only macOS Apple ID email for notarization.
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 Password for the Windows certificate.

Setting Secrets in GitHub Actions

  1. Go to Settings → Secrets and variables → Actions in your repository.
  2. Add each variable as a Repository secret.
  3. Reference them in workflow files as ${{ secrets.VARIABLE_NAME }}.

Setting Variables in Turborepo

In turbo.json, the build task already has env awareness via "inputs": ["$TURBO_DEFAULT$", ".env*"]. For CI-specific variables, pass them through the environment — Turborepo does NOT manage CI secrets.


Switching to AWS S3

To use a private S3 bucket instead of GitHub Releases, update electron-builder.yml:

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

Additional AWS Environment Variables

Variable Description
AWS_ACCESS_KEY_ID IAM access key with S3 write permissions
AWS_SECRET_ACCESS_KEY IAM secret key

S3 Bucket Policy

The bucket must allow public read access to the update files, or you must configure a CloudFront distribution in front of it. electron-updater needs to GET the latest*.yml files without authentication.

Recommended bucket structure:

your-bucket/desktop-releases/
  ├── latest.yml              (Windows)
  ├── latest-mac.yml          (macOS)
  ├── latest-linux.yml        (Linux)
  ├── EigenDesktop-Setup-0.2.0.exe
  ├── EigenDesktop-0.2.0.dmg
  ├── EigenDesktop-0.2.0-mac.zip
  └── EigenDesktop-0.2.0.AppImage

Switching to a Generic File Server

For a self-hosted server (Nginx, Caddy, etc.):

publish:
  provider: generic
  url: https://updates.your-domain.com/desktop

Your server must host the same file structure as S3 above. On each release, upload the installer files and latest*.yml to the server.

Nginx Example

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";
    }
}

Code Signing Requirements

Warning

macOS auto-updates will FAIL without code signing. Apple's Gatekeeper will block unsigned apps, and electron-updater will refuse to apply updates to unsigned builds. This is enforced by the OS, not by Electron.

macOS

  • Requires an Apple Developer ID Application certificate.
  • The electron-builder.yml is already configured with:
    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 version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "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 required 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.
  • Without signing, Windows SmartScreen will show a warning to users.
  • EV certificates eliminate SmartScreen warnings immediately; standard certificates build reputation over time.

Linux

  • Code signing is not required for Linux.
  • AppImage files work without signatures. However, you can optionally sign with GPG for package managers that support it.

Troubleshooting

"Update check failed" on startup

  • Cause: The app is offline, or the update server is unreachable.
  • Impact: None — the error is caught and logged. The app continues to function normally.
  • Verification: Check the main process console for [AutoUpdater] Startup check failed (possibly offline).

app-update.yml not found in production build

  • Cause: The publish block in electron-builder.yml is missing or misconfigured.
  • Fix: Ensure the publish block exists. Run electron-builder --publish never first to verify the file is generated in release/*/resources/app-update.yml.

"Cannot update: code signature is invalid" (macOS)

  • Cause: The app was not signed or the signature is broken.
  • Fix: Ensure CSC_LINK and CSC_KEY_PASSWORD are set correctly in CI. Verify with: codesign --verify --deep --strict release/mac*/EigenDesktop.app.

Updates work on Windows/Linux but not macOS

  • Cause: macOS requires both a signed app AND notarization.
  • Fix: Provide all Apple credential environment variables and ensure the mac.hardenedRuntime and entitlements are configured.