feat(desktop): signed + notarized macOS app, arm64 only, updating itself - #121
Merged
Merged
Conversation
The macOS build is now signed with the Developer ID Application certificate (BKZ6Y9W9MF), notarized and stapled, and built for Apple Silicon only. With a signature in place the app updates itself through electron-updater (Squirrel.Mac) like Windows/Linux, and the app-triggered `brew upgrade --cask` flow is gone. Signing (electron-builder.yml, build/entitlements.mac.plist): - hardened runtime; entitlements = electron-builder's template plus automation.apple-events, for the app AND its helpers. server.js drives Terminal via osascript, and under the hardened runtime that Apple Event is checked against this app — without the key it is refused with no prompt. - mac targets pinned to arm64 (macOS 27 dropped Intel; Homebrew moved it to Tier 3). NSAppleEventsUsageDescription for the Automation prompt. - local notarization reads a notarytool keychain profile named in the gitignored electron-builder.env, which is also excluded from the bundle. Updates (electron/main.js, preload.js, the banner in index.html): - one electron-updater path on every OS; listeners registered once (per-click registration stacked a set on every Retry). - before-quit-for-update sets app.isQuiting: Squirrel closes the windows before quitting and close-to-tray would cancel the quit. Verified with two signed builds on a local feed: with it, 7.17.90 -> 7.17.91 staged, swapped, relaunched; without it the app stayed on 7.17.90 in the tray. - a dmg mount or an App-Translocated copy cannot be swapped in place; the banner says "move the app to Applications" instead of offering a button. - only an error during an install the user started turns the banner into Retry (update:failed) — a failed CHECK also emits `error`. CI (release-desktop.yml): - the mac leg FAILS without MAC_CSC_LINK, or with an incomplete APPLE_* set: Squirrel refuses unsigned updates, so there is no unsigned fallback. - bump-cask handles the single-arch cask and refuses to push when sed did not rewrite version + sha256 (sed exits 0 on no match). The Homebrew cask stays for about a month so installs from before this release, whose update button runs brew, can reach it; then it is retired (docs/electron-desktop/MAC-SIGNING.md). Tests: test/mac-signing.test.js (new, 12 checks) and a rewritten test/update-flow.test.js (35 checks, the banner run in a vm). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
From an independent review (codex, agy) and an end-to-end rerun: - Block only a read-only VOLUME (EROFS: dmg mount, App Translocation). A permission denial (EACCES, /Applications on a standard account) is left to Squirrel — "move the app to Applications" was wrong advice for an app already there. Measured: dmg → EROFS, /private/var/root → EACCES. - Offer what electron-updater accepted (isUpdateAvailable), not a version comparison: it returns updateInfo for a rejected release too, and downloadUpdate() then failed with "Please check update first". - MacUpdater.quitAndInstall() adds a native listener it never removes when Squirrel fails; the app takes it back off so a Retry cannot stack installs. - One install per attempt, and a synchronous quitAndInstall() failure is a reported failure: an e2e run that downloaded twice threw Squirrel's "The command is disabled" out of a timer — an uncaught main-process exception. - Progress and failures go to every window, not getAllWindows()[0]. - Banner: a click refused as blocked restores its title instead of freezing on "Updating… 1s"; a rejected start() becomes Retry. Re-verified end to end with the final code: signed 7.17.90 → 7.17.91 via a local feed, one download, staged, swapped, relaunched, signature valid. test/update-flow.test.js: 51 checks, each new guard mutation-checked. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The macOS build is signed with the Developer ID certificate, notarized, stapled and arm64-only. With a signature in place the app updates itself through
electron-updater(Squirrel.Mac) like Windows/Linux; the app-triggeredbrew upgrade --caskflow is removed.What changes
automation.apple-events(server.js drives Terminal via osascript; under the hardened runtime that event is checked against this app) for the app and its helpers.electron-updaterpath on every OS.before-quit-for-updatesetsapp.isQuiting(Squirrel closes windows before quitting; close-to-tray would cancel it). A dmg mount / App-Translocated copy is refused with "move the app to Applications". Only an error during a user-started install turns the banner into Retry.MAC_CSC_LINKor with an incompleteAPPLE_*set — no unsigned fallback.bump-caskhandles the single-arch cask and verifies its ownsed.docs/electron-desktop/MAC-SIGNING.md.Verification
npm testgreen (newmac-signing.test.js, rewrittenupdate-flow.test.js; key assertions mutation-checked).spctl→source=Notarized Developer ID,stapler validateOK, all 19 Mach-O signed with runtime + timestamp; the app boots (/api/health200).before-quit-for-updateline: stuck on 7.17.90 in the tray.Before the next release
Set the five repo secrets listed in
MAC-SIGNING.md(MAC_CSC_LINK,MAC_CSC_KEY_PASSWORD,APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD,APPLE_TEAM_ID), or the mac leg fails by design. The tap cask is converted to the single-arch shape right after this merges.