Skip to content

feat(updater): add Sparkle Stage 2 integration#108

Open
3manu31 wants to merge 1 commit into
momenbasel:mainfrom
3manu31:sparkle-stage2
Open

feat(updater): add Sparkle Stage 2 integration#108
3manu31 wants to merge 1 commit into
momenbasel:mainfrom
3manu31:sparkle-stage2

Conversation

@3manu31

@3manu31 3manu31 commented May 27, 2026

Copy link
Copy Markdown
Contributor

feat(updater): add Sparkle Stage 2 - signed appcast + release-workflow integration

What does this PR do?

• Completes the Stage 2 signed-update plumbing from [#104] without changing the Stage 1 runtime fallback (follow up from my PR #99).
• Adds Sparkle feed configuration to the app plist so the app can consume a signed appcast when one is available.
• Adds a new Updates section in Settings so users can opt in or out of automatic update checks and choose how often the app checks.
• Extends the release workflow to generate appcast.xml after the final DMG/ZIP artifacts are produced and to publish the appcast with the release assets.
• Keeps the safe fallback: when no SUFeedURL is configured, the updater still opens the GitHub Releases page.

This is intentionally a conservative Stage 2 change. It finishes the release plumbing and leaves unrelated app/runtime behavior alone.

Type of change

  • Bug fix
  • New feature (opt-in, non-invasive)
  • UI/UX enhancement

Files changed

PureMac/Info.plist — add Sparkle feed + public key configuration and keep automatic checks enabled.
PureMac/Views/Settings/SettingsView.swift — add the Updates settings UI for automatic checks and check interval selection.
.github/workflows/release.yml — generate appcast.xml, upload release assets, and keep the formula bump steps aligned.
scripts/SECRETS.md — document the new Sparkle signing secret and the required release credentials.
scripts/release-local.sh — keep the local release path aligned with CI for dry runs and inspection.
PureMac/Services/UpdateService.swift — keep the Stage 1 fallback path intact.
PureMac/PureMacApp.swift — keep the Updates menu wiring intact.
PureMac.xcodeproj/project.pbxproj and project.yml — project wiring updates that were already needed for the Sparkle configuration.

Why this change

• Stage 2 is the signed-appcast half of the updater story. Stage 1 already landed the opt-in menu/fallback behavior.
• The new Settings UI makes the updater user-facing, so people can control automatic checks instead of being locked into a fixed schedule.
• This closes the release-side work needed to make Sparkle use a signed appcast instead of the Releases-page fallback.
• It keeps the app safe for local development builds and does not force the updater into a production feed when one is not configured.

Related issues

• Closes the Stage 2 ask in #94 once shipped.
• Implements the checklist in #104.

Testing done

• Built the app locally and confirmed the Stage 1 updater behavior still works.
• Confirmed the Settings screen exposes automatic update toggling and interval selection.
• Ran a local signed-feed dry test with a hosted appcast and confirmed Sparkle offers the update.
• Confirmed the signed install path relaunches correctly.
• Confirmed a tampered DMG is rejected by Sparkle verification.
• Reviewed the release workflow shape to confirm it includes appcast generation, artifact upload, and formula update steps.

How to test locally

  1. Run the app with a configured SUFeedURL that points at a signed appcast.
  2. Open Settings → Updates and toggle automatic checks on or off.
  3. Change the check interval and confirm the preference is applied.
  4. Use Updates → Check for Updates.
  5. Confirm Sparkle offers the newer version and installs it.
  6. Relaunch the app and confirm it reports itself as up to date.
  7. Repeat with a tampered DMG to confirm Sparkle rejects the download.

What could not be done here, and why

• A full GitHub Actions release run could not be completed because the maintainer’s paid Apple Developer / App Store Connect credentials are not available in this environment.
• Real Developer ID signing, notarization, and stapling in CI therefore remain blocked on external secrets that only the maintainer can provide.
• The workflow and documentation for that path are in place, but the final production proof has to wait for those credentials.

Notes for reviewers

• This PR is intentionally minimal and conservative.
• It does not add Stage 2 UX extras like delta updates, channels, or in-app changelog rendering.
• The Stage 1 fallback stays intact so local development builds without a feed keep opening GitHub Releases.
• The tamper-rejection test is included because it proves the signed-appcast path is actually active.

AI-assistance disclosure

Drafted using Claude Code. Built and tested locally to confirm the non-apple-related Stage 2 pieces work as expected.

@momenbasel

Copy link
Copy Markdown
Owner

Thanks for this — it's a clean, conservative Stage 2 and the direction is right (opt-in UI, appcast generation in CI, safe Releases-page fallback when SUFeedURL is unset).

Before this can land safely on a production auto-updater, Stage 2 needs the full signed-update chain verified end-to-end, not just the plumbing:

  1. EdDSA keypair generated with Sparkle's generate_keys; private key stored as a repo secret (SPARKLE_PRIVATE_KEY), SUPublicEDKey added to Info.plist.
  2. A stable, hosted appcast URL (e.g. a gh-pages appcast.xml) — not a per-release asset URL, since SUFeedURL must be constant across versions.
  3. SUFeedURL stays unset until a signed appcast is actually published at that URL and verified — otherwise we regress the working "open Releases page" fallback and users get Sparkle error dialogs.
  4. Sparkle's nested XPC bundles (Autoupdate, Updater.app) re-signed with Developer ID + hardened runtime and the whole thing re-notarized/stapled.
  5. A tamper test: confirm a modified DMG is rejected by signature validation before we trust the path.

I'd like to take 1–4 in a dedicated commit on top of this, generate the key, and do the tamper test before flipping the feed on. Keeping #104/#94 open to track that. The Updates settings UI here is good groundwork — let's keep it gated behind the unset-feed fallback for now.

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.

2 participants