Skip to content

macOS auto-update downloads but never applies — ShipIt doesn't run, app stuck on old version #2377

@andrewm4894

Description

@andrewm4894

Summary

On macOS, auto-update downloads successfully but never applies. After clicking "Restart to apply", quitAndInstall() stages the install (ShipItState.plist is rewritten) and the app quits — but Squirrel's ShipIt helper never runs the bundle swap or relaunch. The app stays on the old version and does not reopen, so on next launch it re-prompts for the same update. Users get stuck in a permanent update loop.

Reported by @andrewm4894 — stuck on 0.52.49 while 0.52.59/0.52.63 are available. Motivated PR #2372 (adding app_version to events so we can detect stalled versions in analytics).

Impact

Environment

  • macOS (Darwin 25.x), Apple Silicon (darwin-arm64)
  • Electron 41.0.2, Squirrel.Mac auto-updater via update.electronjs.org
  • Installed app: /Applications/PostHog Code.app, properly signed + notarized (Developer ID 2ZQLYN8HTB), writable, not translocated — so signing/permissions/Gatekeeper are ruled out.

Live reproduction (captured 2026-05-26 15:05:42)

Pressed "Restart to apply" with v0.52.59 staged. Captured the deltas to the app log and the Squirrel ShipIt log:

~/.posthog-code/logs/main.log:

[15:05:42.282] (updates)        Installing update and restarting... { downloadedVersion: 'v0.52.59' }
[15:05:42.283] (app-lifecycle)  Partial shutdown started (keeping container)
[15:05:42.283] (watcher-registry) No watchers to shutdown
[15:05:42.321] (database)       Closing database
<log ends — app quit>

After the app quit:

  • ShipItState.plist mtime → updated to 15:05:42 (so quitAndInstall() staged the install), contents:
    bundleIdentifier      = com.posthog.array
    launchAfterInstallation = false        <-- would not relaunch even if it ran
    targetBundleURL       = file:///Applications/PostHog%20Code.app/
    updateBundleURL       = .../update.nBs8I2Q/PostHog%20Code.app/   (verified, signature valid, v0.52.59)
    useUpdateBundleName   = true
    
  • ~/Library/Caches/com.posthog.array.ShipIt/ShipIt_stderr.logbyte-for-byte unchanged (last entry is from 2026-05-23). ShipIt never executed.
  • Installed version after restart: still 0.52.49.
  • App did not relaunch.

The staged bundle itself is fine — codesign --verify --deep --strict passes on update.nBs8I2Q/PostHog Code.app (v0.52.59). The download is not the problem; the install/relaunch handoff is.

Root cause analysis

Finding A — periodic check clobbers the already-staged update (definite code bug)

apps/code/src/main/services/updates/service.tscheckForUpdates():

if (this.updateReady && source !== "periodic") {
  // user check: re-show the existing downloaded-update prompt, return early
  ...
  return { success: true };
}

this.checkingForUpdates = true;
this.emitStatus({ checking: true });
this.performCheck(); // → autoUpdater.checkForUpdates()

Because of && source !== "periodic", a periodic check with updateReady === true does not short-circuit — it kicks off a fresh autoUpdater.checkForUpdates() on top of an already-downloaded update. The hourly timer (CHECK_INTERVAL_MS = 1h) therefore re-checks/re-downloads 0.52.59 repeatedly while it's already staged and waiting for the user.

main.log shows this clearly — repeated "Update available, downloading..." after "Update downloaded, awaiting user confirmation", hour after hour. On Squirrel.Mac, re-initiating a check/download while an update is already downloaded invalidates the pending-install state, which produced the explicit error on earlier attempts:

[09:47:31] (updates) Installing update and restarting... { downloadedVersion: 'v0.52.59' }
           Error: "No update available, can't quit and install"
[11:10:34] (updates) Installing update and restarting... { downloadedVersion: 'v0.52.59' }
           Error: "No update available, can't quit and install"

Proposed fix: short-circuit whenever an update is already staged, regardless of source:

if (this.updateReady) {
  if (source !== "periodic") {
    // user-initiated: re-show the ready prompt
    this.pendingNotification = true;
    this.flushPendingNotification();
    this.emitStatus({
      checking: false,
      updateReady: true,
      version: this.downloadedVersion ?? undefined,
    });
  }
  // never re-check/re-download once an update is staged
  return { success: true };
}

Finding B — ShipIt never runs on apply (live-confirmed; needs deeper investigation)

Even on the latest attempt where quitAndInstall() did not throw and did write ShipItState.plist, ShipIt never executed (stderr log untouched, no swap, no relaunch). Two things to investigate:

  1. installUpdate() ordering in service.ts:

    this.lifecycleService.setQuittingForUpdate();
    await this.lifecycleService.shutdownWithoutContainer(); // awaited cleanup
    this.updater.quitAndInstall();

    Awaiting a partial shutdown before quitAndInstall() may interfere with Squirrel.Mac's expectation about how/when the app terminates and hands off to ShipIt. Worth testing calling quitAndInstall() earlier / verifying the app terminates the way ShipIt's PID-watcher expects.

  2. launchAfterInstallation: false in the staged ShipItState.plist — even a successful swap wouldn't relaunch the app, matching the user's "it never reopens" report. Confirm whether Electron's macOS quitAndInstall() is being invoked in a way that sets this to false, and whether a stale ShipIt state is being reused.

A stale Squirrel state may also be a contributor; clearing ~/Library/Caches/com.posthog.array.ShipIt and retrying is part of the workaround below.

Workaround for affected users

Manual replace (app must be fully quit):

cd ~/Downloads
gh release download v0.52.63 --repo PostHog/code --pattern "*darwin-arm64*.zip" --clobber
ditto -xk *darwin-arm64*.zip .
rm -rf "/Applications/PostHog Code.app"
mv "PostHog Code.app" "/Applications/"
rm -rf "$HOME/Library/Caches/com.posthog.array.ShipIt"   # clear stuck Squirrel state

(Or download the DMG from Releases and drag-replace.)

Suggested next steps

  • Fix Finding A (periodic re-check guard) + add a unit test asserting a periodic checkForUpdates is a no-op when updateReady === true.
  • Investigate Finding B (ShipIt not launching / launchAfterInstallation: false / installUpdate ordering).
  • After feat(analytics): attach app version to all custom events #2372 ships, query analytics on app_version to size how many users are stranded on old versions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions