[โ† Back to Root](../../../README.md) # Desktop Auto-Update System `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. ```mermaid graph TD %% โ”€โ”€โ”€ Styling Definitions (Dark-Mode Friendly Enterprise Palette) โ”€โ”€โ”€ classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a classDef ipcBridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff %% โ”€โ”€โ”€ Subgraphs โ”€โ”€โ”€ subgraph MainProcess ["Main Process (src/main/index.ts)"] AUTO[autoUpdater.checkForUpdatesAndNotify] EVENTS{{Update Events: progress, downloaded, error}} SENDER[sendToRenderer] end subgraph Preload ["IPC Bridge (src/preload/index.ts)"] EXPOSE{contextBridge.exposeInMainWorld} end subgraph Renderer ["Renderer (React App - apps/web)"] HOOK([useElectronUpdater Hook]) ACTIONS[UI Actions: Install, Check] end %% โ”€โ”€โ”€ Flow & Relationships โ”€โ”€โ”€ AUTO ---> EVENTS EVENTS ---> SENDER SENDER ===>|'updater:*' Event Stream| EXPOSE EXPOSE ===>|electronAPI window object| HOOK HOOK -.->|Reactive State Status and Progress| ACTIONS ACTIONS -.->|ipcRenderer.invoke| EXPOSE EXPOSE -.->|Trigger Update or Install| AUTO %% โ”€โ”€โ”€ Apply Styles โ”€โ”€โ”€ class AUTO,EVENTS,SENDER coreEntity; class EXPOSE ipcBridge; class HOOK,ACTIONS appEntity; %% โ”€โ”€โ”€ Subgraph Backgrounds (Transparent for Native GitHub Support) โ”€โ”€โ”€ style MainProcess fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5 style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5 style Renderer fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5 ``` ### 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 ``` > [!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) ```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. 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](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 | 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`: ```yaml 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:** ```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. > [!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:** ```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 > [!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: ```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 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 | 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. |