desktop-updates-electron-updater
Cross-platform auto-update patterns with electron-updater (electron-builder ecosystem)
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-updates-electron-updater/skills/desktop-updates-electron-updater
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Electron Auto-Update Patterns
Quick Guide: Use
electron-updater(from electron-builder) for cross-platform auto-updates. It supports macOS (DMG), Windows (NSIS), and Linux (AppImage/DEB/RPM). Configure a provider (GitHub, S3, generic server) in yourelectron-builderconfig. The updater emits lifecycle events:checking-for-update->update-available->download-progress->update-downloaded. SetautoDownload: falsefor manual download control. Use channels (latest/beta/alpha) for staged releases andstagingPercentagefor gradual rollouts. Code signing is mandatory on macOS and strongly recommended on Windows.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST guard update checks with app.isPackaged -- calling checkForUpdates() in development causes confusing errors and network calls to non-existent endpoints)
(You MUST handle the error event on the updater -- unhandled update errors crash the main process)
(You MUST code-sign macOS builds -- unsigned apps cannot auto-update and the updater silently fails)
(You MUST NOT call quitAndInstall() without confirming the user's intent -- forcing a restart mid-work causes data loss)
(You MUST use named constants for all intervals and timeouts -- no magic numbers in setInterval or retry logic)
</critical_requirements>
Auto-detection: electron-updater, autoUpdater from electron-updater, checkForUpdates, checkForUpdatesAndNotify, update-available, update-downloaded, download-progress, quitAndInstall, autoDownload, stagingPercentage, dev-app-update.yml, NsisUpdater, MacUpdater, AppImageUpdater, setFeedURL, allowPrerelease, allowDowngrade, forceDevUpdateConfig, disableDifferentialDownload
<decision_framework>
Decision Framework
Which Update Approach?
Building with electron-builder?
+-- YES --> Use electron-updater (this skill)
+-- NO --> Building with Electron Forge?
+-- YES --> Using Squirrel maker?
| +-- YES --> Use Electron's built-in autoUpdater module
| +-- NO --> Can use electron-updater with custom config
+-- NO --> Distributing via app store?
+-- YES --> Use the store's native update mechanism
+-- NO --> Use electron-updater with generic provider
Which Provider?
Where are your releases hosted?
+-- GitHub Releases (public or private repo)
| +-- Use provider: github
+-- AWS S3 or compatible (MinIO, Backblaze B2)
| +-- Use provider: s3
+-- DigitalOcean Spaces
| +-- Use provider: spaces
+-- Any HTTP(S) server (Nginx, CDN, custom)
| +-- Use provider: generic
+-- Keygen (license-gated updates)
+-- Use provider: keygen
autoDownload: true vs false?
Should updates download automatically?
+-- App is small (<50 MB) and users expect seamless updates?
| +-- autoDownload: true (default) + checkForUpdatesAndNotify()
+-- App is large or users are on metered connections?
| +-- autoDownload: false + show download prompt in UI
+-- Enterprise environment with IT-managed rollouts?
+-- autoDownload: false + admin-controlled trigger
</decision_framework>
Detailed resources:
- examples/core.md - Setup, lifecycle events, manual download, providers, error handling with retry
- examples/channels-and-rollouts.md - Update channels, staged rollouts, channel switching
- examples/testing.md - Local testing, dev-app-update.yml, debugging with logging
- reference.md - API quick reference, event payloads, provider comparison, security checklist
<red_flags>
RED FLAGS
Critical Issues:
- Calling
checkForUpdates()orcheckForUpdatesAndNotify()outsideapp.isPackagedguard -- causes errors and unnecessary network calls in development - Not handling the
errorevent onautoUpdater-- unhandled update errors crash the main process - Shipping unsigned macOS builds -- auto-update silently fails without code signing
- Calling
quitAndInstall()immediately without user confirmation -- forces restart, risks data loss - Using Electron's built-in
autoUpdatermodule instead of importing fromelectron-updater-- different API, different behavior, no Linux support
Architecture Issues:
- Running update logic in the renderer process --
electron-updatermust run in the main process only - Checking for updates on every app launch without a cooldown -- hammers the update server, especially with large user bases
- Not using
autoInstallOnAppQuitwhenautoDownloadis true -- users never get the update if they don't explicitly restart - Mixing Squirrel.Windows and NSIS updater patterns -- they are incompatible (Squirrel uses
.nupkgdelta files, NSIS uses blockmap-based differential downloads)
Staged Rollout Mistakes:
- Setting
stagingPercentage: 0expecting it to block all updates -- behavior is undefined at 0; use channels for access control instead - Not incrementing version when pulling a broken staged release -- users already on the broken version stay there
- Editing
stagingPercentageinlatest.ymlwithout re-signing -- signature validation fails
Common Mistakes:
- Forgetting
generateUpdatesFilesForAllChannels: truewhen using beta/alpha channels -- only the current channel's YAML is generated - Using
allowPrerelease: trueon the client instead of proper channels --allowPrereleaseonly works with GitHub provider and is less predictable than channels - Not setting
autoUpdater.loggerduring debugging -- update failures are silent without logging configured - Hardcoding update URLs instead of using
electron-builderpublish config -- the build process auto-generates correct metadata only when publish is configured
Gotchas & Edge Cases:
checkForUpdatesAndNotify()returnsnullwhenapp.isPackagedis false -- it silently skips in dev- Differential downloads (blockmap) only work for NSIS on Windows -- macOS and Linux always do full downloads
quitAndInstall(true)(silent mode) only works on Windows NSIS -- macOS ignores theisSilentparameter- The
download-progressevent does not fire when differential download is used -- only fires for full downloads - On Windows, the updater verifies the code signature of the downloaded installer by default (
verifyUpdateCodeSignature) -- unsigned updates are rejected setFeedURL()overrides the provider fromelectron-builderconfig at runtime -- useful for switching environments but can cause confusion if called unintentionally
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST guard update checks with app.isPackaged -- calling checkForUpdates() in development causes confusing errors and network calls to non-existent endpoints)
(You MUST handle the error event on the updater -- unhandled update errors crash the main process)
(You MUST code-sign macOS builds -- unsigned apps cannot auto-update and the updater silently fails)
(You MUST NOT call quitAndInstall() without confirming the user's intent -- forcing a restart mid-work causes data loss)
(You MUST use named constants for all intervals and timeouts -- no magic numbers in setInterval or retry logic)
Failure to follow these rules will cause silent update failures, crashes, or data loss for end users.
</critical_reminders>
Files (skills)
-
examples
-
channels-and-rollouts.md 5.8 KB
# Electron Auto-Update - Channels and Staged Rollouts > Update channels (stable/beta/alpha), staged rollouts with percentage-based distribution, channel switching at runtime. See [SKILL.md](../SKILL.md) for decision frameworks. See [core.md](core.md) for basic setup. --- ## Update Channels Configuration Channels allow you to distribute pre-release versions to specific user groups. The channel is determined by the version suffix in `package.json`. ### Build Configuration ```json // package.json -- beta release { "name": "my-app", "version": "2.1.0-beta", "build": { "generateUpdatesFilesForAllChannels": true } } ``` ```json // package.json -- stable release (no suffix) { "name": "my-app", "version": "2.1.0" } ``` **Key point:** Setting `generateUpdatesFilesForAllChannels: true` produces metadata files for all channels in a single build. Without this, only the current channel's YAML file is generated. ### Generated Metadata Files | Version Suffix | Files Generated (with `generateUpdatesFilesForAllChannels: true`) | | -------------- | ----------------------------------------------------------------- | | `2.1.0` | `latest.yml`, `latest-mac.yml`, `latest-linux.yml` | | `2.1.0-beta` | Above + `beta.yml`, `beta-mac.yml`, `beta-linux.yml` | | `2.1.0-alpha` | Above + `alpha.yml`, `alpha-mac.yml`, `alpha-linux.yml` | ### Channel Inheritance Users receive updates from their channel AND all more-stable channels: ``` alpha channel --> receives: alpha + beta + stable releases beta channel --> receives: beta + stable releases latest (stable) --> receives: stable releases only ``` --- ## Switching Channels at Runtime Allow users to opt into or out of pre-release channels from the app's settings. ```javascript const { autoUpdater } = require("electron-updater"); const { ipcMain } = require("electron"); // Read saved preference (from your config/settings store) const savedChannel = getUserPreference("updateChannel") || "latest"; autoUpdater.channel = savedChannel; // User changes channel in settings ipcMain.handle("set-update-channel", (_event, channel) => { const VALID_CHANNELS = ["latest", "beta", "alpha"]; if (!VALID_CHANNELS.includes(channel)) { throw new Error(`Invalid channel: ${channel}`); } autoUpdater.channel = channel; saveUserPreference("updateChannel", channel); // Setting channel automatically enables allowDowngrade, // so switching from beta -> latest will downgrade to stable autoUpdater.checkForUpdates(); return { channel, allowDowngrade: autoUpdater.allowDowngrade }; }); ``` **Why good:** Validates channel input, persists preference, triggers immediate check after switch, explains allowDowngrade behavior **Gotcha:** When `generateUpdatesFilesForAllChannels` is true, `allowDowngrade` is automatically set to true. This means a user switching from `beta` to `latest` will downgrade to the latest stable version. If you want to prevent downgrades, explicitly set `autoUpdater.allowDowngrade = false` after setting the channel. --- ## Staged Rollouts Staged rollouts distribute an update to a percentage of your user base. This is controlled by editing the metadata YAML file after publishing (not in the electron-builder config). ### Setting Up a Staged Rollout ```yaml # latest.yml (manually edited after build/publish) version: 2.1.0 path: my-app-setup-2.1.0.exe sha512: abc123... releaseDate: "2025-03-15T10:00:00.000Z" stagingPercentage: 10 ``` ### Gradual Rollout Strategy ``` Day 1: stagingPercentage: 10 -- 10% of users, monitor error reports Day 3: stagingPercentage: 30 -- Expand if no issues Day 5: stagingPercentage: 50 -- Half the user base Day 7: stagingPercentage: 100 -- Full rollout (or remove the field) ``` ### How Staging Works Internally Each installation gets a persistent random UUID. The updater hashes this UUID to a number between 0-100 and compares it to `stagingPercentage`. This means: - The same user consistently gets or skips the update (deterministic, not random each check) - Increasing the percentage includes previously-excluded users - Decreasing the percentage excludes some previously-included users ### Pulling a Broken Staged Release If a staged release has critical bugs: ```yaml # WRONG: setting stagingPercentage to 0 does NOT reliably stop the rollout version: 2.1.0 stagingPercentage: 0 # Undefined behavior -- do not rely on this # CORRECT: publish a new version that supersedes the broken one version: 2.1.1 # Fix version -- even if the "fix" is just a revert stagingPercentage: 100 # Ensure all users (including those on 2.1.0) get this ``` **Key point:** Users already on the broken version (2.1.0) will NOT downgrade to 2.1.0 again. You must publish a higher version number. This is the most common staged rollout mistake. --- ## Combining Channels and Staged Rollouts Use channels for user opt-in (beta testers) and staged rollouts for controlled distribution within a channel. ``` Release strategy for v3.0.0: 1. Publish 3.0.0-alpha --> alpha channel testers (days 1-7) 2. Publish 3.0.0-beta --> beta channel testers (days 8-14) 3. Publish 3.0.0 --> stable channel, stagingPercentage: 10 (day 15) 4. Increase staging --> stagingPercentage: 50 (day 18) 5. Full rollout --> stagingPercentage: 100 (day 21) ``` --- ## GitHub Provider with Channels When using the GitHub provider, releases are matched to channels by version tag: ``` GitHub Release: v2.1.0-beta.1 --> beta channel GitHub Release: v2.1.0 --> latest (stable) channel ``` **Gotcha:** GitHub release detection does not always respect the channel from the version tag alone. Set the `channel` property explicitly in your publish config when using GitHub: ```yaml # electron-builder.yml publish: provider: github owner: my-org repo: my-app channel: beta # Explicit channel for GitHub ``` -
core.md 10.4 KB
# Electron Auto-Update - Core Patterns > Setup, lifecycle events, manual download control, provider configuration, error handling with retry. See [SKILL.md](../SKILL.md) for decision frameworks and red flags. See [channels-and-rollouts.md](channels-and-rollouts.md) for update channels and staged rollouts. --- ## Full Lifecycle Setup ```javascript // main.js const { app, BrowserWindow, ipcMain, dialog } = require("electron"); const { autoUpdater } = require("electron-updater"); const log = require("electron-log"); // or your logging solution const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours autoUpdater.logger = log; autoUpdater.logger.transports.file.level = "info"; function setupAutoUpdater(mainWindow) { // CRITICAL: Never check for updates in development if (!app.isPackaged) return; // 1. Checking started autoUpdater.on("checking-for-update", () => { log.info("Checking for update..."); mainWindow.webContents.send("update-status", "checking"); }); // 2. Update found autoUpdater.on("update-available", (info) => { log.info(`Update available: ${info.version}`); mainWindow.webContents.send("update-available", { version: info.version, releaseDate: info.releaseDate, releaseNotes: info.releaseNotes, }); }); // 3. Already up to date autoUpdater.on("update-not-available", (info) => { log.info(`Already up to date: ${info.version}`); mainWindow.webContents.send("update-status", "up-to-date"); }); // 4. Download progress (full downloads only, not differential) autoUpdater.on("download-progress", (progress) => { mainWindow.webContents.send("update-progress", { percent: progress.percent, bytesPerSecond: progress.bytesPerSecond, transferred: progress.transferred, total: progress.total, }); }); // 5. Download complete -- ready to install autoUpdater.on("update-downloaded", (info) => { log.info(`Update downloaded: ${info.version}`); mainWindow.webContents.send("update-downloaded", { version: info.version, releaseNotes: info.releaseNotes, }); }); // 6. Error handling -- MUST be wired up autoUpdater.on("error", (error) => { log.error("Auto-update error:", error.message); mainWindow.webContents.send("update-error", error.message); }); // Initial check + periodic re-check autoUpdater.checkForUpdatesAndNotify(); setInterval(() => autoUpdater.checkForUpdates(), CHECK_INTERVAL_MS); } // Handle user-initiated install ipcMain.handle("install-update", () => { // quitAndInstall() closes all windows and installs autoUpdater.quitAndInstall(); }); app.whenReady().then(() => { const mainWindow = createWindow(); setupAutoUpdater(mainWindow); }); ``` **Why good:** Guards with `app.isPackaged`, handles all lifecycle events including errors, uses named constants for intervals, sends progress to renderer via IPC, logger attached for debugging ```javascript // BAD: common mistakes in a single example const { autoUpdater } = require("electron"); // WRONG: built-in module, not electron-updater autoUpdater.checkForUpdates(); // WRONG: no isPackaged guard setInterval(() => autoUpdater.checkForUpdates(), 60000); // WRONG: magic number // WRONG: no error handler -- crashes if network fails ``` **Why bad:** Imports from wrong package (`electron` vs `electron-updater`), no `app.isPackaged` guard causes dev errors, magic number interval, missing error handler crashes the process --- ## Manual Download Control When `autoDownload` is false, the updater checks for updates but does not download them. You control when the download starts. ```javascript const { autoUpdater } = require("electron-updater"); const { ipcMain } = require("electron"); autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = true; // Install on next quit after download function setupManualUpdater(mainWindow) { if (!app.isPackaged) return; autoUpdater.on("update-available", (info) => { // Send update info to renderer -- let user decide mainWindow.webContents.send("update-available", { version: info.version, releaseDate: info.releaseDate, releaseNotes: info.releaseNotes, }); }); autoUpdater.on("download-progress", (progress) => { mainWindow.webContents.send("update-progress", { percent: Math.round(progress.percent), transferred: progress.transferred, total: progress.total, }); }); autoUpdater.on("update-downloaded", (info) => { mainWindow.webContents.send("update-ready", { version: info.version }); }); autoUpdater.on("error", (error) => { mainWindow.webContents.send("update-error", error.message); }); autoUpdater.checkForUpdates(); } // User clicks "Download Update" in the renderer ipcMain.handle("start-update-download", async () => { const result = await autoUpdater.downloadUpdate(); return result; // Array of downloaded file paths }); // User clicks "Restart and Install" ipcMain.handle("install-update", () => { // IMPORTANT: Confirm user intent before calling this autoUpdater.quitAndInstall(); }); ``` **Why good:** User has full control over when to download and install, progress tracking enabled, `autoInstallOnAppQuit` ensures the update installs even if user does not explicitly restart --- ## Runtime Provider Override with setFeedURL Override the provider at runtime when you need environment-specific update URLs or private repository authentication. ```javascript const { autoUpdater } = require("electron-updater"); // GitHub provider with authentication (private repos) autoUpdater.setFeedURL({ provider: "github", owner: "my-org", repo: "my-app", token: process.env.GH_TOKEN, // For private repos }); // Generic server provider autoUpdater.setFeedURL({ provider: "generic", url: "https://releases.example.com/updates", }); // S3 provider autoUpdater.setFeedURL({ provider: "s3", bucket: "my-app-releases", region: "us-east-1", path: "/releases", }); // Private repo with custom auth header autoUpdater.addAuthHeader(`Bearer ${accessToken}`); autoUpdater.checkForUpdates(); ``` **Why good:** `setFeedURL()` overrides publish config at runtime, `addAuthHeader()` handles private repos without exposing tokens in the build config **Gotcha:** `setFeedURL()` must be called before `checkForUpdates()`. Calling it after a check is in progress has no effect on the current check. --- ## Error Handling with Retry and Backoff ```javascript const { autoUpdater } = require("electron-updater"); const MAX_RETRIES = 3; const BASE_DELAY_MS = 30_000; // 30 seconds let retryCount = 0; function checkForUpdatesWithRetry(mainWindow) { if (!app.isPackaged) return; autoUpdater.on("error", (error) => { log.error( `Update check failed (attempt ${retryCount + 1}):`, error.message, ); if (retryCount < MAX_RETRIES) { retryCount++; const delay = BASE_DELAY_MS * Math.pow(2, retryCount - 1); // Exponential backoff log.info(`Retrying in ${delay / 1000}s...`); setTimeout(() => autoUpdater.checkForUpdates(), delay); } else { log.error("Max retries reached. Update check abandoned."); mainWindow.webContents.send( "update-error", "Update check failed after multiple attempts.", ); retryCount = 0; // Reset for next scheduled check } }); autoUpdater.on("update-available", () => { retryCount = 0; // Reset on success }); autoUpdater.on("update-not-available", () => { retryCount = 0; // Reset on success }); autoUpdater.checkForUpdates(); } ``` **Why good:** Exponential backoff prevents hammering the server, resets retry count on success, named constants for retry limits and delays, logs each attempt for debugging --- ## Preload Script Integration Expose update-related IPC channels to the renderer through the preload script. ```javascript // preload.js const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("updaterAPI", { // Listen for update events from main onUpdateAvailable: (callback) => { ipcRenderer.on("update-available", (_event, info) => callback(info)); }, onUpdateProgress: (callback) => { ipcRenderer.on("update-progress", (_event, progress) => callback(progress)); }, onUpdateReady: (callback) => { ipcRenderer.on("update-ready", (_event, info) => callback(info)); }, onUpdateError: (callback) => { ipcRenderer.on("update-error", (_event, message) => callback(message)); }, onUpdateStatus: (callback) => { ipcRenderer.on("update-status", (_event, status) => callback(status)); }, // Trigger actions from renderer startDownload: () => ipcRenderer.invoke("start-update-download"), installUpdate: () => ipcRenderer.invoke("install-update"), checkForUpdates: () => ipcRenderer.invoke("check-for-updates"), // Cleanup removeAllListeners: () => { ipcRenderer.removeAllListeners("update-available"); ipcRenderer.removeAllListeners("update-progress"); ipcRenderer.removeAllListeners("update-ready"); ipcRenderer.removeAllListeners("update-error"); ipcRenderer.removeAllListeners("update-status"); }, }); ``` **Why good:** Follows Electron's contextBridge pattern, exposes narrow typed API, includes cleanup method to prevent memory leaks, separates event listeners from action triggers --- ## quitAndInstall Options (Windows NSIS) ```javascript // Standard install -- shows installer UI, runs app after install autoUpdater.quitAndInstall(); // Silent install -- no UI, runs app after install (Windows NSIS only) const IS_SILENT = true; const RUN_AFTER = true; autoUpdater.quitAndInstall(IS_SILENT, RUN_AFTER); // Silent install, do NOT run app after (useful for background services) autoUpdater.quitAndInstall(true, false); ``` **Gotcha:** The `isSilent` parameter only works on Windows NSIS. On macOS, the installer always shows the standard DMG UI. The `isForceRunAfter` parameter is also Windows-only. --- ## Logging for Debugging ```javascript const { autoUpdater } = require("electron-updater"); const log = require("electron-log"); // or your logging solution // Attach logger -- update events are logged automatically autoUpdater.logger = log; autoUpdater.logger.transports.file.level = "debug"; // Verbose for debugging // Set to null to disable all logging // autoUpdater.logger = null; ``` **Key point:** With a logger attached, electron-updater logs all event transitions, download progress, provider resolution, and errors to the log file. This is the primary debugging tool when updates fail silently. -
testing.md 6.3 KB
# Electron Auto-Update - Testing > Local testing with dev-app-update.yml, debugging with logging, testing without packaging. See [SKILL.md](../SKILL.md) for decision frameworks. See [core.md](core.md) for production setup. --- ## Local Testing with dev-app-update.yml Test the update flow during development without packaging the app. Create a `dev-app-update.yml` in your project root that mirrors your publish config. ### Generic Server (Simplest) ```yaml # dev-app-update.yml (project root) provider: generic url: http://localhost:8080/updates ``` Serve the update artifacts from a local HTTP server: ``` local-updates/ latest.yml # Metadata pointing to the installer my-app-setup-2.0.0.exe # The actual installer (or .dmg / .AppImage) ``` ```yaml # latest.yml (hand-crafted for testing) version: 2.0.0 path: my-app-setup-2.0.0.exe sha512: <sha512-hash-of-the-installer> releaseDate: "2025-03-15T10:00:00.000Z" ``` ### Enable Dev Config in Main Process ```javascript const { autoUpdater } = require("electron-updater"); const { app } = require("electron"); const log = require("electron-log"); // or your logging solution // Force dev update config when not packaged if (!app.isPackaged) { autoUpdater.forceDevUpdateConfig = true; } // Verbose logging for debugging autoUpdater.logger = log; autoUpdater.logger.transports.file.level = "debug"; autoUpdater.checkForUpdates(); ``` **Key point:** `forceDevUpdateConfig` makes the updater look for `dev-app-update.yml` in the project root instead of the packaged `app-update.yml`. This is the ONLY way to test update checks without a packaged app. --- ## Using a Local S3-Compatible Server For testing S3 provider flows, use MinIO as a local S3-compatible server. ```yaml # dev-app-update.yml provider: s3 bucket: test-updates endpoint: http://localhost:9000 path: /releases ``` Upload your test artifacts to the MinIO bucket, including the `latest.yml` metadata file. --- ## Debugging Checklist When updates are not working, check these in order: ### 1. Logger Output ```javascript autoUpdater.logger = log; autoUpdater.logger.transports.file.level = "debug"; // Check log file location: // macOS: ~/Library/Logs/<app-name>/main.log // Windows: %USERPROFILE%\AppData\Roaming\<app-name>\logs\main.log // Linux: ~/.config/<app-name>/logs/main.log ``` ### 2. Verify Metadata File ```yaml # latest.yml must contain these fields: version: 2.0.0 # Must be higher than current app version path: my-app-setup.exe # Must match actual filename on server sha512: <valid-hash> # Must match actual file hash releaseDate: "2025-..." # ISO 8601 format ``` ### 3. Common Failure Points | Symptom | Cause | Fix | | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ | | `checkForUpdates()` returns null | `app.isPackaged` is false and `forceDevUpdateConfig` not set | Set `forceDevUpdateConfig = true` | | "No published versions" error | Metadata file not found at provider URL | Verify URL + path combination resolves to `latest.yml` | | "sha512 checksum mismatch" | Installer was modified or hash is wrong | Regenerate `latest.yml` with correct hash | | "Code signing verification failed" | Windows: unsigned installer / wrong publisher | Sign the installer or disable verification for testing | | Silently does nothing | Logger not attached | Set `autoUpdater.logger = log` and check log file | | "net::ERR_CONNECTION_REFUSED" | Local server not running | Start your HTTP/S3 server | ### 4. Disable Code Signing Verification (Testing Only) On Windows, the updater verifies the code signature of downloaded installers by default. For local testing with unsigned builds: ```javascript // TESTING ONLY -- never do this in production if (!app.isPackaged) { autoUpdater.forceDevUpdateConfig = true; // Skip code signature verification for local unsigned builds const { NsisUpdater } = require("electron-updater"); if (autoUpdater instanceof NsisUpdater) { autoUpdater.verifyUpdateCodeSignature = () => Promise.resolve(null); } } ``` **Why this is dangerous in production:** Disabling signature verification means any file matching the URL pattern could be installed -- including malware injected by a network attacker. --- ## Generating sha512 for Manual latest.yml When hand-crafting `latest.yml` for local testing, you need the sha512 hash of the installer: ```bash # macOS / Linux shasum -a 512 my-app-setup-2.0.0.exe | awk '{print $1}' | xxd -r -p | base64 # Or with openssl openssl dgst -sha512 -binary my-app-setup-2.0.0.exe | base64 # Windows (PowerShell) $hash = Get-FileHash -Algorithm SHA512 my-app-setup-2.0.0.exe [Convert]::ToBase64String([System.Convert]::FromHexString($hash.Hash)) ``` **Key point:** The hash in `latest.yml` must be base64-encoded, not hex. electron-updater uses base64 format. --- ## Integration Test Strategy For automated testing of the update flow: 1. **Unit test the update event handlers** -- mock `autoUpdater` and verify your handler functions call the right IPC methods 2. **Test the preload API** -- verify the exposed `updaterAPI` methods map to correct IPC channels 3. **End-to-end flow** -- requires a packaged app + update server; typically tested manually or in CI with a staging environment ```javascript // Example: unit testing the update handler const { EventEmitter } = require("events"); function createMockUpdater() { const mock = new EventEmitter(); // Use your test framework's mock/spy function mock.checkForUpdates = vi.fn(); // or jest.fn(), sinon.stub(), etc. mock.checkForUpdatesAndNotify = vi.fn(); mock.downloadUpdate = vi.fn().mockResolvedValue(["/path/to/file"]); mock.quitAndInstall = vi.fn(); mock.autoDownload = true; mock.logger = null; return mock; } ``` **Key point:** Full auto-update E2E tests are expensive and fragile. Focus unit tests on your event handler logic and preload API. Reserve E2E testing for CI pipelines with packaged builds against a staging update server.
-
-
reference.md 9.8 KB
# Electron Auto-Update Reference > Quick-lookup tables, event payloads, provider comparison, and security checklist. See [SKILL.md](SKILL.md) for decision frameworks and red flags. See [examples/](examples/) for full code examples. --- ## Update Lifecycle Events | Event | Payload | When | | ---------------------- | -------------- | --------------------------------------- | | `checking-for-update` | (none) | Check started | | `update-available` | `UpdateInfo` | Newer version found on server | | `update-not-available` | `UpdateInfo` | Current version is latest | | `download-progress` | `ProgressInfo` | During full download (not differential) | | `update-downloaded` | `UpdateInfo` | Download complete, ready to install | | `error` | `Error` | Any failure during check or download | --- ## UpdateInfo Fields | Field | Type | Description | | ---------------------- | ----------------------------- | ---------------------------------------------------- | | `version` | `string` | Semver version of the update | | `files` | `UpdateFileInfo[]` | Array of update file metadata | | `releaseDate` | `string` | ISO 8601 timestamp | | `releaseName` | `string?` | Optional display name | | `releaseNotes` | `string \| ReleaseNoteInfo[]` | Changelog (string or array if `fullChangelog: true`) | | `stagingPercentage` | `number?` | Rollout percentage (0-100) | | `minimumSystemVersion` | `string?` | Minimum required OS version | --- ## ProgressInfo Fields | Field | Type | Description | | ---------------- | -------- | --------------------------- | | `bytesPerSecond` | `number` | Download speed | | `percent` | `number` | Download completion (0-100) | | `transferred` | `number` | Bytes downloaded | | `total` | `number` | Total bytes to download | --- ## AppUpdater Properties | Property | Type | Default | Description | | ----------------------------- | ---------------------- | ----------- | ---------------------------------------------------- | | `autoDownload` | `boolean` | `true` | Download update automatically when available | | `autoInstallOnAppQuit` | `boolean` | `true` | Install downloaded update on app exit | | `autoRunAppAfterInstall` | `boolean` | `true` | Launch app after installer completes | | `allowPrerelease` | `boolean` | `false` | Accept pre-release versions (GitHub only) | | `allowDowngrade` | `boolean` | `false` | Enable downgrade between channels | | `fullChangelog` | `boolean` | `false` | Return release notes as array (GitHub) | | `channel` | `string` | from config | Override update channel at runtime | | `forceDevUpdateConfig` | `boolean` | `false` | Use `dev-app-update.yml` instead of `app-update.yml` | | `disableDifferentialDownload` | `boolean` | `false` | Force full download (NSIS Windows only) | | `disableWebInstaller` | `boolean` | `false` | Prevent loading unsigned web installers | | `logger` | `Logger?` | `null` | Logger implementing `{ info, warn, error }` | | `requestHeaders` | `OutgoingHttpHeaders?` | `null` | Custom HTTP headers for update requests | --- ## AppUpdater Methods | Method | Returns | Description | | --------------------------------------------- | ------------------------------------ | ---------------------------------------------------- | | `checkForUpdates()` | `Promise<UpdateCheckResult \| null>` | Check for updates silently | | `checkForUpdatesAndNotify(opts?)` | `Promise<UpdateCheckResult \| null>` | Check + show OS notification on download | | `downloadUpdate(token?)` | `Promise<string[]>` | Start download manually (when `autoDownload: false`) | | `quitAndInstall(isSilent?, isForceRunAfter?)` | `void` | Close app and run installer | | `setFeedURL(options)` | `void` | Override provider config at runtime | | `addAuthHeader(token)` | `void` | Add auth header for private repos | --- ## Provider Comparison | Provider | Config Key | Auth | Differential | Notes | | -------- | ---------- | ------------------ | -------------- | ----------------------- | | GitHub | `github` | `GH_TOKEN` env | No | Easiest for open source | | Generic | `generic` | Custom headers | Yes (blockmap) | Any HTTP(S) server | | S3 | `s3` | AWS creds | Yes (blockmap) | Scales well | | Spaces | `spaces` | `DO_KEY_ID` env | Yes (blockmap) | DigitalOcean | | Keygen | `keygen` | `KEYGEN_TOKEN` env | No | License-gated updates | --- ## Platform-Specific Behavior | Feature | macOS (DMG) | Windows (NSIS) | Linux (AppImage) | | ------------------------- | -------------------- | --------------------------- | --------------------- | | Code signing required | Yes (mandatory) | No (recommended) | No | | Differential download | No | Yes (blockmap) | No | | Silent install | No (always shows UI) | Yes (`isSilent` param) | N/A (replaces binary) | | `download-progress` event | Yes | Full download only | Yes | | Signature verification | Apple code sign | `verifyUpdateCodeSignature` | No | | `autoRunAppAfterInstall` | No effect | Yes | No effect | --- ## Channel Quick Reference | Version in package.json | Channel | Metadata Files | Receives Updates From | | ----------------------- | ----------------- | --------------------------------------- | --------------------- | | `2.1.0` | `latest` (stable) | `latest.yml` | Stable only | | `2.1.0-beta` | `beta` | `beta.yml` + `latest.yml` | Beta + stable | | `2.1.0-alpha` | `alpha` | `alpha.yml` + `beta.yml` + `latest.yml` | Alpha + beta + stable | **Requires:** `generateUpdatesFilesForAllChannels: true` in electron-builder config. --- ## Security Checklist - [ ] macOS builds are code-signed with a Developer ID certificate - [ ] Windows builds are code-signed with an EV or standard code signing certificate - [ ] Update server uses HTTPS (not plain HTTP in production) - [ ] Private repo tokens are stored in environment variables, not in code - [ ] `verifyUpdateCodeSignature` is NOT disabled in production (Windows) - [ ] `disableWebInstaller` is `true` unless web installer is specifically needed - [ ] `setFeedURL()` is not called with user-controlled input (prevents redirect attacks) - [ ] `latest.yml` sha512 hash matches the actual installer binary --- ## Metadata File Format (latest.yml) ```yaml version: 2.1.0 path: my-app-setup-2.1.0.exe sha512: <base64-encoded-sha512-of-installer> releaseDate: "2025-03-15T10:00:00.000Z" stagingPercentage: 100 # Optional: 0-100 for staged rollouts ``` **Key point:** The `sha512` value is base64-encoded, not hex. This catches most manual-editing errors. --- ## electron-updater vs Electron's Built-in autoUpdater | Feature | `electron-updater` | Built-in `autoUpdater` | | ------------------------- | ----------------------------------- | --------------------------------------- | | Package | `electron-updater` (npm) | `electron` (built-in) | | Linux support | Yes (AppImage, DEB, RPM) | No | | Windows mechanism | NSIS | Squirrel.Windows | | Providers | GitHub, S3, Spaces, Keygen, Generic | Custom URL only | | Differential downloads | Yes (blockmap on Windows) | Squirrel delta packages | | Staged rollouts | Yes (`stagingPercentage`) | No | | Update channels | Yes (alpha/beta/stable) | No | | Code signing validation | macOS + Windows | macOS only | | `download-progress` event | Yes | No | | Maintained | Yes (electron-builder team) | Limited (Squirrel.Windows unmaintained) | -
SKILL.md 13.6 KB
--- name: desktop-updates-electron-updater description: Cross-platform auto-update patterns with electron-updater (electron-builder ecosystem) --- # Electron Auto-Update Patterns > **Quick Guide:** Use `electron-updater` (from electron-builder) for cross-platform auto-updates. It supports macOS (DMG), Windows (NSIS), and Linux (AppImage/DEB/RPM). Configure a provider (GitHub, S3, generic server) in your `electron-builder` config. The updater emits lifecycle events: `checking-for-update` -> `update-available` -> `download-progress` -> `update-downloaded`. Set `autoDownload: false` for manual download control. Use channels (`latest`/`beta`/`alpha`) for staged releases and `stagingPercentage` for gradual rollouts. Code signing is mandatory on macOS and strongly recommended on Windows. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST guard update checks with `app.isPackaged` -- calling `checkForUpdates()` in development causes confusing errors and network calls to non-existent endpoints)** **(You MUST handle the `error` event on the updater -- unhandled update errors crash the main process)** **(You MUST code-sign macOS builds -- unsigned apps cannot auto-update and the updater silently fails)** **(You MUST NOT call `quitAndInstall()` without confirming the user's intent -- forcing a restart mid-work causes data loss)** **(You MUST use named constants for all intervals and timeouts -- no magic numbers in `setInterval` or retry logic)** </critical_requirements> --- **Auto-detection:** electron-updater, autoUpdater from electron-updater, checkForUpdates, checkForUpdatesAndNotify, update-available, update-downloaded, download-progress, quitAndInstall, autoDownload, stagingPercentage, dev-app-update.yml, NsisUpdater, MacUpdater, AppImageUpdater, setFeedURL, allowPrerelease, allowDowngrade, forceDevUpdateConfig, disableDifferentialDownload <philosophy> **When to use:** - Implementing auto-updates in Electron apps built with electron-builder - Configuring update providers (GitHub Releases, S3, generic HTTP server) - Setting up update channels for beta/alpha testing - Implementing staged rollouts with percentage-based distribution - Controlling download behavior (manual download, progress tracking) - Handling update errors with retry strategies - Testing the update flow locally during development **When NOT to use:** - Apps packaged with Electron Forge using Squirrel (use Electron's built-in `autoUpdater` module instead) - Apps distributed exclusively through platform app stores (macOS App Store, Microsoft Store) -- those have their own update mechanisms - Apps that only need to check for updates and show a "download from website" link (no in-app update needed) </philosophy> --- <patterns> ## Key Patterns ### Pattern 1: Basic Setup with Lifecycle Events Import `autoUpdater` from `electron-updater` (not Electron's built-in module). Wire up lifecycle events in the main process after the app is ready. ```javascript import { autoUpdater } from "electron-updater"; const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours function setupAutoUpdater(mainWindow) { if (!app.isPackaged) return; // Never check in development autoUpdater.on("update-available", (info) => { mainWindow.webContents.send("update-available", info); }); autoUpdater.on("update-downloaded", (info) => { mainWindow.webContents.send("update-downloaded", info); }); autoUpdater.on("error", (error) => { log.error("Update error:", error); }); autoUpdater.checkForUpdatesAndNotify(); setInterval(() => autoUpdater.checkForUpdates(), CHECK_INTERVAL_MS); } ``` **Key point:** `checkForUpdatesAndNotify()` checks and shows a native OS notification when an update downloads. Use `checkForUpdates()` for silent checks when you handle UI yourself. See [examples/core.md](examples/core.md). --- ### Pattern 2: Manual Download Control Set `autoDownload: false` to let users decide when to download. This is essential for metered connections or large updates. ```javascript autoUpdater.autoDownload = false; autoUpdater.on("update-available", (info) => { // Show UI prompt -- user decides whether to download mainWindow.webContents.send("update-available", info); }); // User clicks "Download" in the renderer ipcMain.handle("start-update-download", () => { return autoUpdater.downloadUpdate(); }); ``` **Key point:** With `autoDownload: false`, the `download-progress` and `update-downloaded` events only fire after you explicitly call `downloadUpdate()`. See [examples/core.md](examples/core.md). --- ### Pattern 3: Update Providers Configure where the updater looks for releases. The provider is set in your `electron-builder` config file and can be overridden at runtime with `setFeedURL()`. ```yaml # electron-builder.yml -- GitHub provider (default if GH_TOKEN set) publish: provider: github owner: my-org repo: my-app ``` ```yaml # electron-builder.yml -- Generic HTTP server publish: provider: generic url: https://releases.example.com/updates ``` ```yaml # electron-builder.yml -- S3 bucket publish: provider: s3 bucket: my-app-releases region: us-east-1 path: /releases ``` **Key point:** The first provider in the list is the auto-update source. Additional providers are publishing targets only. See [examples/core.md](examples/core.md) for runtime `setFeedURL()` override. --- ### Pattern 4: Update Channels (Stable/Beta/Alpha) Channels distribute pre-release versions to specific user groups. Append `-beta` or `-alpha` to your `package.json` version to produce channel-specific metadata files. ```json { "version": "2.1.0-beta" } ``` ```yaml # electron-builder.yml generateUpdatesFilesForAllChannels: true ``` ```javascript // Switch channel at runtime autoUpdater.channel = "beta"; // Setting channel automatically enables allowDowngrade ``` **Key point:** Users on `alpha` receive alpha, beta, and stable releases. Users on `beta` receive beta and stable. Users on `latest` (stable) only receive stable releases. See [examples/channels-and-rollouts.md](examples/channels-and-rollouts.md). --- ### Pattern 5: Staged Rollouts Roll out updates gradually by setting `stagingPercentage` in your metadata YAML file. The updater assigns each installation a persistent random ID and compares it against the percentage. ```yaml # latest.yml (manually edited after publishing) version: 2.1.0 stagingPercentage: 10 # Ship to 10% of users first ``` **Key point:** Increment the version when pulling a broken staged release -- users already on the broken version will not downgrade to the same version number. See [examples/channels-and-rollouts.md](examples/channels-and-rollouts.md). --- ### Pattern 6: Error Handling and Retry Network failures during update checks are common. Wrap retry logic around the check and always handle the `error` event. ```javascript const MAX_RETRIES = 3; const RETRY_DELAY_MS = 30_000; // 30 seconds autoUpdater.on("error", (error) => { log.error("Auto-update error:", error.message); // Notify renderer for user-facing feedback mainWindow.webContents.send("update-error", error.message); }); ``` **Key point:** The `error` event fires for network failures, signature verification failures, and corrupted downloads. Never ignore it -- unhandled errors in the updater crash the main process. See [examples/core.md](examples/core.md) for retry with exponential backoff. --- ### Pattern 7: Testing Locally Use `dev-app-update.yml` and `forceDevUpdateConfig` to test the update flow without packaging. ```yaml # dev-app-update.yml (project root) provider: generic url: http://localhost:8080/updates ``` ```javascript if (!app.isPackaged) { autoUpdater.forceDevUpdateConfig = true; } ``` **Key point:** You still need a local HTTP server serving the update artifacts (installer + `latest.yml`). Minio is commonly used as a local S3-compatible server for this purpose. See [examples/testing.md](examples/testing.md). </patterns> --- <decision_framework> ## Decision Framework ### Which Update Approach? ``` Building with electron-builder? +-- YES --> Use electron-updater (this skill) +-- NO --> Building with Electron Forge? +-- YES --> Using Squirrel maker? | +-- YES --> Use Electron's built-in autoUpdater module | +-- NO --> Can use electron-updater with custom config +-- NO --> Distributing via app store? +-- YES --> Use the store's native update mechanism +-- NO --> Use electron-updater with generic provider ``` ### Which Provider? ``` Where are your releases hosted? +-- GitHub Releases (public or private repo) | +-- Use provider: github +-- AWS S3 or compatible (MinIO, Backblaze B2) | +-- Use provider: s3 +-- DigitalOcean Spaces | +-- Use provider: spaces +-- Any HTTP(S) server (Nginx, CDN, custom) | +-- Use provider: generic +-- Keygen (license-gated updates) +-- Use provider: keygen ``` ### autoDownload: true vs false? ``` Should updates download automatically? +-- App is small (<50 MB) and users expect seamless updates? | +-- autoDownload: true (default) + checkForUpdatesAndNotify() +-- App is large or users are on metered connections? | +-- autoDownload: false + show download prompt in UI +-- Enterprise environment with IT-managed rollouts? +-- autoDownload: false + admin-controlled trigger ``` </decision_framework> --- **Detailed resources:** - [examples/core.md](examples/core.md) - Setup, lifecycle events, manual download, providers, error handling with retry - [examples/channels-and-rollouts.md](examples/channels-and-rollouts.md) - Update channels, staged rollouts, channel switching - [examples/testing.md](examples/testing.md) - Local testing, dev-app-update.yml, debugging with logging - [reference.md](reference.md) - API quick reference, event payloads, provider comparison, security checklist --- <red_flags> ## RED FLAGS **Critical Issues:** - Calling `checkForUpdates()` or `checkForUpdatesAndNotify()` outside `app.isPackaged` guard -- causes errors and unnecessary network calls in development - Not handling the `error` event on `autoUpdater` -- unhandled update errors crash the main process - Shipping unsigned macOS builds -- auto-update silently fails without code signing - Calling `quitAndInstall()` immediately without user confirmation -- forces restart, risks data loss - Using Electron's built-in `autoUpdater` module instead of importing from `electron-updater` -- different API, different behavior, no Linux support **Architecture Issues:** - Running update logic in the renderer process -- `electron-updater` must run in the main process only - Checking for updates on every app launch without a cooldown -- hammers the update server, especially with large user bases - Not using `autoInstallOnAppQuit` when `autoDownload` is true -- users never get the update if they don't explicitly restart - Mixing Squirrel.Windows and NSIS updater patterns -- they are incompatible (Squirrel uses `.nupkg` delta files, NSIS uses blockmap-based differential downloads) **Staged Rollout Mistakes:** - Setting `stagingPercentage: 0` expecting it to block all updates -- behavior is undefined at 0; use channels for access control instead - Not incrementing version when pulling a broken staged release -- users already on the broken version stay there - Editing `stagingPercentage` in `latest.yml` without re-signing -- signature validation fails **Common Mistakes:** - Forgetting `generateUpdatesFilesForAllChannels: true` when using beta/alpha channels -- only the current channel's YAML is generated - Using `allowPrerelease: true` on the client instead of proper channels -- `allowPrerelease` only works with GitHub provider and is less predictable than channels - Not setting `autoUpdater.logger` during debugging -- update failures are silent without logging configured - Hardcoding update URLs instead of using `electron-builder` publish config -- the build process auto-generates correct metadata only when publish is configured **Gotchas & Edge Cases:** - `checkForUpdatesAndNotify()` returns `null` when `app.isPackaged` is false -- it silently skips in dev - Differential downloads (blockmap) only work for NSIS on Windows -- macOS and Linux always do full downloads - `quitAndInstall(true)` (silent mode) only works on Windows NSIS -- macOS ignores the `isSilent` parameter - The `download-progress` event does not fire when differential download is used -- only fires for full downloads - On Windows, the updater verifies the code signature of the downloaded installer by default (`verifyUpdateCodeSignature`) -- unsigned updates are rejected - `setFeedURL()` overrides the provider from `electron-builder` config at runtime -- useful for switching environments but can cause confusion if called unintentionally </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST guard update checks with `app.isPackaged` -- calling `checkForUpdates()` in development causes confusing errors and network calls to non-existent endpoints)** **(You MUST handle the `error` event on the updater -- unhandled update errors crash the main process)** **(You MUST code-sign macOS builds -- unsigned apps cannot auto-update and the updater silently fails)** **(You MUST NOT call `quitAndInstall()` without confirming the user's intent -- forcing a restart mid-work causes data loss)** **(You MUST use named constants for all intervals and timeouts -- no magic numbers in `setInterval` or retry logic)** **Failure to follow these rules will cause silent update failures, crashes, or data loss for end users.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.