- Updated CONFIGURATION.md to improve navigation and added mermaid diagrams for better visualization of processes. - Revised IPC_ARCHITECTURE.md to clarify the security model and added diagrams to illustrate the architecture. - Improved README.md files in core-api, core-events, core-i18n, and core-storage for consistency and clarity, including better descriptions and structural enhancements.
381 lines
20 KiB
Markdown
381 lines
20 KiB
Markdown
[โ 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
|
|
<?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
|
|
|
|
| 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. |
|