Loading...

Automatically generate appcast.xml and DMG files for your Mac app updates

swift macapps sparkle
Ram Patra Published on September 5, 2025
Image placeholder

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”
  • 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)

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 under SUPublicEDKey.
  • 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)

Your app must also have its feed URL set (either at runtime or in Info.plist):

  • SUFeedURLhttps://<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: ()". 2) Right-click → Export → format `.p12` → choose a password (optional, can be empty). 3) Base64-encode the `.p12` so it’s safe to store in GitHub Secrets:

# 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 at https://appleid.apple.com → Sign-In & Security → App-Specific Passwords → Generate
  • APPLE_TEAM_ID → find at https://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 at appleid.apple.com
  • APPLE_TEAM_ID — Your 10‑char Apple Team ID
  • SPARKLE_PRIVATE_KEY — Ed25519 private key (matching SUPublicEDKey in app)
  • DEV_ID_SIGN_IDENTITY — Optional explicit signing identity string
  • DEPLOY_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 and appcast.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 3
  • SUFeedURL → the Pages URL to appcast.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.
  • 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 by sign_update.
  • “Host key verification failed” when pushing to updates repo:
    • The workflow sets GIT_SSH_COMMAND with -o StrictHostKeyChecking=no. Make sure DEPLOY_KEY is the private key matching the deploy key added with write access in the updates repo.
  • 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.

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.

Take your presentation to the next level.

Put your face and name on your screen.

Your to-dos on your menu bar.

Fill forms using your right-click menu.

Ram Patra Published on September 5, 2025
Image placeholder

Keep reading

If you liked this article, you may like these as well

August 20, 2019 Nested Classes in Java

A class inside another class is called a Nested Class. In other words, as a class has member variables and member methods it can also have member classes.

referral discount tesla September 19, 2025 Tesla Model 3 Discount

Tesla cars are hard to beat when it comes to looks, efficiency, performance, software, and even interior styling. Well, okay—maybe I went a bit too far with the interior styling. But jokes aside, I still think they offer the best bang for your buck compared to the competition.

May 22, 2019 Access Control in Java

Modifiers fall into two categories:

Like my work?

Please, feel free to reach out. I would be more than happy to chat.