feat(updates): in-app auto-update via Sparkle#3
Merged
Conversation
Replace the manual UpdateChecker (which only opened GitHub Releases) with Sparkle 2.x: daily background checks (opt-in toggle in Settings, default off), EdDSA signature verification against a public key in Info.plist, silent install + restart through Sparkle's standard user driver. After the first install, subsequent updates skip the Gatekeeper prompt entirely — no more "download from website + right-click → Open" for every version. Why opt-in: the prior Privacy boundary in CLAUDE.md committed to "never automatic" outside the quota path. The toggle defaults off, so users who never flip it keep the original guarantee. CLAUDE.md is updated to reflect the new contract (opt-in automatic check, still no telemetry / no auth / no local data uploaded). Why ad-hoc + Sparkle works without an Apple Developer ID: Sparkle's trust anchor is the EdDSA public key in Info.plist, not Apple's notarization chain. First install still gets one Gatekeeper prompt (ad-hoc signature limitation); every subsequent update is verified by Sparkle's own signature check. Build/sign notes: - SwiftPM stages Sparkle.framework under .build/release/ — package.sh ditto's it into Contents/Frameworks/ and adds @executable_path/../Frameworks to the binary's rpath (SwiftPM doesn't know it's targeting an .app bundle). - Codesign order is inner-first WITHOUT --deep on the outer .app; --deep would rewrite Sparkle's pre-signed nested binaries (XPCServices / Updater.app / Autoupdate) and break its internal trust chain. - UpdateManager.shared is touched at applicationDidFinishLaunching so the scheduled-check timer starts at launch, not lazily when Settings is opened. - CI runs Sparkle's generate_appcast with the SPARKLE_PRIVATE_KEY secret; sparkle:edSignature is grep-asserted in the generated appcast.xml so an unsigned feed can't ship. One-time maintainer setup is documented in CLAUDE.md's new "Sparkle key setup" bullet. Co-Authored-By: Claude Opus 4.7 <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.
Summary
Replaces the manual "open GitHub Releases"
UpdateCheckerwith a full Sparkle 2.x integration. After this lands, users no longer have to download a.dmgfrom the website and clear Gatekeeper for every version — they enable a toggle once, and future versions install silently with one confirmation.appcast.xmlonce a day from GitHub Releases.SPARKLE_PRIVATE_KEYrepo secret; the matching public key (SUPublicEDKey) is baked intoInfo.plist. Sparkle hard-checks signatures, so a tampered feed/zip is silently rejected.Implementation notes
package.shditto's.build/release/Sparkle.frameworkintoContents/Frameworks/and adds@executable_path/../Frameworksto the binary's rpath viainstall_name_tool(SwiftPM doesn't know it's targeting an .app bundle).--deep.--deepwould rewrite Sparkle's pre-signed nested binaries and break its internal trust chain.UpdateManager.sharedis touched inapplicationDidFinishLaunchingso Sparkle's scheduled-check timer starts at app launch, not lazily when the Settings sheet is opened.release.ymlpulls Sparkle 2.9.3'sgenerate_appcasttool (version aligned withPackage.resolved), runs it with theSPARKLE_PRIVATE_KEYsecret, and grep-assertssparkle:edSignaturein the output so an unsigned feed can't ship.One-time maintainer setup (already done for this repo)
bin/generate_keysScripts/Info.plist'sSUPublicEDKey(already done in this PR)SPARKLE_PRIVATE_KEYFull procedure documented in CLAUDE.md's new "Sparkle key setup" bullet.
Test plan
swift build— succeeds./Scripts/package.sh— produces a valid ad-hoc-signed.appwithSparkle.frameworkembeddedcodesign --verify --deep --strict dist/CodingBar.app— passes (nested Sparkle signatures preserved)dist/CodingBar.app/Contents/MacOS/CodingBar --self-test— ALL PASSSparkle.framework/Versions/B/Sparkleloaded from inside the .app bundle, no crashesappcast.xmlalongside.dmg/.zip, then install from the previous version and verify Sparkle's "立刻更新" flow ships v0.2.0 silentlySPARKLE_PRIVATE_KEYsecret is set in repo Settings before the first tag push (CI fails loudly with::error::SPARKLE_PRIVATE_KEY secret is missingif it isn't)🤖 Generated with Claude Code