In this blog post we will see how we can fully automate macOS app updates with Sparkle 2 + GitHub Actions + GitHub Pages. We will build, notorize, sign, auto-generate appcast.xml and DMG files, etc. using GitHub Actions whenever you create a new release (this can be changed to on commit too).
Highlights:
- Build your app on macOS runners
- Re-sign Sparkle components properly
- Notarize your app and DMG
- Sign the DMG with Sparkle’s Ed25519 key
- Generate an
appcast.xml
from the GitHub Release notes - Publish the DMG and appcast to a public repo served via GitHub Pages
The guide uses a working workflow as reference; see the exact file here that I use for my Presentify app: sparkle-publish.yml gist. It generalizes the steps so you can adapt them for your own app.
What you’ll need
- A macOS app already set up with Sparkle 2 (framework embedded) and the public update key in
Info.plist
. - A Developer ID Application certificate (installed locally in Keychain) to sign your app for distribution outside the Mac App Store.
- An Apple ID with two-factor auth and an App-Specific Password.
- A GitHub repo for your app (private or public) where the workflow runs.
- A second public GitHub repo (e.g.,
yourname/myapp-updates
) where the DMG +appcast.xml
will be published via GitHub Pages.
Step 1 — Create a public “updates” repo and enable GitHub Pages
1) Create a new public repo, e.g., yourname/myapp-updates
.
2) Go to Settings → Pages → Build and deployment:
- Source: Deploy from a branch
- Branch:
main
(or your default), folder:/
(root) - Save 3) Pages URL will look like:
https://<your-username>.github.io/<updates-repo>/
- Your future appcast URL will be:
https://<your-username>.github.io/<updates-repo>/appcast.xml
Optional: If you use a custom domain, add your CNAME file and DNS before continuing.
Step 2 — Create a GitHub Deploy Key pair for publishing
You’ll push from the app’s repo (CI) into the public “updates” repo via SSH using a deploy key.
Generate a dedicated key pair on your machine (no passphrase):
ssh-keygen -t ed25519 -C "gh-actions deploy key for <updates-repo>" -N "" -f github_deploy_key
# Produces github_deploy_key (private) and github_deploy_key.pub (public)
Wire it up:
- In the UPDATES repo (public): Settings → Deploy keys → Add deploy key
- Title: “GitHub Actions (from
)" - Key: contents of
github_deploy_key.pub
- Enable “Allow write access”
- Title: “GitHub Actions (from
- In the APP repo (where the workflow runs): Settings → Secrets and variables → Actions → New repository secret
- Name:
DEPLOY_KEY
- Value: full text of
github_deploy_key
(the private key)
- Name:
Step 3 — Generate Sparkle Ed25519 keys and embed the public key in your app
Sparkle 2 uses Ed25519 for update signatures. You must embed the public key in the app, and keep the private key in CI.
Generate keys using Sparkle’s CLI (installed via Sparkle project or the workflow step installs the CLI for you):
# From Sparkle 2 source (or installed tools)
generate_keys
This prints a public key and a private key. Do the following:
- In your app’s
Info.plist
, set the public key underSUPublicEDKey
. - In the APP repo, add the private key as a secret:
- Name:
SPARKLE_PRIVATE_KEY
- Value: the exact private key string (don’t add newlines unless included)
- Name:
Your app must also have its feed URL set (either at runtime or in Info.plist
):
SUFeedURL
→https://<your-username>.github.io/<updates-repo>/appcast.xml
Step 4 — Export your Developer ID Application certificate as a .p12
1) Open Keychain Access → My Certificates → find “Developer ID Application:
# macOS
base64 -i DeveloperID.p12 | pbcopy
# (or)
base64 < DeveloperID.p12 > developer_id_base64.txt
Add these to the APP repo secrets:
DEV_ID_P12_BASE64
→ the base64 string of your.p12
DEV_ID_P12_PASSWORD
→ the password you set when exporting (leave empty if none)
Optional but helpful:
DEV_ID_SIGN_IDENTITY
→ exact identity string, e.g.,Developer ID Application: Your Name (TEAMID)
- If omitted, the workflow will auto-detect a Developer ID Application identity from the imported keychain.
Step 5 — Create an Apple App-Specific Password and note your Team ID
APPLE_ID
→ your Apple ID email (used for notarization)APPLE_APP_PASSWORD
→ create athttps://appleid.apple.com
→ Sign-In & Security → App-Specific Passwords → GenerateAPPLE_TEAM_ID
→ find athttps://developer.apple.com/account/
(also visible in many Apple developer emails and certificates)
Add all three as APP repo secrets.
Step 6 — Add the GitHub Actions workflow
Create .github/workflows/sparkle-publish.yml
in your APP repo with the contents similar to the working workflow: sparkle-publish.yml gist. Key things to adapt:
- Project and scheme names in the
xcodebuild
step - Entitlements path used to re-sign your app
- App name (used for DMG name and
create-dmg
overlay) - Appcast link and DMG download URL (should match your Pages site and updates repo)
Below is just a excerpt; see the full working file here that I use for my Presentify app: sparkle-publish.yml gist):
name: Sparkle Publish
on:
release:
types: [published]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install tools
run: brew install create-dmg jq
- name: Setup Sparkle CLI tools
uses: jozefizso/setup-sparkle@v1
with:
version: 2.7.1
# 1) Import Developer ID .p12 into a temporary keychain
- name: Import Developer ID certificate
env:
DEV_ID_P12_BASE64: $
DEV_ID_P12_PASSWORD: $
run: |
set -euo pipefail
KEYCHAIN=build.keychain
KEYCHAIN_PASSWORD=$(uuidgen)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" login.keychain
security default-keychain -s "$KEYCHAIN"
(echo "$DEV_ID_P12_BASE64" | base64 -D > dev_id.p12) || (echo "$DEV_ID_P12_BASE64" | base64 -d > dev_id.p12)
security import dev_id.p12 -k "$KEYCHAIN" -P "${DEV_ID_P12_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
# 2) Build app (adapt project, scheme, entitlements)
- name: Build App
run: |
xcodebuild -project YourApp.xcodeproj -scheme YourApp -configuration Release -derivedDataPath build \
CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="Developer ID Application" \
DEVELOPMENT_TEAM=$ \
OTHER_CODE_SIGN_FLAGS="--keychain build.keychain --timestamp --options runtime"
# 3) Re-sign Sparkle nested items and your app with entitlements
# 4) Verify, zip, notarize app; staple
# 5) Create DMG; notarize; staple
# 6) Sign DMG with Sparkle private key (SPARKLE_PRIVATE_KEY secret)
# 7) Generate appcast.xml using release notes
# 8) Push DMG + appcast.xml to public updates repo using DEPLOY_KEY
- name: Setup Deploy Key
run: |
mkdir -p ~/.ssh
echo "$" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Push to public updates repo
env:
GIT_SSH_COMMAND: 'ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no'
run: |
git clone [email protected]:<your-username>/<updates-repo>.git deploy
cp output/YourApp-*.dmg deploy/
cp appcast.xml deploy/
cd deploy
git config user.name "GitHub Actions"
git config user.email "[email protected]"
git add .
git commit -m "Release $"
git push origin main
Tip: In the working gist, the workflow also auto-extracts version/build from Info.plist
, uses the GitHub Release body as release notes, and sets the DMG enclosure
with the correct sparkle:edSignature
produced by sign_update
.
Step 7 — Add all required GitHub Secrets (checklist)
Set these in the APP repo (Settings → Secrets and variables → Actions):
DEV_ID_P12_BASE64
— Base64 of your exported Developer ID Application.p12
DEV_ID_P12_PASSWORD
— Password used while exporting the.p12
(blank if none)APPLE_ID
— Your Apple ID email (for notarization)APPLE_APP_PASSWORD
— App-specific password created atappleid.apple.com
APPLE_TEAM_ID
— Your 10‑char Apple Team IDSPARKLE_PRIVATE_KEY
— Ed25519 private key (matchingSUPublicEDKey
in app)DEV_ID_SIGN_IDENTITY
— Optional explicit signing identity stringDEPLOY_KEY
— Private SSH key used to push into the updates repo
Step 8 — Publish a release to trigger the workflow
1) Bump your app’s version and build in Info.plist
.
2) Tag a release from GitHub → Releases → Draft a new release:
- Tag version, e.g.,
v2.3.0
- Release title and description — this becomes your Sparkle release notes
- Publish release 3) The workflow runs automatically:
- Builds and signs your app, re-signs Sparkle nested items
- Notarizes both the app zip and the DMG
- Signs the DMG with Sparkle’s Ed25519 key
- Generates
appcast.xml
- Pushes
YourApp-<version>.dmg
andappcast.xml
to<updates-repo>
4) Verify: - DMG:
https://<your-username>.github.io/<updates-repo>/YourApp-<version>.dmg
- Appcast:
https://<your-username>.github.io/<updates-repo>/appcast.xml
Step 9 — Point your app to the feed
Ensure your app’s Info.plist
has:
SUPublicEDKey
→ the exact public key from Step 3SUFeedURL
→ the Pages URL toappcast.xml
When you run a previous version of your app, Sparkle should detect the update and apply it (assuming versions increase correctly and signatures match).
Troubleshooting
- Notarization fails with “Invalid” or “Rejected”:
- Inspect the notarization log printed by the workflow. Fix entitlements and re-signing of Sparkle XPCs; verify
codesign --verify --deep --strict
is clean before notarization.
- Inspect the notarization log printed by the workflow. Fix entitlements and re-signing of Sparkle XPCs; verify
- Sparkle update fails to install:
- Ensure Sparkle nested binaries and XPCs are re-signed with the same Developer ID identity and the app is re-signed last with entitlements and
--options runtime
. - Confirm
SUPublicEDKey
matches the private key used bysign_update
.
- Ensure Sparkle nested binaries and XPCs are re-signed with the same Developer ID identity and the app is re-signed last with entitlements and
- “Host key verification failed” when pushing to updates repo:
- The workflow sets
GIT_SSH_COMMAND
with-o StrictHostKeyChecking=no
. Make sureDEPLOY_KEY
is the private key matching the deploy key added with write access in the updates repo.
- The workflow sets
- DMG not found / wrong name:
- Keep the app name consistent across
create-dmg
, signing, and copy steps. Update file names if you rename the app.
- Keep the app name consistent across
Appendix — What this workflow does in detail
High-level sequence performed by the workflow provided in the working gist (sparkle-publish.yml):
1) Install dependencies (create-dmg
, jq
) and Sparkle CLI
2) Create a temporary keychain, import your Developer ID .p12
, and allow codesign access
3) Build the app with xcodebuild
while pointing codesign to the temporary keychain
4) Re-sign Sparkle framework components and XPCs, then re-sign the app with entitlements and timestamp
5) Verify signing (codesign --verify --deep --strict
) and dump entitlements
6) Zip app for notarization, submit with notarytool --wait
, then staple
7) Create DMG, notarize DMG with notarytool --wait
, then staple
8) Sign DMG with Sparkle’s sign_update
using your SPARKLE_PRIVATE_KEY
9) Extract version, build, size, signature; render appcast.xml
using the GitHub Release body as notes
10) Push DMG and appcast.xml
to the public updates repo using DEPLOY_KEY
You can copy this approach as-is and just replace names, paths, and URLs for your app.