Skip to content

feat: add dormant Sparkle auto-update integration (PR A) - #127

Merged
IchenDEV merged 1 commit into
mainfrom
t3/add-auto-update-support
Oct 17, 2026
Merged

IchenDEV merged 1 commit into
mainfrom
t3/add-auto-update-support

Conversation

@IchenDEV

Copy link
Copy Markdown
Owner

Outcome

PR A of two for in-app auto-update (Sparkle 2.10.0, pinned exact): the dormant integration. Sparkle is linked, embedded, signed and verified in every build, and the code that drives it exists, but no build carries an update feed. The SU* keys exist only in a plist passed to build-app.sh --updates --update-info, which refuses ad-hoc signing, and none is committed. Dev, PR, manual and release-pipeline builds therefore stay inert: the configuration gate resolves to unavailable, no driver is created, nothing contacts a server, and the update UI is hidden. PR B (separate) adds the committed key plist, the release-pipeline metadata steps, docs and the rehearsal.

Originating decision: intent, spec and plan are approved in the bundle below.

SDLC bundle and risk

  • Bundle: docs/sdlc/changes/2026-10-17-auto-update/ (intent, spec, plan approved; verification.md approved for Part A on 2026-10-17)
  • Risk: high (new dependency, signing, release distribution)
  • Human decisions still required:
    • The owner's explicit merge authorization (not given yet).
    • Whether Sparkle's license notice should ship inside the app now or as a separate change (the repo ships no third-party notices today).
    • Running the packaged-app launch smoke with the installed Utter quit (see gaps).

What changed

  • UtterUpdates module: portable configuration gate, status reducer, busy-held relaunch gate and controller; Sparkle only under Native/. UpdateService contract, UpdateProjection, menu row and About section (hidden when inert), automaticUpdateChecks setting (default on), en and zh-Hans strings. The updater is excluded from the recovery composition.
  • Packaging: embed-sparkle.sh (embed, drop XPCServices, thin to arm64, add rpath), three-pass signing with the Sparkle components signed inside-out with the hardened runtime and no entitlements, update-options.sh, check-update-plist.py.
  • verify-release-artifact.sh: Sparkle layout and signing are checked on every build (hardened runtime, same signer as the app, no entitlements, timestamp for Developer ID); SU* keys are forbidden unless --require-updater.
  • Untouched: .github/workflows/**, Resources/Info.plist, Resources/OpenType.entitlements; no ATS exception; Package.resolved differs only by the Sparkle pin and originHash.

Verification

Evidence: verification.md and evidence/.

  • bash scripts/sdlc-checks.sh (the template's python3 scripts/sdlc.py validate --worktree does not exist in this repo)
  • bash scripts/ci-basic-checks.sh (script tests 31 → 52)
  • swift test (XCTest 1143 → 1193, 0 failures, same 18 skips)
  • Release-style app builds with a throwaway self-signed identity (inert, armed with --updates, and the PR-CI style --sign=-); nested signatures and entitlements equal the v0.0.55 baseline apart from the new rpath
  • Real-window light/dark and narrow-width QA: not applicable yet, the UI is hidden in every PR A build; planned for PR B
  • Packaged Utter launch smoke: not run (the owner's Utter was running and shares hotkey, keychain and defaults). Mitigation: a probe app with the same identity, hardened runtime and entitlements loads the embedded Sparkle, with a negative control
  • Independent verifier (fresh context, read-only): PASS WITH FINDINGS; the one high finding (Sparkle signing checks ran only with --require-updater) is fixed and re-checked as resolved

Residual risk and rollback

  • Developer ID signing and notarization of the new nested code are only provable in CI with the real identity; the first dormant release run after merge must pass signing, notarization, stapling and Gatekeeper.
  • The real SPUUpdater path is compiled and driven by fakes only; it cannot run in a shipped build until PR B, whose rehearsal is the first end-to-end proof.
  • Script unit tests now take about 63 s instead of about 10 s.
  • Rollback: revert this PR. It is dormant, so no installed copy ever contacts a server and nothing needs to be withdrawn.

Reviewer focus

  • scripts/verify-sparkle.sh and the three-pass signing in scripts/build-app.sh: any way a check can pass vacuously, or Sparkle's installer can inherit the app's entitlements.
  • That no dormant build can ever carry or act on a feed (UpdateConfiguration.resolve, check-update-plist.py --forbid, --updates refusing ad-hoc).
  • The spec cites ModelCatalogSnapshot.isBusy, which does not exist; the implementation uses CatalogModelStatus.isBusy over all catalogs (documented in verification.md, spec not edited).

Link, embed, sign and verify Sparkle 2.10.0 in every build while shipping no
update feed. The SU* keys exist only in a plist passed to
build-app.sh --updates --update-info, which refuses ad-hoc signing, so dev, PR
and release-pipeline builds stay inert: the configuration gate resolves to
unavailable, no driver is created, nothing contacts a server and the update UI
is hidden.

- UtterUpdates module (portable gate, status reducer, busy-held relaunch;
  Sparkle only under Native/), UpdateService contract and presentation
- automaticUpdateChecks setting (default on), en and zh-Hans strings
- embed-sparkle.sh, verify-sparkle.sh, update-options.sh, check-update-plist.py;
  three-pass signing with Sparkle components signed without entitlements
- verify-release-artifact.sh checks Sparkle layout and signing on every build
  and forbids SU* keys unless --require-updater
- SDLC bundle with intent, spec, plan and approved Part A verification

Co-authored-by: Cursor <cursoragent@cursor.com>
@IchenDEV
IchenDEV merged commit 077f6d8 into main Oct 17, 2026
3 checks passed
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.

1 participant