$npx -y skills add fayazara/macos-app-skills --skill releaseRelease a native macOS app to GitHub with DMG packaging and Sparkle appcast updates. Use this skill whenever the user wants to publish a new version, create a release, ship an update, push a release to GitHub, or update the appcast. Also trigger when the user mentions DMG creatio
| 1 | # Release macOS App |
| 2 | |
| 3 | This skill covers the full release pipeline for distributing a native macOS app outside the Mac App Store via GitHub Releases with Sparkle auto-update support. |
| 4 | |
| 5 | ## Release Pipeline Overview |
| 6 | |
| 7 | ``` |
| 8 | Bump version → Archive → Notarize → Export → Create DMG → Sign DMG → Update appcast.xml → Git push → GitHub Release |
| 9 | ``` |
| 10 | |
| 11 | ## Prerequisites |
| 12 | |
| 13 | The user needs these tools installed: |
| 14 | |
| 15 | | Tool | Install | Purpose | |
| 16 | |------|---------|---------| |
| 17 | | `create-dmg` | `brew install create-dmg` | Creates the DMG installer | |
| 18 | | `gh` | `brew install gh` | Creates GitHub releases | |
| 19 | | `git` | Built-in | Pushes appcast changes | |
| 20 | | Sparkle `sign_update` | Built automatically when the project is built with Sparkle | EdDSA-signs the DMG | |
| 21 | |
| 22 | The `sign_update` binary lives in DerivedData after building the project in Xcode: |
| 23 | |
| 24 | ```bash |
| 25 | find ~/Library/Developer/Xcode/DerivedData -name "sign_update" -type f 2>/dev/null | head -1 |
| 26 | ``` |
| 27 | |
| 28 | ## Step-by-Step Release Guide |
| 29 | |
| 30 | ### 1. Bump Version Numbers |
| 31 | |
| 32 | In Xcode, update: |
| 33 | - `MARKETING_VERSION` (e.g., `1.2`) -- the user-facing version |
| 34 | - `CURRENT_PROJECT_VERSION` (e.g., `5`) -- the build number (must be unique per release) |
| 35 | |
| 36 | Or via command line: |
| 37 | |
| 38 | ```bash |
| 39 | # Check current values |
| 40 | grep -E "MARKETING_VERSION|CURRENT_PROJECT_VERSION" YourApp.xcodeproj/project.pbxproj | head -4 |
| 41 | ``` |
| 42 | |
| 43 | ### 2. Archive in Xcode |
| 44 | |
| 45 | Product > Archive. This creates a release build with the proper signing identity. |
| 46 | |
| 47 | ### 3. Notarize and Export |
| 48 | |
| 49 | In the Archives organizer: |
| 50 | 1. Select the archive > Distribute App |
| 51 | 2. Choose "Direct Distribution" (or "Developer ID" for notarization) |
| 52 | 3. Wait for notarization to complete |
| 53 | 4. Export to `~/Downloads/YourApp.app` |
| 54 | |
| 55 | ### 4. Create DMG |
| 56 | |
| 57 | ```bash |
| 58 | create-dmg \ |
| 59 | --volname "YourApp" \ |
| 60 | --window-pos 200 120 \ |
| 61 | --window-size 660 400 \ |
| 62 | --icon-size 160 \ |
| 63 | --icon "YourApp.app" 180 170 \ |
| 64 | --app-drop-link 480 170 \ |
| 65 | --hide-extension "YourApp.app" \ |
| 66 | ~/Downloads/YourApp.dmg \ |
| 67 | ~/Downloads/YourApp.app |
| 68 | ``` |
| 69 | |
| 70 | ### 5. Sign DMG with Sparkle |
| 71 | |
| 72 | ```bash |
| 73 | /path/to/sign_update ~/Downloads/YourApp.dmg |
| 74 | ``` |
| 75 | |
| 76 | This outputs the EdDSA signature and file length: |
| 77 | |
| 78 | ``` |
| 79 | sparkle:edSignature="BASE64..." length="12345" |
| 80 | ``` |
| 81 | |
| 82 | Save both values for the appcast. |
| 83 | |
| 84 | ### 6. Update appcast.xml |
| 85 | |
| 86 | Add a new `<item>` at the top of the `<channel>` in your `appcast.xml`: |
| 87 | |
| 88 | ```xml |
| 89 | <item> |
| 90 | <title>Version 1.2 (Build 5)</title> |
| 91 | <pubDate>Mon, 26 May 2026 12:00:00 +0000</pubDate> |
| 92 | <sparkle:version>5</sparkle:version> |
| 93 | <sparkle:shortVersionString>1.2</sparkle:shortVersionString> |
| 94 | <sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion> |
| 95 | <description><![CDATA[<ul><li>Feature one</li><li>Bug fix two</li></ul>]]></description> |
| 96 | <enclosure url="https://github.com/OWNER/REPO/releases/download/v1.2/YourApp.dmg" |
| 97 | type="application/octet-stream" |
| 98 | sparkle:edSignature="THE_SIGNATURE_FROM_STEP_5" |
| 99 | length="THE_LENGTH_FROM_STEP_5" /> |
| 100 | </item> |
| 101 | ``` |
| 102 | |
| 103 | The `pubDate` should be RFC 2822 format. Generate it: |
| 104 | |
| 105 | ```bash |
| 106 | date -R |
| 107 | ``` |
| 108 | |
| 109 | ### 7. Commit and Push Appcast |
| 110 | |
| 111 | ```bash |
| 112 | git add appcast.xml |
| 113 | git commit -m "Release v1.2 appcast" |
| 114 | git push origin main |
| 115 | ``` |
| 116 | |
| 117 | ### 8. Create GitHub Release |
| 118 | |
| 119 | ```bash |
| 120 | gh release create v1.2 \ |
| 121 | ~/Downloads/YourApp.dmg \ |
| 122 | --title "v1.2" \ |
| 123 | --notes "- Feature one |
| 124 | - Bug fix two" |
| 125 | ``` |
| 126 | |
| 127 | ## Automating the Pipeline |
| 128 | |
| 129 | For frequent releases, build a CLI tool that automates steps 4-8. See `references/release-pipeline.md` for a template Go CLI that handles DMG creation, signing, appcast updates, and GitHub release creation in one command. |
| 130 | |
| 131 | The CLI should: |
| 132 | - Find `sign_update` in DerivedData automatically |
| 133 | - Read version/build from the exported app's Info.plist via `plutil` |
| 134 | - Parse and update appcast.xml (preserving existing entries) |
| 135 | - Interactively collect release notes |
| 136 | - Show a summary and ask for confirmation before proceeding |
| 137 | - Create the GitHub release with the DMG attached |
| 138 | |
| 139 | ## Release CLI Tool |
| 140 | |
| 141 | This repo includes a Go CLI that automates steps 4-8 (DMG creation through GitHub release) in a single command. It reads configuration from a `release.json` file in your project root. |
| 142 | |
| 143 | ### Setup |
| 144 | |
| 145 | 1. Create a `release.json` in your project root (see `cli/release.example.json`): |
| 146 | |
| 147 | ```json |
| 148 | { |
| 149 | "app_name": "MyApp", |
| 150 | "github_repo": "owner/myapp" |
| 151 | } |
| 152 | ``` |
| 153 | |
| 154 | Only `app_name` and `github_repo` are required. Everything else has sensible defaults: |
| 155 | |
| 156 | | Field | Default | Description | |
| 157 | |-------|---------|-------------| |
| 158 | | `bundle_nam |