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.log → byte-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.ts — checkForUpdates():
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:
-
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.
-
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
Summary
On macOS, auto-update downloads successfully but never applies. After clicking "Restart to apply",
quitAndInstall()stages the install (ShipItState.plistis 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.49while0.52.59/0.52.63are available. Motivated PR #2372 (addingapp_versionto events so we can detect stalled versions in analytics).Impact
app_versionevent property once feat(analytics): attach app version to all custom events #2372 lands.Environment
darwin-arm64)update.electronjs.org/Applications/PostHog Code.app, properly signed + notarized (Developer ID2ZQLYN8HTB), 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.59staged. Captured the deltas to the app log and the Squirrel ShipIt log:~/.posthog-code/logs/main.log:After the app quit:
ShipItState.plistmtime → updated to 15:05:42 (soquitAndInstall()staged the install), contents:~/Library/Caches/com.posthog.array.ShipIt/ShipIt_stderr.log→ byte-for-byte unchanged (last entry is from 2026-05-23). ShipIt never executed.The staged bundle itself is fine —
codesign --verify --deep --strictpasses onupdate.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.ts—checkForUpdates():Because of
&& source !== "periodic", a periodic check withupdateReady === truedoes not short-circuit — it kicks off a freshautoUpdater.checkForUpdates()on top of an already-downloaded update. The hourly timer (CHECK_INTERVAL_MS = 1h) therefore re-checks/re-downloads0.52.59repeatedly while it's already staged and waiting for the user.main.logshows 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:Proposed fix: short-circuit whenever an update is already staged, regardless of source:
Finding B — ShipIt never runs on apply (live-confirmed; needs deeper investigation)
Even on the latest attempt where
quitAndInstall()did not throw and did writeShipItState.plist, ShipIt never executed (stderr log untouched, no swap, no relaunch). Two things to investigate:installUpdate()ordering inservice.ts: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 callingquitAndInstall()earlier / verifying the app terminates the way ShipIt's PID-watcher expects.launchAfterInstallation: falsein the stagedShipItState.plist— even a successful swap wouldn't relaunch the app, matching the user's "it never reopens" report. Confirm whether Electron's macOSquitAndInstall()is being invoked in a way that sets this tofalse, and whether a stale ShipIt state is being reused.A stale Squirrel state may also be a contributor; clearing
~/Library/Caches/com.posthog.array.ShipItand retrying is part of the workaround below.Workaround for affected users
Manual replace (app must be fully quit):
(Or download the DMG from Releases and drag-replace.)
Suggested next steps
checkForUpdatesis a no-op whenupdateReady === true.launchAfterInstallation: false/installUpdateordering).app_versionto size how many users are stranded on old versions.