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
- User clicks Check for Updates (or the app checks automatically on launch, optionally).
- If an update is available, a native update sheet/window shows the release notes.
- 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
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
AboutViewhits 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
Implementation plan
1. Add Sparkle 2 via SPM
In
project.yml:Add
Sparkleas a dependency of theTusktarget. Regenerate withxcodegen generate.2. Generate & store the EdDSA signing key
Info.plistasSUPublicEDKey.3. Publish an appcast
Create
docs/appcast.xml(served via GitHub Pages athttps://shape-machine.github.io/tusk-macos/appcast.xml) or host it as a raw file in the repo.Minimum appcast structure:
4. Sign each release DMG
Add to the
/releaseskill after the DMG is created:Paste the output
sparkle:edSignatureandlengthinto the appcast item.5. Wire Sparkle into the app
Info.plistadditions:(
SUEnableAutomaticChecksoff keeps the current manual-only philosophy. Can be revisited.)TuskApp.swift— hold a reference to the updater:AboutView.swift— replace the customcheckForUpdates()with Sparkle's:The
.available(version)state,GitHubReleasestruct, andisNewer()helper can all be deleted — Sparkle handles this entirely.6. Update the release skill
Automate appcast generation in
.claude/skills/release:sign_updateand capture the signature + byte length.docs/appcast.xmlwith the new item (prepend; keep previous items for delta support).docs/appcast.xmlalongside the version bump.Notarization note
Sparkle will work without notarization, but the freshly downloaded
.appmay 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
project.ymlTusk/Resources/Info.plistSUFeedURL,SUPublicEDKey,SUEnableAutomaticChecksTusk/TuskApp.swiftSPTUpdaterControllerTusk/Views/About/AboutView.swiftupdater.checkForUpdates()docs/appcast.xml.claude/skills/releasesign_updatestep + appcast commitReferences