Mac apps with Sparkle
For a Mac app, the project builds the app and signs it ad hoc. The hub does the rest:
- it checks the app before anything is signed: both version keys must equal the tag;
- it checks that the bundle id, the feed address, the Sparkle key and every nested bundle are what
tapster.ymlsays; - it signs every piece of code inside out with identifiers and entitlements it chooses itself;
- it builds the DMG without Finder, then signs, notarizes and staples it;
- it signs the DMG and the update feed with the app’s Sparkle key;
- it publishes the DMG, the feed and
checksums.txtand writes the cask.
In Setup → Optional extras → Sparkle keys, Create a Sparkle key generates a key in your local keychain. Use a fresh label and copy the public key into the app and its entry under Projects → Configuration. Read that keychain account in the Sparkle section to make an offline backup and store the key in the hub. Existing key labels remain available in Read or store a key, including keys already stored in the hub.
What the app needs
Section titled “What the app needs”In the app’s Info.plist:
| Key | Value |
|---|---|
SUFeedURL |
https://github.com/you/MyApp/releases/latest/download/appcast.xml |
SUPublicEDKey |
the public half of the app’s Sparkle key |
SURequireSignedFeed |
true |
SUVerifyUpdateBeforeExtraction |
true |
CFBundleShortVersionString and CFBundleVersion |
the tag’s version, such as 1.2.3 for v1.2.3 |
LSMinimumSystemVersion |
macOS 11, 12, 13, 14, 15 or 26 |
The app must embed Sparkle 2.9.0 or newer. Apps are released from tags like v1.2.3. Prereleases are not published,
because the feed would never offer them.
1. The Sparkle key
Section titled “1. The Sparkle key”Every app signs its updates with a Sparkle key. tapster calls each key by a label: the project’s name in lowercase
letters and digits, such as myapp for my-app.
A new app gets a new key. In a clone of your hub:
bunx @doguyilmaz/tapster init --sparkle myapp --newPut that public key into the app’s SUPublicEDKey now, before step 2. tapster then asks you to confirm an offline copy
of the key and stores it in the hub.
An app that already ships with a key keeps it. You store it after step 2.
Both commands change one line in the hub’s workflow. Commit and push it.
2. Add the app
Section titled “2. Add the app”In a clone of the app:
bunx @doguyilmaz/tapster addtapster finds the app through its SUFeedURL, in an XcodeGen project.yml or an Info.plist. It writes
.github/workflows/release.yml. The build runs make app when your Makefile has an app target. Otherwise
tapster says so. Then set the build command and the app’s path in that file yourself. The build must sign the app ad hoc.
The hub entry it proposes looks like this:
- name: myapp repo: you/MyApp kind: app app: MyApp.app bundle_id: com.you.MyApp assets: darwin-arm64: "MyApp-{version}.app.tar.gz" dmg: { name: "MyApp-{version}.dmg" } sparkle: { key: myapp, public_key: "<SUPublicEDKey>" }If tapster could not find the bundle id or the public key, the entry holds a placeholder and is printed instead of
proposed. Fill it in and add it to the hub’s tapster.yml yourself. Then follow Commit, merge and install.
For an app that already ships with a key, store that key once the entry is merged and before the first tag. In tapster setup, open Optional extras: under Sparkle keys, click Read it from my keychain, paste the key it shows into the form below it and click Store in the hub. Your password manager offers to save the key at that point, which is the offline copy you need. tapster also passes the key to the hub workflow for you.
Or, in a clone of your hub:
git pullbunx @doguyilmaz/tapster init --sparkle myapp --account ed25519ed25519 is the account Sparkle’s generate_keys uses unless you named another. macOS asks whether tapster may read
the key; choose Allow. tapster checks that the key’s public half matches sparkle.public_key in tapster.yml, then
asks Do you keep an offline backup of this key? Type yes to store it in the hub:. Commit and push the workflow line it
changes.
Nested code and entitlements
Section titled “Nested code and entitlements”The hub chooses the identifier and the entitlements of every piece of code it signs. For helpers, login items or
frameworks that need their own, list them under code:
code: Contents/Library/LoginItems/MyHelper.app: identifier: com.you.MyApp.Helper entitlements: { com.apple.security.app-sandbox: true }Files that must stay in every release unchanged, such as a frozen feed for old builds, go under carry with their
digest. See tapster.yml for every key.
Rotate a Sparkle key
Section titled “Rotate a Sparkle key”A leaked or retired key is replaced over two releases, so installed copies follow along:
- Make a key with a new label:
tapster init --sparkle myapp2 --new. - In the app’s entry, set
next_key: myapp2andnext_public_keyto its public key. Put that public key into the app’sSUPublicEDKey. - Release. The hub signs the DMG with the new key and the feed with the old one. Sparkle accepts the change because the DMG carries your Developer ID.
- Make the new key the current one: set
keyandpublic_keyto the new values and removenext_keyandnext_public_key. From the next release on, the hub signs the feed with the new key too. - Run
tapster init --workflowso the workflow stops passing the old key, then delete its secret:gh secret delete SPARKLE_KEY_MYAPP --env tapster.
Copies that skip the release in step 3 recover after 20 days, when Sparkle falls back to your Developer ID. Keep the old key’s offline copy until then.
