Files
trackgo-fe/apps/desktop/docs/AUTO_UPDATER.md
T
Firman Ramdhani 75deeece9f refactor: Revise configuration and IPC architecture documentation for clarity and security enhancements
- Updated CONFIGURATION.md to reflect changes in target app orchestration, environment variables, and production routing.
- Enhanced IPC_ARCHITECTURE.md with a focus on privilege separation, standardized operating procedures, and critical audit checklists.
- Added detailed guidelines for extending the IPC bridge and maintaining security integrity.
- Introduced new package scripts for macOS, Windows, and Linux builds in package.json.
2026-04-06 10:17:32 +07:00

16 KiB

Auto-Update System

The Unified Update Lifecycle.

This document defines the strategic implementation of our cross-platform auto-update system. Powered by electron-updater, this architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.


Table of Contents


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.

┌──────────────────────────────────────────────────────────────────┐
│  Main Process  (src/main/index.ts)                               │
│                                                                  │
│  autoUpdater.checkForUpdatesAndNotify()                          │
│       │                                                          │
│       ├─→ 'checking-for-update'                                  │
│       ├─→ 'update-available'     → { version, releaseDate }      │
│       ├─→ 'download-progress'    → { percent, bytesPerSecond }   │
│       ├─→ 'update-downloaded'    → { version, releaseNotes }     │
│       └─→ 'error'               → { message }                   │
│                                                                  │
│  sendToRenderer('updater:*', payload)                            │
├──────────────────────── IPC Bridge ──────────────────────────────┤
│  Preload  (src/preload/index.ts)                                 │
│                                                                  │
│  contextBridge.exposeInMainWorld('electronAPI', {                 │
│    onUpdateAvailable,  onDownloadProgress,                       │
│    onUpdateDownloaded, onUpdateError,                            │
│    checkForUpdates,    installUpdate                             │
│  })                                                              │
├──────────────────────── Renderer ────────────────────────────────┤
│  React App  (apps/web)                                           │
│                                                                  │
│  useElectronUpdater() hook                                       │
│  → Reactive state: status, progress, updateInfo, errorMessage    │
│  → Actions: checkForUpdates(), installUpdate()                   │
└──────────────────────────────────────────────────────────────────┘

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 streamingdownload-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 installautoUpdater.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_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

# 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

Caution

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

  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:

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

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

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

Your server must host the same directory structure as the S3 layout above.

Important

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

Warning

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

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:

# 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 Console logs [AutoUpdater] Startup check failed (possibly offline)
Root Cause The machine is offline, or the update server (GitHub/S3/generic) is unreachable.
Resolution 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 electron-updater throws "Cannot find app-update.yml" immediately after launch.
Root Cause The publish block in electron-builder.yml is missing or misconfigured. electron-builder generates app-update.yml only when a valid provider is declared.
Resolution 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 The updater downloads a new version but refuses to apply it, logging a signature validation error.
Root Cause The application was not signed, or the signing certificate has expired / been revoked.
Resolution 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 Windows and Linux users receive updates, but macOS users see no update prompt.
Root Cause macOS requires both a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload.
Resolution 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 Users report that the update downloads but fails to install, or the downloaded file is 0 bytes.
Root Cause The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale latest*.yml files.
Resolution Verify MIME types: .ymltext/yaml, .exe/.dmg/.AppImage/.zipapplication/octet-stream. Add Cache-Control: no-cache headers to latest*.yml responses. Invalidate CDN cache after publishing a new release.