Short, practical guide to signing + releasing updates.
Two separate systems:
-
Apple signing (codesign + notarization) → lets macOS run your app
-
Sparkle signing (Ed25519) → lets your app trust updates
Do not mix them.
- Feed: https://updates.cotabby.app/appcast.xml
- Public key (
SUPublicEDKey):efJeZNfUISOs6npbxI2MLLe7sBB5tT/sVnTk9t/qBSY=
Private key = secret. Never commit it.
mkdir -p ~/bin
ln -sf "$(find ~/Library/Developer/Xcode/DerivedData -name generate_keys -type f | head -n 1)" ~/bin/sparkle-generate-keys
ln -sf "$(find ~/Library/Developer/Xcode/DerivedData -name sign_update -type f | head -n 1)" ~/bin/sparkle-sign-update
ln -sf "$(find ~/Library/Developer/Xcode/DerivedData -name generate_appcast -type f | head -n 1)" ~/bin/sparkle-generate-appcast
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
source ~/.zshrcCheck:
which sparkle-generate-keyssparkle-generate-keyssparkle-generate-keys -pMust match SUPublicEDKey.
sparkle-generate-keys -x ~/secure/Cotabby-key.txtsparkle-generate-keys -f ~/secure/Cotabby-key.txtcodesign --force --deep --options runtime \
--sign "Developer ID Application: Jacob Fu (G946M8K23B)" \
./Cotabby.appditto -c -k --keepParent ./Cotabby.app Cotabby.zip
xcrun notarytool submit Cotabby.zip --keychain-profile "AC_PASSWORD" --wait
xcrun stapler staple ./Cotabby.appThe release pipeline now builds a styled installer DMG through:
python3 -m pip install "dmgbuild[badge_icons]==1.6.7"
python3 scripts/build_release_dmg.py \
--app-path /path/to/Cotabby.app \
--output-path /path/to/Cotabby.dmg \
--background-path assets/release/dmg_background.png \
--background-2x-path assets/release/dmg_background@2x.png \
--volume-name CotabbyWhat this does:
- packages
Cotabby.appwith anApplicationsshortcut - applies the committed background art from
assets/release/dmg_background.png - locks the icon layout for the drag-to-Applications flow
- reuses the app bundle icon as a best-effort mounted-volume badge when available
sparkle-sign-update /path/to/Cotabby.dmgpython3 scripts/generate_appcast.py \
--release-version 1.0.0 \
--build-number 100 \
--archive /path/to/Cotabby.dmg \
--output build/appcast.xml \
--ed-key-file ~/secure/Cotabby-key.txtOn your Mac, --ed-key-file is optional if the key is already in Keychain.
In GitHub Actions, we pass the key file explicitly from the SPARKLE_ED25519_PRIVATE_KEY secret.
Workflow:
.github/workflows/release.yml
Trigger:
- Push a tag like
v0.0.2-betaorv1.0.0 - Or run manually with
workflow_dispatchfor validation
Tags with a hyphen suffix (e.g., v0.0.1-beta, v1.0.0-rc1) are automatically
marked as Pre-release on the GitHub Releases page. This means they won't
become the "Latest" release.
- Beta/RC release:
git tag v0.0.2-beta && git push origin v0.0.2-beta - Stable release:
git tag v1.0.0 && git push origin v1.0.0
To promote a pre-release to Latest without re-running the pipeline:
gh release edit v0.0.1-beta --prerelease=false --latestTo re-release the same tag on a newer commit (e.g., hotfix):
gh release delete v0.0.1-beta --yes # delete the GitHub Release
git push origin :refs/tags/v0.0.1-beta # delete remote tag
git tag -f v0.0.1-beta # re-tag at current HEAD
git push origin v0.0.1-beta # push triggers pipelineRequired repo secrets:
APPLE_IDAPPLE_TEAM_IDAPPLE_APP_SPECIFIC_PASSWORDDEVELOPER_ID_APPLICATION_CERTDEVELOPER_ID_CERT_PASSWORDSPARKLE_ED25519_PRIVATE_KEY
What CI does:
- Imports the Developer ID certificate into a temporary keychain.
- Installs the pinned
dmgbuild[badge_icons]dependency. - Archives a Release build.
- Packages a styled
Cotabby.dmgwithscripts/build_release_dmg.py. - Sends the DMG to Apple notarization.
- Staples and validates the notarization ticket.
- Verifies the Sparkle private key matches
SUPublicEDKey. - Signs the final DMG with Sparkle.
- Creates a GitHub Release with
Cotabby.dmg. - Publishes
appcast.xmlto GitHub Pages last.
Pages output:
/appcast.xml
The /appcast.xml path matches the current feed URL (https://updates.cotabby.app/appcast.xml).
Check Apple signing:
spctl -a -t exec -vv ./Cotabby.appCheck Sparkle signature:
sparkle-sign-update /path/to/Cotabby.dmgSignature must match appcast.
Check installer layout locally:
hdiutil attach /path/to/Cotabby.dmgVerify the mounted image opens in icon view, shows the committed background
art, places Cotabby.app above the arrows, and places the Applications shortcut
inside the dashed drop target. The mounted volume badge is best-effort; do not
fail a release if the window layout is correct but Finder falls back to the
default disk icon.
- Never lose Sparkle private key → breaks updates
- Never rotate key casually → old installs will reject updates
- Never commit private key
- Always sign AFTER final DMG is built (no changes after)
- Always publish appcast AFTER the GitHub Release asset exists
Sparkle follows the appcast, not the GitHub Releases page.
To rollback:
- Find the previous successful release run.
- Restore that run's
appcast.xml. - Redeploy it to GitHub Pages.
- Leave the bad GitHub Release alone unless there is a security reason to remove it.
Common issues:
- Wrong Sparkle key → updates rejected
- DMG changed after signing → signature invalid
- Missing notarization → macOS blocks app
Fix those first.