Skip to content

feat: in-app updates via Sparkle 2 #63

Description

@sri-rang

Summary

Replace the current "View on GitHub" update flow with a full in-app update experience using Sparkle 2. When an update is available, the user should be able to download and install it without leaving the app.


Current behaviour

AboutView hits the GitHub Releases API, compares the version string, and — if a newer release exists — shows a "View on GitHub" link that opens the browser. The user then manually downloads the DMG, mounts it, drags the app, and relaunches.


Desired behaviour

  1. User clicks Check for Updates (or the app checks automatically on launch, optionally).
  2. If an update is available, a native update sheet/window shows the release notes.
  3. User clicks Install & Relaunch — Sparkle downloads the DMG, verifies the signature, replaces the app bundle via its XPC helper, and relaunches.

Implementation plan

1. Add Sparkle 2 via SPM

In project.yml:

packages:
  Sparkle:
    url: https://github.com/sparkle-project/Sparkle
    from: "2.6.0"

Add Sparkle as a dependency of the Tusk target. Regenerate with xcodegen generate.

2. Generate & store the EdDSA signing key

# Run once, keep the private key out of the repo
./Pods/Sparkle/bin/generate_keys   # or via the SPM plugin
  • Store the private key in 1Password / your secrets manager.
  • Paste the public key into Info.plist as SUPublicEDKey.

3. Publish an appcast

Create docs/appcast.xml (served via GitHub Pages at https://shape-machine.github.io/tusk-macos/appcast.xml) or host it as a raw file in the repo.

Minimum appcast structure:

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
  <channel>
    <title>Tusk</title>
    <item>
      <title>Tusk 2026.07</title>
      <sparkle:version>14</sparkle:version>
      <sparkle:shortVersionString>2026.07</sparkle:shortVersionString>
      <sparkle:releaseNotesLink>https://github.com/Shape-Machine/tusk-macos/releases/tag/v2026.07</sparkle:releaseNotesLink>
      <enclosure
        url="https://github.com/Shape-Machine/tusk-macos/releases/download/v2026.07/Tusk-2026.07.dmg"
        sparkle:edSignature="SIGNATURE_HERE"
        length="BYTES"
        type="application/octet-stream" />
    </item>
  </channel>
</rss>

4. Sign each release DMG

Add to the /release skill after the DMG is created:

./path/to/sparkle/bin/sign_update dist/Tusk-VERSION.dmg

Paste the output sparkle:edSignature and length into the appcast item.

5. Wire Sparkle into the app

Info.plist additions:

<key>SUFeedURL</key>
<string>https://shape-machine.github.io/tusk-macos/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>YOUR_PUBLIC_KEY</string>
<key>SUEnableAutomaticChecks</key>
<false/>

(SUEnableAutomaticChecks off keeps the current manual-only philosophy. Can be revisited.)

TuskApp.swift — hold a reference to the updater:

import Sparkle

@main
struct TuskApp: App {
    private let updaterController = SPTUpdaterController(
        startingUpdater: true,
        updaterDelegate: nil,
        userDriverDelegate: nil
    )
    // ...
}

AboutView.swift — replace the custom checkForUpdates() with Sparkle's:

import Sparkle

// Inject or pass updaterController.updater
Button("Check for Updates") {
    updater.checkForUpdates()
}
.disabled(!updater.canCheckForUpdates)

The .available(version) state, GitHubRelease struct, and isNewer() helper can all be deleted — Sparkle handles this entirely.

6. Update the release skill

Automate appcast generation in .claude/skills/release:

  • After creating the DMG, run sign_update and capture the signature + byte length.
  • Update docs/appcast.xml with the new item (prepend; keep previous items for delta support).
  • Commit docs/appcast.xml alongside the version bump.

Notarization note

Sparkle will work without notarization, but the freshly downloaded .app may be quarantined by Gatekeeper before Sparkle relaunches it. Sparkle strips the quarantine attribute as part of its install step — this should be verified on a clean machine after implementation.

If quarantine issues surface, the right long-term fix is Apple notarization, not a workaround.


Files touched

File Change
project.yml Add Sparkle SPM package + target dependency
Tusk/Resources/Info.plist Add SUFeedURL, SUPublicEDKey, SUEnableAutomaticChecks
Tusk/TuskApp.swift Instantiate SPTUpdaterController
Tusk/Views/About/AboutView.swift Replace custom update logic with updater.checkForUpdates()
docs/appcast.xml New file — Sparkle feed
.claude/skills/release Add sign_update step + appcast commit

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions