{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-05-code-signing-and-notarization-sparkle-and-tears/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "![](https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/header.png)\n\n*Or: How I Learned to Stop Worrying and Love the XPC Services*\n\nIf you’ve ever tried to implement automatic updates in a sandboxed macOS app using Sparkle, you know it can feel like trying to solve a Rubik’s cube while wearing oven mitts. After creating way too many beta releases and spending countless hours debugging cryptic authorization errors, I finally cracked the code. Here’s my step-by-step guide to code signing, notarization, and Sparkle auto-updates in sandboxed macOS apps.\n\n## The Setup: Vibe Meter Meets Sparkle\n\n[Vibe Meter](https://vibemeter.ai) is a sandboxed macOS menu bar app that tracks AI service spending. When I decided to add automatic updates using Sparkle 2.x, I thought it would be straightforward. After all, Sparkle is the de facto standard for macOS app updates, right?\n\nOh, sweet summer child.\n\n## Act 1: The Mysterious Authorization Failure\n\nI’d already mastered code signing and notarization (its own nightmare), so I just told Claude: “Look at this repository and do what we did there.” It nailed the signing and notarization process.\n\nMy first attempts seemed promising. The app built, signed, and notarized successfully. But when users tried to update, they were greeted with:\n\n```plaintext\nError: Failed to gain authorization required to update target\n```\n\nThis error is Sparkle’s polite way of saying “I can’t talk to my XPC services, and I have no idea why.”\n\n## Act 2: The Entitlements Enigma\n\nAfter digging through [Sparkle’s documentation](https://sparkle-project.org/documentation/sandboxing/) and Console logs, I discovered my first issue: missing mach-lookup entitlements. In a sandboxed app, Sparkle uses XPC services to perform privileged operations, and these services need special permissions to communicate.\n\n### The Missing Piece\n\nMy entitlements file was missing a critical entry:\n\n```xml\n<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>\n<array>\n    <string>com.steipete.vibemeter-spks</string>\n    <string>com.steipete.vibemeter-spki</string>\n</array>\n```\n\nBut here’s the kicker - I initially only added `-spks`, thinking it stood for “Sparkle Server.” Turns out, you need BOTH:\n\n- `-spks`: Sparkle Server (for the InstallerLauncher.xpc service)\n- `-spki`: Sparkle Installer (for the Installer.xpc service)\n\nMissing either one results in the dreaded authorization error.\n\n## Act 3: The Code Signing Circus\n\nNext came the code signing adventures. My notarization script was doing what seemed logical:\n\n```bash\ncodesign --deep --force --sign \"Developer ID\" VibeMeter.app\n```\n\nBut Sparkle’s documentation specifically warns against using `--deep`. Why? Because it can mess up the XPC services’ signatures. Instead, you need to sign components in a specific order.\n\nHere’s the correct approach from my [codesign script](https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh):\n\n```bash\n# Sign XPC services first\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc\"\n\n# Preserve entitlements for Downloader.xpc (Sparkle 2.6+)\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime --preserve-metadata=entitlements \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc\"\n\n# Sign other Sparkle components\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate\"\n\n# Then sign the framework\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework\"\n\n# Finally, sign the app WITHOUT --deep\ncodesign --force --sign \"$SIGN_IDENTITY\" --entitlements VibeMeter.entitlements \\\n    --options runtime VibeMeter.app\n```\n\n## Act 4: The Bundle ID Bamboozle\n\nAt one point, I thought I was being clever (or rather, [Claude Code](https://steipete.me/posts/claude-code-is-my-computer/) thought…) by trying to manually modify the XPC services’ bundle identifiers. Big mistake.\n\nFor sandboxed apps, Sparkle requires specific bundle ID suffixes:\n\n- Use `-spks` suffix for the InstallerLauncher service\n- Use `-spki` suffix for the Installer service\n\nThe framework expects these exact suffixes. Use anything else, and you’ll get cryptic XPC connection errors that will make you question your career choices.\n\n## Act 5: The Build Number Blues\n\nEven after fixing all the sandboxing issues, I hit another snag. Users were seeing “You’re up to date!” when updates were clearly available. The culprit? My appcast[^1] generation script was defaulting build numbers to “1”.\n\nSparkle uses build numbers (CFBundleVersion), not version strings, to determine if an update is available. If your build numbers don’t increment, Sparkle thinks there’s nothing new.\n\nMy [appcast generation script](https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh) now properly handles this:\n\n```bash\n# Extract build number from Info.plist\nBUILD_NUMBER=$(plutil -extract CFBundleVersion raw \"$INFO_PLIST\")\n\n# Ensure build numbers increment correctly\nif [[ \"$BUILD_NUMBER\" -le \"$PREVIOUS_BUILD_NUMBER\" ]]; then\n    echo \"Error: Build number must be greater than previous release\"\n    exit 1\nfi\n```\n\n## The Grand Finale: It Works!\n\n![Sparkle update dialog working perfectly](https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/sparkle.png)\n\nAfter two days of intense debugging, I finally had a working setup. My complete automation pipeline is now rock-solid, with [comprehensive scripts](https://github.com/steipete/VibeMeter/tree/main/scripts) that handle every aspect of the process.\n\n### The Magic Recipe\n\n1. **Entitlements**: Include BOTH `-spks` and `-spki` mach-lookup exceptions\n2. **Bundle IDs**: Never change Sparkle’s XPC service bundle IDs\n3. **Code Signing**: Sign XPC services individually, never use `--deep`\n4. **Build Numbers**: Always increment them, and verify your appcast\n5. **Info.plist**: Set `SUEnableInstallerLauncherService = true` and `SUEnableDownloaderService = false`[^2]\n\n### The Working Configuration\n\nThe XPC services themselves keep their original Sparkle bundle IDs (`org.sparkle-project.InstallerLauncher` and `org.sparkle-project.Downloader`). What we configure in the entitlements are the mach-lookup communication channels:\n\n```xml\n<!-- VibeMeter.entitlements -->\n<key>com.apple.security.app-sandbox</key>\n<true/>\n<key>com.apple.security.network.client</key>\n<true/>\n<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>\n<array>\n    <string>com.steipete.vibemeter-spks</string>\n    <string>com.steipete.vibemeter-spki</string>\n</array>\n```\n\n## The Scripts That Saved My Sanity\n\nMy complete build pipeline consists of several specialized scripts:\n\n- **[preflight-check.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/preflight-check.sh)**: Validates environment and prerequisites\n- **[codesign-app.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh)**: Handles the complex code signing process\n- **[notarize-app.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh)**: Manages notarization and stapling\n- **[create-dmg.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/create-dmg.sh)**: Creates and signs distribution DMGs\n- **[generate-appcast.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh)**: Generates Sparkle appcast files\n- **[release.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh)**: Orchestrates the entire release process\n\nLook at this beauty! Now even Claude can do releases without messing up 🎉 I just tell it “Create a new beta release, see release.md” and it handles everything.\n\n```bash\n# Now create the release\n./scripts/release.sh stable        # For stable 1.1.0\n# OR\n./scripts/release.sh beta 1        # For 1.1.0-beta.1\n\n# Use the version script to bump versions\n./scripts/version.sh --minor       # 1.0.0 -> 1.1.0\n./scripts/version.sh --major       # 1.0.0 -> 2.0.0\n./scripts/version.sh --set 1.1.0   # Set specific version\n```\n\nEverything happens automatically - from building to GitHub release creation. These scripts are tuned for [Tuist](https://tuist.dev/) and can easily be adapted for simpler setups.\n\n## The Notarization Nightmare\n\nApple’s notarization process adds another layer of complexity. Notarization verifies your app is safe - you upload it to Apple’s servers for scanning, and if approved, they “staple” a ticket to your app. The process typically takes a few minutes in Apple’s notarization queue (officially called the “processing queue”). The [notarization script](https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh) uses the modern [`notarytool`](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) approach:\n\n```bash\n# Create ZIP for notarization (not DMG!)\nditto -c -k --keepParent \"$APP_BUNDLE\" \"$ZIP_PATH\"\n\n# Submit for notarization\nxcrun notarytool submit \"$ZIP_PATH\" \\\n    --key \"$API_KEY_PATH\" \\\n    --key-id \"$API_KEY_ID\" \\\n    --issuer \"$ISSUER_ID\" \\\n    --wait\n\n# Staple the ticket\nxcrun stapler staple \"$APP_BUNDLE\"\n\n# Verify everything worked\nspctl -a -t exec -vv \"$APP_BUNDLE\"\nxcrun stapler validate \"$APP_BUNDLE\"\n```\n\nOf course this needs [certificates from Apple](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution). Luckily Claude is smart enough to guide you through the whole process of downloading those files and adding them to the keychain and your environment.\n\n## Pretty Changelogs in Sparkle\n\nOne challenge I hadn’t anticipated was making the update dialogs actually useful. Sparkle can display rich HTML changelogs, but getting from my Markdown changelog to properly formatted HTML required some creativity.\n\nThe [changelog-to-html.sh script](https://github.com/steipete/VibeMeter/blob/main/scripts/changelog-to-html.sh) extracts version-specific sections from `CHANGELOG.md` and converts them to HTML with what I call the “Poor Man’s Markdown Parser”:\n\n```bash\n# Extract version section and convert Markdown to HTML\nawk \"/^## \\\\[$version\\\\]/{flag=1;next}/^## \\\\[/{flag=0}flag\" CHANGELOG.md | \\\nsed 's/^### \\(.*\\)/<h3>\\1<\\/h3>/' | \\\nsed 's/^- \\(.*\\)/<li>\\1<\\/li>/' | \\\nsed 's/\\*\\*\\([^*]*\\)\\*\\*/<strong>\\1<\\/strong>/g'\n```\n\nThe result? Users see properly formatted changelogs with headers, lists, and styled text instead of raw Markdown. Note: This parser has limitations - it doesn’t handle nested lists, images, or links. Keep your changelogs simple or extend the parser for more complex formatting.\n\n## The Complete Architecture\n\nWhat emerged is a surprisingly elegant zero-infrastructure solution that leverages GitHub’s existing services:\n\n### GitHub-Centric Distribution\n\n- **Releases**: [GitHub releases](https://github.com/steipete/VibeMeter/releases) host the actual DMG files\n- **Appcast Hosting**: [Raw GitHub URLs](https://raw.githubusercontent.com/steipete/VibeMeter/main/appcast.xml) serve the XML feeds\n- **Dual Feeds**: Separate appcasts for [stable](https://github.com/steipete/VibeMeter/blob/main/appcast.xml) and [pre-release](https://github.com/steipete/VibeMeter/blob/main/appcast-prerelease.xml) channels\n- **Version Control**: Appcast files are versioned alongside the code\n\n### Dynamic Channel Switching\n\nThe app includes runtime logic to switch between update channels without reinstallation. Users can choose “stable” for production releases or “pre-release” for beta access, and the app dynamically points to the appropriate appcast URL.\n\n![Vibe Meter settings showing update channel options](https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/vibemeter-settings.png)\n\n### Automated Everything\n\nThe [release.sh script](https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh) orchestrates the entire pipeline:\n\n1. Build and sign the app\n2. Create GitHub release with DMG\n3. Generate both appcast files with proper signatures\n4. Commit and push everything\n\nNo separate hosting, no Jekyll setup, no additional infrastructure - just GitHub doing what it does best. The key is that [`release.sh`](https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh) is the master orchestrator that calls most other scripts in sequence for a complete automated release.\n\nView the complete script flow architecture\n\n```plaintext\n🚀 Main Release Flow (release.sh)\n\nrelease.sh\n├── 1. preflight-check.sh ────────────── Validates everything ready\n├── 2. generate-xcproj.sh ────────────── Generates Xcode project\n├── 3. build.sh ──────────────────────── Builds app with IS_PRERELEASE_BUILD flag\n├── 4. sign-and-notarize.sh ──────────── Signs & notarizes app\n│   ├── codesign-app.sh ─────────────── Code signs app bundle\n│   └── notarize-app.sh ─────────────── Notarizes signed app\n├── 5. create-dmg.sh ─────────────────── Creates & signs DMG\n├── 6. GitHub CLI (gh) ───────────────── Creates GitHub release\n├── 7. generate-appcast.sh ───────────── Updates appcast XML files\n└── 8. verify-appcast.sh ─────────────── Validates appcast (optional)\n\n✅ Verification Flow\n\npreflight-check.sh ───────────────────── Pre-release validation\n└── verify-prerelease-system.sh ──────── IS_PRERELEASE_BUILD system check\n\nverify-app.sh ────────────────────────── Post-build verification\nverify-appcast.sh ────────────────────── Appcast validation\n\n🔐 Manual Signing Flow\n\nsign-and-notarize.sh\n├── codesign-app.sh ──────────────────── Code sign app\n├── notarize-app.sh ──────────────────── Notarize app\n└── create-dmg.sh ────────────────────── Create distribution DMG\n\n🛠️ Utility Scripts (Called by others)\n\n- changelog-to-html.sh ← Called by update-appcast.sh\n- version.sh ← Standalone version management\n```\n\n## Lessons Learned\n\n1. **Read the documentation carefully** - But also know that it might not cover every edge case\n2. **Console.app is your friend** - Filter by your process name and watch for XPC errors\n3. **Don’t be clever** - Follow Sparkle’s conventions exactly\n4. **Test updates, not just builds** - A successful build doesn’t mean updates will work\n5. **Automate everything** - Manual processes lead to human errors\n6. **Version control your scripts** - Build automation is as important as your app code\n\n## Final Thoughts\n\nImplementing Sparkle in a sandboxed app is like solving a puzzle where the pieces keep changing shape. But once you understand the rules - respect the XPC services, get your entitlements right, and sign everything properly - it works beautifully.\n\nThe irony? The final solution is actually quite simple. It’s getting there that’s the adventure. I don’t know how anyone manages to ship working macOS apps at all, honestly.\n\nNow go download [Vibe Meter](https://vibemeter.ai) and read my dev diary on how I built it with Claude Code: [Vibe Meter: Monitor Your AI Costs](https://steipete.me/posts/vibe-meter-monitor-your-ai-costs).\n\n## Resources\n\n- [Sparkle Sandboxing Documentation](https://sparkle-project.org/documentation/sandboxing/)\n- [Apple’s Code Signing Guide](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)\n- [VibeMeter Source Code](https://github.com/steipete/VibeMeter)\n- [VibeMeter Build Scripts](https://github.com/steipete/VibeMeter/tree/main/scripts)\n\n* * *\n\n*Special thanks to the Sparkle team and Claude Code - without them, this automation pipeline wouldn’t exist.*\n\n**P.S.** Just [steal my scripts](https://github.com/steipete/VibeMeter/tree/main/scripts). It’ll save you days of debugging. ☕️\n\n[^1]: An appcast is an RSS-like XML feed that Sparkle uses to check for updates. It contains information about available versions, download URLs, and release notes. See [Sparkle’s documentation on appcasts](https://sparkle-project.org/documentation/publishing/) for more details.\n\n[^2]: The Downloader service is only needed if your app doesn’t have network access. Since Vibe Meter includes the `com.apple.security.network.client` entitlement, we can disable SUEnableDownloaderService. This simplifies the setup and avoids the Downloader service’s limitations. See [Sparkle’s sandboxing documentation](https://sparkle-project.github.io/documentation/sandboxing/) for details.\n",
  "authors": [],
  "captured_at": "2026-09-27T19:29:15.293681927+00:00",
  "category": "programming",
  "content_html": "<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/header.png\" alt=\"\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><em>Or: How I Learned to Stop Worrying and Love the XPC Services</em></p>\n<p>If you’ve ever tried to implement automatic updates in a sandboxed macOS app using Sparkle, you know it can feel like trying to solve a Rubik’s cube while wearing oven mitts. After creating way too many beta releases and spending countless hours debugging cryptic authorization errors, I finally cracked the code. Here’s my step-by-step guide to code signing, notarization, and Sparkle auto-updates in sandboxed macOS apps.</p>\n<h2>The Setup: Vibe Meter Meets Sparkle</h2>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://vibemeter.ai\">Vibe Meter</a> is a sandboxed macOS menu bar app that tracks AI service spending. When I decided to add automatic updates using Sparkle 2.x, I thought it would be straightforward. After all, Sparkle is the de facto standard for macOS app updates, right?</p>\n<p>Oh, sweet summer child.</p>\n<h2>Act 1: The Mysterious Authorization Failure</h2>\n<p>I’d already mastered code signing and notarization (its own nightmare), so I just told Claude: “Look at this repository and do what we did there.” It nailed the signing and notarization process.</p>\n<p>My first attempts seemed promising. The app built, signed, and notarized successfully. But when users tried to update, they were greeted with:</p>\n<pre><code class=\"language-plaintext\"><span class=\"code-snippet\">Error: Failed to gain authorization required to update target\n</span></code></pre>\n<p>This error is Sparkle’s polite way of saying “I can’t talk to my XPC services, and I have no idea why.”</p>\n<h2>Act 2: The Entitlements Enigma</h2>\n<p>After digging through <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://sparkle-project.org/documentation/sandboxing/\">Sparkle’s documentation</a> and Console logs, I discovered my first issue: missing mach-lookup entitlements. In a sandboxed app, Sparkle uses XPC services to perform privileged operations, and these services need special permissions to communicate.</p>\n<h3>The Missing Piece</h3>\n<p>My entitlements file was missing a critical entry:</p>\n<pre><code class=\"language-xml\"><span class=\"code-snippet\" data-language=\"XML\"><span class=\"syntax-text syntax-xml\"><span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.apple.security.temporary-exception.mach-lookup.global-name<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">array</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.steipete.vibemeter-spks<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.steipete.vibemeter-spki<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">array</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n</span></span></code></pre>\n<p>But here’s the kicker - I initially only added <code>-spks</code>, thinking it stood for “Sparkle Server.” Turns out, you need BOTH:</p>\n<ul>\n<li><code>-spks</code>: Sparkle Server (for the InstallerLauncher.xpc service)</li>\n<li><code>-spki</code>: Sparkle Installer (for the Installer.xpc service)</li>\n</ul>\n<p>Missing either one results in the dreaded authorization error.</p>\n<h2>Act 3: The Code Signing Circus</h2>\n<p>Next came the code signing adventures. My notarization script was doing what seemed logical:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>deep</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>force</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>sign</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>Developer ID<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> VibeMeter.app</span>\n</span></span></code></pre>\n<p>But Sparkle’s documentation specifically warns against using <code>--deep</code>. Why? Because it can mess up the XPC services’ signatures. Instead, you need to sign components in a specific order.</p>\n<p>Here’s the correct approach from my <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh\">codesign script</a>:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Sign XPC services first</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>f</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>s</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SIGN_IDENTITY</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>o</span> runtime <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>    <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span>/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Preserve entitlements for Downloader.xpc (Sparkle 2.6+)</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>f</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>s</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SIGN_IDENTITY</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>o</span> runtime<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>preserve-metadata</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-option syntax-shell\">=</span>entitlements <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>    <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span>/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Sign other Sparkle components</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>f</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>s</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SIGN_IDENTITY</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>o</span> runtime <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>    <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span>/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Then sign the framework</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>f</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>s</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SIGN_IDENTITY</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>o</span> runtime <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>    <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span>/Contents/Frameworks/Sparkle.framework<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Finally, sign the app WITHOUT --deep</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>force</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>sign</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SIGN_IDENTITY</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>entitlements</span> VibeMeter.entitlements <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>options</span> runtime VibeMeter.app</span>\n</span></span></code></pre>\n<h2>Act 4: The Bundle ID Bamboozle</h2>\n<p>At one point, I thought I was being clever (or rather, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/claude-code-is-my-computer/\">Claude Code</a> thought…) by trying to manually modify the XPC services’ bundle identifiers. Big mistake.</p>\n<p>For sandboxed apps, Sparkle requires specific bundle ID suffixes:</p>\n<ul>\n<li>Use <code>-spks</code> suffix for the InstallerLauncher service</li>\n<li>Use <code>-spki</code> suffix for the Installer service</li>\n</ul>\n<p>The framework expects these exact suffixes. Use anything else, and you’ll get cryptic XPC connection errors that will make you question your career choices.</p>\n<h2>Act 5: The Build Number Blues</h2>\n<p>Even after fixing all the sandboxing issues, I hit another snag. Users were seeing “You’re up to date!” when updates were clearly available. The culprit? My appcast<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup> generation script was defaulting build numbers to “1”.</p>\n<p>Sparkle uses build numbers (CFBundleVersion), not version strings, to determine if an update is available. If your build numbers don’t increment, Sparkle thinks there’s nothing new.</p>\n<p>My <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh\">appcast generation script</a> now properly handles this:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Extract build number from Info.plist</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">BUILD_NUMBER</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\"><span class=\"syntax-meta syntax-group syntax-expansion syntax-command syntax-parens syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell\">(</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">plutil</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>extract</span> CFBundleVersion raw <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">INFO_PLIST</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell\">)</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Ensure build numbers increment correctly</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-keyword syntax-control syntax-if syntax-begin syntax-shell\">if</span> <span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-support syntax-function syntax-double-brace syntax-begin syntax-shell\">[[</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">BUILD_NUMBER</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">-</span>le</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">PREVIOUS_BUILD_NUMBER</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-support syntax-function syntax-double-brace syntax-end syntax-shell\">]]</span></span><span class=\"syntax-keyword syntax-operator syntax-logical syntax-continue syntax-shell\">;</span> <span class=\"syntax-keyword syntax-control syntax-if syntax-then syntax-shell\">then</span>\n    <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-echo syntax-shell\">echo</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>Error: Build number must be greater than previous release<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n    <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-exit syntax-shell\">exit</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> 1</span>\n<span class=\"syntax-keyword syntax-control syntax-if syntax-end syntax-shell\">fi</span>\n</span></span></code></pre>\n<h2>The Grand Finale: It Works!</h2>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/sparkle.png\" alt=\"Sparkle update dialog working perfectly\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>After two days of intense debugging, I finally had a working setup. My complete automation pipeline is now rock-solid, with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/tree/main/scripts\">comprehensive scripts</a> that handle every aspect of the process.</p>\n<h3>The Magic Recipe</h3>\n<ol>\n<li><strong>Entitlements</strong>: Include BOTH <code>-spks</code> and <code>-spki</code> mach-lookup exceptions</li>\n<li><strong>Bundle IDs</strong>: Never change Sparkle’s XPC service bundle IDs</li>\n<li><strong>Code Signing</strong>: Sign XPC services individually, never use <code>--deep</code></li>\n<li><strong>Build Numbers</strong>: Always increment them, and verify your appcast</li>\n<li><strong>Info.plist</strong>: Set <code>SUEnableInstallerLauncherService = true</code> and <code>SUEnableDownloaderService = false</code><sup class=\"footnote-ref\"><a href=\"#fn-2\" id=\"fnref-2\" data-footnote-ref>2</a></sup></li>\n</ol>\n<h3>The Working Configuration</h3>\n<p>The XPC services themselves keep their original Sparkle bundle IDs (<code>org.sparkle-project.InstallerLauncher</code> and <code>org.sparkle-project.Downloader</code>). What we configure in the entitlements are the mach-lookup communication channels:</p>\n<pre><code class=\"language-xml\"><span class=\"code-snippet\" data-language=\"XML\"><span class=\"syntax-text syntax-xml\"><span class=\"syntax-comment syntax-block syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-xml\">&lt;!--</span> VibeMeter.entitlements <span class=\"syntax-punctuation syntax-definition syntax-comment syntax-end syntax-xml\">--&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.apple.security.app-sandbox<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">true</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">/&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.apple.security.network.client<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">true</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">/&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.apple.security.temporary-exception.mach-lookup.global-name<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">array</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.steipete.vibemeter-spks<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.steipete.vibemeter-spki<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">array</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n</span></span></code></pre>\n<h2>The Scripts That Saved My Sanity</h2>\n<p>My complete build pipeline consists of several specialized scripts:</p>\n<ul>\n<li><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/preflight-check.sh\">preflight-check.sh</a></strong>: Validates environment and prerequisites</li>\n<li><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh\">codesign-app.sh</a></strong>: Handles the complex code signing process</li>\n<li><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh\">notarize-app.sh</a></strong>: Manages notarization and stapling</li>\n<li><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/create-dmg.sh\">create-dmg.sh</a></strong>: Creates and signs distribution DMGs</li>\n<li><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh\">generate-appcast.sh</a></strong>: Generates Sparkle appcast files</li>\n<li><strong><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh\">release.sh</a></strong>: Orchestrates the entire release process</li>\n</ul>\n<p>Look at this beauty! Now even Claude can do releases without messing up 🎉 I just tell it “Create a new beta release, see release.md” and it handles everything.</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Now create the release</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">./scripts/release.sh</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> stable        <span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> For stable 1.1.0</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> OR</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">./scripts/release.sh</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> beta 1        <span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> For 1.1.0-beta.1</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span></span>\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Use the version script to bump versions</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">./scripts/version.sh</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>minor       <span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> 1.0.0 -&gt; 1.1.0</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span></span></span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">./scripts/version.sh</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>major       <span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> 1.0.0 -&gt; 2.0.0</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span></span></span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">./scripts/version.sh</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>set</span> 1.1.0   <span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Set specific version</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span></span></span></span></code></pre>\n<p>Everything happens automatically - from building to GitHub release creation. These scripts are tuned for <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://tuist.dev/\">Tuist</a> and can easily be adapted for simpler setups.</p>\n<h2>The Notarization Nightmare</h2>\n<p>Apple’s notarization process adds another layer of complexity. Notarization verifies your app is safe - you upload it to Apple’s servers for scanning, and if approved, they “staple” a ticket to your app. The process typically takes a few minutes in Apple’s notarization queue (officially called the “processing queue”). The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh\">notarization script</a> uses the modern <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution\"><code>notarytool</code></a> approach:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Create ZIP for notarization (not DMG!)</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">ditto</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>c</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>k</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>keepParent</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">ZIP_PATH</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Submit for notarization</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">xcrun</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> notarytool submit <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">ZIP_PATH</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>key</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">API_KEY_PATH</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>key-id</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">API_KEY_ID</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>issuer</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">ISSUER_ID</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">    --</span>wait</span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Staple the ticket</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">xcrun</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> stapler staple <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Verify everything worked</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">spctl</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>a</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>t</span> exec<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>vv</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">xcrun</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> stapler validate <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">APP_BUNDLE</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n</span></span></code></pre>\n<p>Of course this needs <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution\">certificates from Apple</a>. Luckily Claude is smart enough to guide you through the whole process of downloading those files and adding them to the keychain and your environment.</p>\n<h2>Pretty Changelogs in Sparkle</h2>\n<p>One challenge I hadn’t anticipated was making the update dialogs actually useful. Sparkle can display rich HTML changelogs, but getting from my Markdown changelog to properly formatted HTML required some creativity.</p>\n<p>The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/changelog-to-html.sh\">changelog-to-html.sh script</a> extracts version-specific sections from <code>CHANGELOG.md</code> and converts them to HTML with what I call the “Poor Man’s Markdown Parser”:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Extract version section and convert Markdown to HTML</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">awk</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>/^## <span class=\"syntax-constant syntax-character syntax-escape syntax-shell\">\\\\</span>[<span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">version</span></span><span class=\"syntax-constant syntax-character syntax-escape syntax-shell\">\\\\</span>]/{flag=1;next}/^## <span class=\"syntax-constant syntax-character syntax-escape syntax-shell\">\\\\</span>[/{flag=0}flag<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> CHANGELOG.md</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">sed</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>s/^### \\(.*\\)/&lt;h3&gt;\\1&lt;\\/h3&gt;/<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">sed</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>s/^- \\(.*\\)/&lt;li&gt;\\1&lt;\\/li&gt;/<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">sed</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>s/\\*\\*\\([^*]*\\)\\*\\*/&lt;strong&gt;\\1&lt;\\/strong&gt;/g<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span></span>\n</span></span></code></pre>\n<p>The result? Users see properly formatted changelogs with headers, lists, and styled text instead of raw Markdown. Note: This parser has limitations - it doesn’t handle nested lists, images, or links. Keep your changelogs simple or extend the parser for more complex formatting.</p>\n<h2>The Complete Architecture</h2>\n<p>What emerged is a surprisingly elegant zero-infrastructure solution that leverages GitHub’s existing services:</p>\n<h3>GitHub-Centric Distribution</h3>\n<ul>\n<li><strong>Releases</strong>: <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/releases\">GitHub releases</a> host the actual DMG files</li>\n<li><strong>Appcast Hosting</strong>: <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://raw.githubusercontent.com/steipete/VibeMeter/main/appcast.xml\">Raw GitHub URLs</a> serve the XML feeds</li>\n<li><strong>Dual Feeds</strong>: Separate appcasts for <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/appcast.xml\">stable</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/appcast-prerelease.xml\">pre-release</a> channels</li>\n<li><strong>Version Control</strong>: Appcast files are versioned alongside the code</li>\n</ul>\n<h3>Dynamic Channel Switching</h3>\n<p>The app includes runtime logic to switch between update channels without reinstallation. Users can choose “stable” for production releases or “pre-release” for beta access, and the app dynamically points to the appropriate appcast URL.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/vibemeter-settings.png\" alt=\"Vibe Meter settings showing update channel options\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<h3>Automated Everything</h3>\n<p>The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh\">release.sh script</a> orchestrates the entire pipeline:</p>\n<ol>\n<li>Build and sign the app</li>\n<li>Create GitHub release with DMG</li>\n<li>Generate both appcast files with proper signatures</li>\n<li>Commit and push everything</li>\n</ol>\n<p>No separate hosting, no Jekyll setup, no additional infrastructure - just GitHub doing what it does best. The key is that <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh\"><code>release.sh</code></a> is the master orchestrator that calls most other scripts in sequence for a complete automated release.</p>\n<p>View the complete script flow architecture</p>\n<pre><code class=\"language-plaintext\"><span class=\"code-snippet\">🚀 Main Release Flow (release.sh)\n\nrelease.sh\n├── 1. preflight-check.sh ────────────── Validates everything ready\n├── 2. generate-xcproj.sh ────────────── Generates Xcode project\n├── 3. build.sh ──────────────────────── Builds app with IS_PRERELEASE_BUILD flag\n├── 4. sign-and-notarize.sh ──────────── Signs &amp; notarizes app\n│   ├── codesign-app.sh ─────────────── Code signs app bundle\n│   └── notarize-app.sh ─────────────── Notarizes signed app\n├── 5. create-dmg.sh ─────────────────── Creates &amp; signs DMG\n├── 6. GitHub CLI (gh) ───────────────── Creates GitHub release\n├── 7. generate-appcast.sh ───────────── Updates appcast XML files\n└── 8. verify-appcast.sh ─────────────── Validates appcast (optional)\n\n✅ Verification Flow\n\npreflight-check.sh ───────────────────── Pre-release validation\n└── verify-prerelease-system.sh ──────── IS_PRERELEASE_BUILD system check\n\nverify-app.sh ────────────────────────── Post-build verification\nverify-appcast.sh ────────────────────── Appcast validation\n\n🔐 Manual Signing Flow\n\nsign-and-notarize.sh\n├── codesign-app.sh ──────────────────── Code sign app\n├── notarize-app.sh ──────────────────── Notarize app\n└── create-dmg.sh ────────────────────── Create distribution DMG\n\n🛠️ Utility Scripts (Called by others)\n\n- changelog-to-html.sh ← Called by update-appcast.sh\n- version.sh ← Standalone version management\n</span></code></pre>\n<h2>Lessons Learned</h2>\n<ol>\n<li><strong>Read the documentation carefully</strong> - But also know that it might not cover every edge case</li>\n<li><strong>Console.app is your friend</strong> - Filter by your process name and watch for XPC errors</li>\n<li><strong>Don’t be clever</strong> - Follow Sparkle’s conventions exactly</li>\n<li><strong>Test updates, not just builds</strong> - A successful build doesn’t mean updates will work</li>\n<li><strong>Automate everything</strong> - Manual processes lead to human errors</li>\n<li><strong>Version control your scripts</strong> - Build automation is as important as your app code</li>\n</ol>\n<h2>Final Thoughts</h2>\n<p>Implementing Sparkle in a sandboxed app is like solving a puzzle where the pieces keep changing shape. But once you understand the rules - respect the XPC services, get your entitlements right, and sign everything properly - it works beautifully.</p>\n<p>The irony? The final solution is actually quite simple. It’s getting there that’s the adventure. I don’t know how anyone manages to ship working macOS apps at all, honestly.</p>\n<p>Now go download <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://vibemeter.ai\">Vibe Meter</a> and read my dev diary on how I built it with Claude Code: <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/vibe-meter-monitor-your-ai-costs\">Vibe Meter: Monitor Your AI Costs</a>.</p>\n<h2>Resources</h2>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://sparkle-project.org/documentation/sandboxing/\">Sparkle Sandboxing Documentation</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution\">Apple’s Code Signing Guide</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter\">VibeMeter Source Code</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/tree/main/scripts\">VibeMeter Build Scripts</a></li>\n</ul>\n<hr />\n<p><em>Special thanks to the Sparkle team and Claude Code - without them, this automation pipeline wouldn’t exist.</em></p>\n<p><strong>P.S.</strong> Just <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/VibeMeter/tree/main/scripts\">steal my scripts</a>. It’ll save you days of debugging. ☕️</p>\n<section class=\"footnotes\" data-footnotes>\n<ol>\n<li id=\"fn-1\">\n<p>An appcast is an RSS-like XML feed that Sparkle uses to check for updates. It contains information about available versions, download URLs, and release notes. See <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://sparkle-project.org/documentation/publishing/\">Sparkle’s documentation on appcasts</a> for more details. <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n</li>\n<li id=\"fn-2\">\n<p>The Downloader service is only needed if your app doesn’t have network access. Since Vibe Meter includes the <code>com.apple.security.network.client</code> entitlement, we can disable SUEnableDownloaderService. This simplifies the setup and avoids the Downloader service’s limitations. See <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://sparkle-project.github.io/documentation/sandboxing/\">Sparkle’s sandboxing documentation</a> for details. <a href=\"#fnref-2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n</li>\n</ol>\n</section>\n",
  "content_markdown": "![](https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/header.png)\n\n*Or: How I Learned to Stop Worrying and Love the XPC Services*\n\nIf you’ve ever tried to implement automatic updates in a sandboxed macOS app using Sparkle, you know it can feel like trying to solve a Rubik’s cube while wearing oven mitts. After creating way too many beta releases and spending countless hours debugging cryptic authorization errors, I finally cracked the code. Here’s my step-by-step guide to code signing, notarization, and Sparkle auto-updates in sandboxed macOS apps.\n\n## The Setup: Vibe Meter Meets Sparkle\n\n[Vibe Meter](https://vibemeter.ai) is a sandboxed macOS menu bar app that tracks AI service spending. When I decided to add automatic updates using Sparkle 2.x, I thought it would be straightforward. After all, Sparkle is the de facto standard for macOS app updates, right?\n\nOh, sweet summer child.\n\n## Act 1: The Mysterious Authorization Failure\n\nI’d already mastered code signing and notarization (its own nightmare), so I just told Claude: “Look at this repository and do what we did there.” It nailed the signing and notarization process.\n\nMy first attempts seemed promising. The app built, signed, and notarized successfully. But when users tried to update, they were greeted with:\n\n```plaintext\nError: Failed to gain authorization required to update target\n```\n\nThis error is Sparkle’s polite way of saying “I can’t talk to my XPC services, and I have no idea why.”\n\n## Act 2: The Entitlements Enigma\n\nAfter digging through [Sparkle’s documentation](https://sparkle-project.org/documentation/sandboxing/) and Console logs, I discovered my first issue: missing mach-lookup entitlements. In a sandboxed app, Sparkle uses XPC services to perform privileged operations, and these services need special permissions to communicate.\n\n### The Missing Piece\n\nMy entitlements file was missing a critical entry:\n\n```xml\n<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>\n<array>\n    <string>com.steipete.vibemeter-spks</string>\n    <string>com.steipete.vibemeter-spki</string>\n</array>\n```\n\nBut here’s the kicker - I initially only added `-spks`, thinking it stood for “Sparkle Server.” Turns out, you need BOTH:\n\n- `-spks`: Sparkle Server (for the InstallerLauncher.xpc service)\n- `-spki`: Sparkle Installer (for the Installer.xpc service)\n\nMissing either one results in the dreaded authorization error.\n\n## Act 3: The Code Signing Circus\n\nNext came the code signing adventures. My notarization script was doing what seemed logical:\n\n```bash\ncodesign --deep --force --sign \"Developer ID\" VibeMeter.app\n```\n\nBut Sparkle’s documentation specifically warns against using `--deep`. Why? Because it can mess up the XPC services’ signatures. Instead, you need to sign components in a specific order.\n\nHere’s the correct approach from my [codesign script](https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh):\n\n```bash\n# Sign XPC services first\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc\"\n\n# Preserve entitlements for Downloader.xpc (Sparkle 2.6+)\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime --preserve-metadata=entitlements \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc\"\n\n# Sign other Sparkle components\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate\"\n\n# Then sign the framework\ncodesign -f -s \"$SIGN_IDENTITY\" -o runtime \\\n    \"$APP_BUNDLE/Contents/Frameworks/Sparkle.framework\"\n\n# Finally, sign the app WITHOUT --deep\ncodesign --force --sign \"$SIGN_IDENTITY\" --entitlements VibeMeter.entitlements \\\n    --options runtime VibeMeter.app\n```\n\n## Act 4: The Bundle ID Bamboozle\n\nAt one point, I thought I was being clever (or rather, [Claude Code](https://steipete.me/posts/claude-code-is-my-computer/) thought…) by trying to manually modify the XPC services’ bundle identifiers. Big mistake.\n\nFor sandboxed apps, Sparkle requires specific bundle ID suffixes:\n\n- Use `-spks` suffix for the InstallerLauncher service\n- Use `-spki` suffix for the Installer service\n\nThe framework expects these exact suffixes. Use anything else, and you’ll get cryptic XPC connection errors that will make you question your career choices.\n\n## Act 5: The Build Number Blues\n\nEven after fixing all the sandboxing issues, I hit another snag. Users were seeing “You’re up to date!” when updates were clearly available. The culprit? My appcast[^1] generation script was defaulting build numbers to “1”.\n\nSparkle uses build numbers (CFBundleVersion), not version strings, to determine if an update is available. If your build numbers don’t increment, Sparkle thinks there’s nothing new.\n\nMy [appcast generation script](https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh) now properly handles this:\n\n```bash\n# Extract build number from Info.plist\nBUILD_NUMBER=$(plutil -extract CFBundleVersion raw \"$INFO_PLIST\")\n\n# Ensure build numbers increment correctly\nif [[ \"$BUILD_NUMBER\" -le \"$PREVIOUS_BUILD_NUMBER\" ]]; then\n    echo \"Error: Build number must be greater than previous release\"\n    exit 1\nfi\n```\n\n## The Grand Finale: It Works!\n\n![Sparkle update dialog working perfectly](https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/sparkle.png)\n\nAfter two days of intense debugging, I finally had a working setup. My complete automation pipeline is now rock-solid, with [comprehensive scripts](https://github.com/steipete/VibeMeter/tree/main/scripts) that handle every aspect of the process.\n\n### The Magic Recipe\n\n1. **Entitlements**: Include BOTH `-spks` and `-spki` mach-lookup exceptions\n2. **Bundle IDs**: Never change Sparkle’s XPC service bundle IDs\n3. **Code Signing**: Sign XPC services individually, never use `--deep`\n4. **Build Numbers**: Always increment them, and verify your appcast\n5. **Info.plist**: Set `SUEnableInstallerLauncherService = true` and `SUEnableDownloaderService = false`[^2]\n\n### The Working Configuration\n\nThe XPC services themselves keep their original Sparkle bundle IDs (`org.sparkle-project.InstallerLauncher` and `org.sparkle-project.Downloader`). What we configure in the entitlements are the mach-lookup communication channels:\n\n```xml\n<!-- VibeMeter.entitlements -->\n<key>com.apple.security.app-sandbox</key>\n<true/>\n<key>com.apple.security.network.client</key>\n<true/>\n<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>\n<array>\n    <string>com.steipete.vibemeter-spks</string>\n    <string>com.steipete.vibemeter-spki</string>\n</array>\n```\n\n## The Scripts That Saved My Sanity\n\nMy complete build pipeline consists of several specialized scripts:\n\n- **[preflight-check.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/preflight-check.sh)**: Validates environment and prerequisites\n- **[codesign-app.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/codesign-app.sh)**: Handles the complex code signing process\n- **[notarize-app.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh)**: Manages notarization and stapling\n- **[create-dmg.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/create-dmg.sh)**: Creates and signs distribution DMGs\n- **[generate-appcast.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/generate-appcast.sh)**: Generates Sparkle appcast files\n- **[release.sh](https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh)**: Orchestrates the entire release process\n\nLook at this beauty! Now even Claude can do releases without messing up 🎉 I just tell it “Create a new beta release, see release.md” and it handles everything.\n\n```bash\n# Now create the release\n./scripts/release.sh stable        # For stable 1.1.0\n# OR\n./scripts/release.sh beta 1        # For 1.1.0-beta.1\n\n# Use the version script to bump versions\n./scripts/version.sh --minor       # 1.0.0 -> 1.1.0\n./scripts/version.sh --major       # 1.0.0 -> 2.0.0\n./scripts/version.sh --set 1.1.0   # Set specific version\n```\n\nEverything happens automatically - from building to GitHub release creation. These scripts are tuned for [Tuist](https://tuist.dev/) and can easily be adapted for simpler setups.\n\n## The Notarization Nightmare\n\nApple’s notarization process adds another layer of complexity. Notarization verifies your app is safe - you upload it to Apple’s servers for scanning, and if approved, they “staple” a ticket to your app. The process typically takes a few minutes in Apple’s notarization queue (officially called the “processing queue”). The [notarization script](https://github.com/steipete/VibeMeter/blob/main/scripts/notarize-app.sh) uses the modern [`notarytool`](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) approach:\n\n```bash\n# Create ZIP for notarization (not DMG!)\nditto -c -k --keepParent \"$APP_BUNDLE\" \"$ZIP_PATH\"\n\n# Submit for notarization\nxcrun notarytool submit \"$ZIP_PATH\" \\\n    --key \"$API_KEY_PATH\" \\\n    --key-id \"$API_KEY_ID\" \\\n    --issuer \"$ISSUER_ID\" \\\n    --wait\n\n# Staple the ticket\nxcrun stapler staple \"$APP_BUNDLE\"\n\n# Verify everything worked\nspctl -a -t exec -vv \"$APP_BUNDLE\"\nxcrun stapler validate \"$APP_BUNDLE\"\n```\n\nOf course this needs [certificates from Apple](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution). Luckily Claude is smart enough to guide you through the whole process of downloading those files and adding them to the keychain and your environment.\n\n## Pretty Changelogs in Sparkle\n\nOne challenge I hadn’t anticipated was making the update dialogs actually useful. Sparkle can display rich HTML changelogs, but getting from my Markdown changelog to properly formatted HTML required some creativity.\n\nThe [changelog-to-html.sh script](https://github.com/steipete/VibeMeter/blob/main/scripts/changelog-to-html.sh) extracts version-specific sections from `CHANGELOG.md` and converts them to HTML with what I call the “Poor Man’s Markdown Parser”:\n\n```bash\n# Extract version section and convert Markdown to HTML\nawk \"/^## \\\\[$version\\\\]/{flag=1;next}/^## \\\\[/{flag=0}flag\" CHANGELOG.md | \\\nsed 's/^### \\(.*\\)/<h3>\\1<\\/h3>/' | \\\nsed 's/^- \\(.*\\)/<li>\\1<\\/li>/' | \\\nsed 's/\\*\\*\\([^*]*\\)\\*\\*/<strong>\\1<\\/strong>/g'\n```\n\nThe result? Users see properly formatted changelogs with headers, lists, and styled text instead of raw Markdown. Note: This parser has limitations - it doesn’t handle nested lists, images, or links. Keep your changelogs simple or extend the parser for more complex formatting.\n\n## The Complete Architecture\n\nWhat emerged is a surprisingly elegant zero-infrastructure solution that leverages GitHub’s existing services:\n\n### GitHub-Centric Distribution\n\n- **Releases**: [GitHub releases](https://github.com/steipete/VibeMeter/releases) host the actual DMG files\n- **Appcast Hosting**: [Raw GitHub URLs](https://raw.githubusercontent.com/steipete/VibeMeter/main/appcast.xml) serve the XML feeds\n- **Dual Feeds**: Separate appcasts for [stable](https://github.com/steipete/VibeMeter/blob/main/appcast.xml) and [pre-release](https://github.com/steipete/VibeMeter/blob/main/appcast-prerelease.xml) channels\n- **Version Control**: Appcast files are versioned alongside the code\n\n### Dynamic Channel Switching\n\nThe app includes runtime logic to switch between update channels without reinstallation. Users can choose “stable” for production releases or “pre-release” for beta access, and the app dynamically points to the appropriate appcast URL.\n\n![Vibe Meter settings showing update channel options](https://steipete.me/assets/img/2025/code-signing-and-notarization-sparkle-and-tears/vibemeter-settings.png)\n\n### Automated Everything\n\nThe [release.sh script](https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh) orchestrates the entire pipeline:\n\n1. Build and sign the app\n2. Create GitHub release with DMG\n3. Generate both appcast files with proper signatures\n4. Commit and push everything\n\nNo separate hosting, no Jekyll setup, no additional infrastructure - just GitHub doing what it does best. The key is that [`release.sh`](https://github.com/steipete/VibeMeter/blob/main/scripts/release.sh) is the master orchestrator that calls most other scripts in sequence for a complete automated release.\n\nView the complete script flow architecture\n\n```plaintext\n🚀 Main Release Flow (release.sh)\n\nrelease.sh\n├── 1. preflight-check.sh ────────────── Validates everything ready\n├── 2. generate-xcproj.sh ────────────── Generates Xcode project\n├── 3. build.sh ──────────────────────── Builds app with IS_PRERELEASE_BUILD flag\n├── 4. sign-and-notarize.sh ──────────── Signs & notarizes app\n│   ├── codesign-app.sh ─────────────── Code signs app bundle\n│   └── notarize-app.sh ─────────────── Notarizes signed app\n├── 5. create-dmg.sh ─────────────────── Creates & signs DMG\n├── 6. GitHub CLI (gh) ───────────────── Creates GitHub release\n├── 7. generate-appcast.sh ───────────── Updates appcast XML files\n└── 8. verify-appcast.sh ─────────────── Validates appcast (optional)\n\n✅ Verification Flow\n\npreflight-check.sh ───────────────────── Pre-release validation\n└── verify-prerelease-system.sh ──────── IS_PRERELEASE_BUILD system check\n\nverify-app.sh ────────────────────────── Post-build verification\nverify-appcast.sh ────────────────────── Appcast validation\n\n🔐 Manual Signing Flow\n\nsign-and-notarize.sh\n├── codesign-app.sh ──────────────────── Code sign app\n├── notarize-app.sh ──────────────────── Notarize app\n└── create-dmg.sh ────────────────────── Create distribution DMG\n\n🛠️ Utility Scripts (Called by others)\n\n- changelog-to-html.sh ← Called by update-appcast.sh\n- version.sh ← Standalone version management\n```\n\n## Lessons Learned\n\n1. **Read the documentation carefully** - But also know that it might not cover every edge case\n2. **Console.app is your friend** - Filter by your process name and watch for XPC errors\n3. **Don’t be clever** - Follow Sparkle’s conventions exactly\n4. **Test updates, not just builds** - A successful build doesn’t mean updates will work\n5. **Automate everything** - Manual processes lead to human errors\n6. **Version control your scripts** - Build automation is as important as your app code\n\n## Final Thoughts\n\nImplementing Sparkle in a sandboxed app is like solving a puzzle where the pieces keep changing shape. But once you understand the rules - respect the XPC services, get your entitlements right, and sign everything properly - it works beautifully.\n\nThe irony? The final solution is actually quite simple. It’s getting there that’s the adventure. I don’t know how anyone manages to ship working macOS apps at all, honestly.\n\nNow go download [Vibe Meter](https://vibemeter.ai) and read my dev diary on how I built it with Claude Code: [Vibe Meter: Monitor Your AI Costs](https://steipete.me/posts/vibe-meter-monitor-your-ai-costs).\n\n## Resources\n\n- [Sparkle Sandboxing Documentation](https://sparkle-project.org/documentation/sandboxing/)\n- [Apple’s Code Signing Guide](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)\n- [VibeMeter Source Code](https://github.com/steipete/VibeMeter)\n- [VibeMeter Build Scripts](https://github.com/steipete/VibeMeter/tree/main/scripts)\n\n* * *\n\n*Special thanks to the Sparkle team and Claude Code - without them, this automation pipeline wouldn’t exist.*\n\n**P.S.** Just [steal my scripts](https://github.com/steipete/VibeMeter/tree/main/scripts). It’ll save you days of debugging. ☕️\n\n[^1]: An appcast is an RSS-like XML feed that Sparkle uses to check for updates. It contains information about available versions, download URLs, and release notes. See [Sparkle’s documentation on appcasts](https://sparkle-project.org/documentation/publishing/) for more details.\n\n[^2]: The Downloader service is only needed if your app doesn’t have network access. Since Vibe Meter includes the `com.apple.security.network.client` entitlement, we can disable SUEnableDownloaderService. This simplifies the setup and avoids the Downloader service’s limitations. See [Sparkle’s sandboxing documentation](https://sparkle-project.github.io/documentation/sandboxing/) for details.\n",
  "dateCreated": "2026-09-27T19:29:15.293681927+00:00",
  "date_created": "2025-06-05T07:00:00+00:00",
  "date_modified": "2025-06-05T07:00:00+00:00",
  "date_published": "2025-06-05T07:00:00+00:00",
  "description": "My brutal journey implementing Sparkle auto-updates in sandboxed macOS apps - from 40 failed releases to enlightenment.",
  "external_url": "https://steipete.me/posts/2025/code-signing-and-notarization-sparkle-and-tears/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/steipete-me/2025/06/2025-06-05-code-signing-and-notarization-sparkle-and-tears.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/steipete-me/2025/06/2025-06-05-code-signing-and-notarization-sparkle-and-tears.md"
  },
  "id": "urn:aggr:item:76d5f33058c84a76be425a01280acb64b1a1a4d7",
  "isBasedOn": {
    "@id": "https://steipete.me/posts/2025/code-signing-and-notarization-sparkle-and-tears/"
  },
  "mainEntity": {
    "@id": "https://steipete.me/posts/2025/code-signing-and-notarization-sparkle-and-tears/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-05-code-signing-and-notarization-sparkle-and-tears/#webpage"
    },
    "dateModified": "2025-06-05T07:00:00+00:00",
    "datePublished": "2025-06-05T07:00:00+00:00",
    "headline": "Code Signing and Notarization: Sparkle and Tears",
    "timeRequired": "PT9M",
    "url": "https://steipete.me/posts/2025/code-signing-and-notarization-sparkle-and-tears/",
    "wordCount": 1806
  },
  "name": "Archived snapshot: Code Signing and Notarization: Sparkle and Tears",
  "preview": null,
  "reading_minutes": 9,
  "replicated_at": null,
  "source": "steipete-me",
  "source_name": "Peter Steinberger",
  "summary": "My brutal journey implementing Sparkle auto-updates in sandboxed macOS apps - from 40 failed releases to enlightenment.",
  "tags": [],
  "temporalCoverage": "2026-09-27T19:29:15.293681927+00:00",
  "title": "Code Signing and Notarization: Sparkle and Tears",
  "url": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-05-code-signing-and-notarization-sparkle-and-tears/",
  "word_count": 1806
}