Claude Skill

desktop-updates-electron-updater

Cross-platform auto-update patterns with electron-updater (electron-builder ecosystem)

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_desktop-updates-electron-updater_skills_desktop-updates-electron-updater-3a51ef5.zip · 16 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-updates-electron-updater/skills/desktop-updates-electron-updater
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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 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



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


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

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.

No comments yet.

Reviews (0)

No reviews yet.

Related