Skip to content

feat(updates): self-update the packaged app from GitHub releases - #1122

Merged
Juliusolsson05 merged 4 commits into
mainfrom
feat/auto-update
Sep 22, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
feat/auto-update

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Problem

Released apps update only by manually downloading dmg files; today's extension capabilities reached main while the released app (v0.1.0) predates them, and an extension install failed with an accurate-but-stale "unknown capability" error. The release pipeline already produces electron-updater's complete feed (latest-mac.yml + blockmaps on every release) — the app just never read it. Fixes #1120.

Implementation

  • src/main/updates/UpdateService.ts — the safety core, test-first with an injected transport: never auto-installs (autoInstallOnAppQuit forced false at construction AND every check), rate-limited checks (4h, persisted via updateCheckStore's atomic write), dev/unsigned builds no-op with one log line, OS-notification surface with actionable error mapping (offline / rate-limit / signature).
  • Shutdown-gate install exit (sessionShutdownGate.ts, threaded via applicationShutdown.ts): when drain resolves with a pending update, install() REPLACES the final app.quit() — quitAndInstall() re-enters will-quit (where shutdownComplete admits it and onQuitAllowed runs); calling it after a plain quit races process exit and silently drops the update. Pinned by tests.
  • Keep-Editing veto resets only the intent (via the existing setWindowCloseVetoedObserver); the staged update survives for retry — important because MacUpdater latches quitAndInstallCalled forever.
  • Watchdog (10s): if ShipIt never takes over, notify + restore retryability; never app.exit() (races ShipIt).
  • Wiring (index.ts): real autoUpdater from electron-updater 6.6.2 (production dep), T+3min first check + 4h interval + wake re-check (packaged only, unref'd timers), File-menu "Check for Updates…" (dual duty: force check / apply ready update).
  • electron-builder.yml: publish: github — embeds resources/app-update.yml, the previously missing wire. Verified embedded in a locally built .app (full dmg target; note --dir builds skip it by design). Release workflows already upload release/*.yml + blockmaps.

Consulted decisions (Claude agents, recorded per user request)

  1. Policy consult (before finalizing): (a) install must REPLACE the gate's final quit, not follow it; (b) hold quitAndInstall until after drain because of the one-shot latch; (c) isSilent/isForceRunAfter are no-ops on macOS; (d) never app.relaunch() (Squirrel owns relaunch; double instance fights the single-instance lock); (e) tmux skew risk: an update restart is the one restart guaranteed to skew the bundled tmux client against a surviving tmux server (terminal recovery); recovery terminals may need a fresh server after updating — disclosed to users in the ready-notification copy rather than killing sessions.
  2. Pre-merge pitfalls/code review: running; findings will be addressed before merge and recorded here.

Verification

  • 22/22 unit tests across src/main/updates/ + sessionShutdownGate (no-auto-install invariant, gate-installs-instead-of-quits ordering, veto reset, watchdog, dev no-op, rate limit, error mapping)
  • tsc -b clean for touched projects
  • app-update.yml embed proven in a locally packaged .app; unsigned local builds still succeed (package-mac policy untouched)
  • Signed local build with the Developer ID cert in progress
  • CI full suite on this branch

Known limits (honest)

  • No automated old→new interactive update observation (no UI automation in this environment); the release verification is feed-level: v0.1.1 assets include latest-mac.yml pointing at 0.1.1, anonymous fetch works
  • macOS signature identity must stay constant across releases for updates to apply (same cert)

…all path

UpdateService owns the safety core (test-first, injected transport):
never auto-installs, rate-limited checks with persisted clock, OS
notifications, dev-build no-op. The shutdown gate gains an update exit —
a pending update REPLACES the final app.quit with quitAndInstall, per
consulted review: calling it after a plain quit races process exit and
silently drops the update, and MacUpdater latches quitAndInstallCalled
forever, so Keep-Editing resets only the intent. A watchdog restores
retryability instead of force-exiting. electron-builder publish config
embeds app-update.yml (the previously missing wire); File-menu item
forces a check or applies the ready update. Consulted risk recorded:
an update restart can skew the bundled tmux client against a surviving
tmux server; recovery terminals may need a fresh server after updating.

Fixes #1120
A local npm regeneration dropped 27 other-platform esbuild optional
entries, which npm ci on Linux (minimum-node fixture gate) needs; the
hand merge keeps main's complete lock and adds only the updater
subtree.

Refs #1120
… stalled install

Two consulted-review findings: a rejected lastCheck write would become
an unhandledRejection incident on a full disk, and the watchdog's
retry copy promised an in-process restart the committed-quit gate
cannot deliver (a second quit exits without consulting pending()).

Refs #1120
@Juliusolsson05
Juliusolsson05 merged commit 311a2a5 into main Sep 22, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the feat/auto-update branch September 22, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(updates): self-update the packaged app from GitHub releases

1 participant