# 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](#architecture-overview) - [Current Provider: GitHub Releases](#current-provider-github-releases) - [Release Workflow](#release-workflow) - [CI/CD Environment Variables](#cicd-environment-variables) - [Switching to AWS S3](#switching-to-aws-s3) - [Switching to a Generic File Server](#switching-to-a-generic-file-server) - [Code Signing Requirements](#code-signing-requirements) - [Troubleshooting](#troubleshooting) --- ## 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 progress** → `download-progress` events are forwarded to the renderer. 4. **Update downloaded** → The renderer shows a "Restart to Update" prompt. 5. **User clicks install** → `autoUpdater.quitAndInstall()` restarts the app with the new version. --- ## Current Provider: GitHub Releases The update provider is configured in `electron-builder.yml`: ```yaml 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 ```bash # 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: ```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 | 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](https://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`: ```yaml 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.): ```yaml 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 ```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"; } } ``` --- ## 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: ```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 com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables ``` - **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.