plugins/mac-build-notarize-plugin/skills/mac-build-notarize/SKILL.md
Generate a build, sign, notarize, and package script for a macOS app with Sparkle auto-update support via GitHub Releases. Use when the user asks to create a build script, notarization script, distribution script, release pipeline, or auto-update setup for a Mac app. Also use when the user mentions Sparkle, appcast, notarization, or distributing a macOS app outside the App Store.
npx skillsauth add memfrag/apparata-plugins mac-build-notarizeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generate a complete release pipeline for a macOS app distributed via GitHub Releases with Sparkle auto-updates.
Discover the Team ID by running:
security find-identity -v -p codesigning | grep "Developer ID Application" | head -1
Extract the team ID (the 10-character alphanumeric string in parentheses at the end).
The keychain profile for notarization defaults to notary (stored via xcrun notarytool store-credentials). Ask the user if they have a different profile name.
Generate the release pipeline for the app: $ARGUMENTS
.xcodeproj or .xcworkspace in the current directoryxcodebuild -list$ARGUMENTS is provided, use it as the app name; otherwise infer from the projectproject.pbxproj for: bundle identifier, sandbox status (ENABLE_APP_SANDBOX), current MARKETING_VERSION and CURRENT_PROJECT_VERSIONgit remote get-url originBefore generating, ask:
MARKETING_VERSION and CURRENT_PROJECT_VERSION in project.pbxproj, commits, and pushes.)-arch arm64 -arch x86_64)?scripts/ExportOptions.plist<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>TEAM_ID_HERE</string>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
scripts/build-and-notarize.shThe script should follow this flow:
set -euo pipefail...), em-dashes, curly quotes, or other non-ASCII characters. Bash can interpret Unicode characters adjacent to $VARIABLE names as part of the variable name, causing "unbound variable" errors.SCHEME, APP_NAME, KEYCHAIN_PROFILE="notary", SPARKLE_VERSION="2.9.0"SCRIPT_DIR, PROJECT_DIR, BUILD_DIR, SPARKLE_TOOLS_DIR, ARCHIVE_PATH, EXPORT_DIR, EXPORT_OPTIONSSPARKLE_TOOLS_DIR must be $PROJECT_DIR/Sparkle-tools (not inside $BUILD_DIR), because build/ is cleaned at the start of each run and the tools should persist across builds.build/ directoryif [ ! -x "$SPARKLE_TOOLS_DIR/bin/sign_update" ]; then
curl -sL "https://github.com/sparkle-project/Sparkle/releases/download/$SPARKLE_VERSION/Sparkle-$SPARKLE_VERSION.tar.xz" -o "$BUILD_DIR/Sparkle.tar.xz"
mkdir -p "$SPARKLE_TOOLS_DIR"
tar -xf "$BUILD_DIR/Sparkle.tar.xz" -C "$SPARKLE_TOOLS_DIR"
rm "$BUILD_DIR/Sparkle.tar.xz"
fi
Sparkle-tools/ to the project root .gitignorexcodebuild -showBuildSettings | grep MARKETING_VERSION first. If not found (some projects don't use MARKETING_VERSION), fall back to reading CFBundleShortVersionString from Info.plist via PlistBuddy. The grep pipeline must not fail silently under set -euo pipefail — use || true to handle the empty-result case.gh release view --repo <owner/repo> --json tagName -q '.tagName'Version update must always include Info.plist. generate_appcast reads CFBundleVersion and CFBundleShortVersionString from the built app's Info.plist, not from project.pbxproj build settings. If the project uses MARKETING_VERSION/CURRENT_PROJECT_VERSION in project.pbxproj, update those via sed as well. But always also update CFBundleShortVersionString and CFBundleVersion in Info.plist via PlistBuddy:
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$INFO_PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" "$INFO_PLIST"
Both CFBundleVersion and CFBundleShortVersionString must be set to the same version string. Sparkle uses CFBundleVersion (sparkle:version) for update comparison logic and CFBundleShortVersionString (sparkle:shortVersionString) for display. If they diverge (e.g., CFBundleVersion stays at 1 while the marketing version advances), generate_appcast will emit the wrong sparkle:version and Sparkle's update comparison will break.
Archive with minimal flags — do not add extra build setting overrides:
xcodebuild archive \
-project "$PROJECT_DIR/$APP_NAME.xcodeproj" \
-scheme "$SCHEME" \
-archivePath "$ARCHIVE_PATH" \
-configuration Release \
-arch arm64 \
ENABLE_HARDENED_RUNTIME=YES \
2>&1 | tee "$BUILD_DIR/archive.log" | tail -5
Export with just the plist — no extra flags:
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$EXPORT_OPTIONS" \
2>&1 | tee "$BUILD_DIR/export.log" | tail -5
Do NOT use bare | tail -1 on xcodebuild commands. Under set -euo pipefail, if xcodebuild fails, tail still succeeds and the error is silently swallowed. Use | tee "$BUILD_DIR/<step>.log" | tail -5 to capture full output to a log file while showing progress. After each step, verify the expected output exists (e.g., check the archive directory or exported .app exists) and print the last 30 lines of the log on failure.
SKIP_INSTALL, BUILD_LIBRARY_FOR_DISTRIBUTION, or -allowProvisioningUpdates — these can interfere with SPM dependency signing and cause export failures.CODE_SIGN_IDENTITY as an xcodebuild override — it applies to all targets including SPM dependencies, which may not have a development team set, causing "Signing requires a development team" errors. The signing identity should be configured in the Xcode project's build settings instead.Distribute as DMG, not ZIP. Finder's Archive Utility resolves symlinks when extracting zips, which breaks Sparkle's framework seal and causes Gatekeeper to reject the app with "unsealed contents present in the root directory of an embedded framework."
DMG_STAGING="$BUILD_DIR/dmg-staging"
mkdir -p "$DMG_STAGING"
cp -a "$APP_PATH" "$DMG_STAGING/"
ln -s /Applications "$DMG_STAGING/Applications"
hdiutil create -volname "$APP_NAME" -srcfolder "$DMG_STAGING" -ov -format UDZO "$DMG_PATH"
rm -rf "$DMG_STAGING"
codesign --verify --deep --strictxcrun stapler staple"$SPARKLE_TOOLS_DIR/bin/sign_update" "$DMG_PATH"v prefix)gh release create attaching the DMG--generate-notes for auto-generated release notesDo NOT download all old release DMGs — older releases may share the same CFBundleVersion, causing generate_appcast to fail with "Duplicate updates" errors. Instead:
APPCAST_DIR="$BUILD_DIR/appcast-assets"
mkdir -p "$APPCAST_DIR"
# Copy existing appcast so generate_appcast can append to it
if [ -f "$PROJECT_DIR/appcast.xml" ]; then
cp "$PROJECT_DIR/appcast.xml" "$APPCAST_DIR/"
fi
# Only include the new DMG
cp "$DMG_PATH" "$APPCAST_DIR/"
"$SPARKLE_TOOLS_DIR/bin/generate_appcast" \
--download-url-prefix "https://github.com/<owner>/<repo>/releases/download/$TAG/" \
-o "$APPCAST_DIR/appcast.xml" \
"$APPCAST_DIR"
cp "$APPCAST_DIR/appcast.xml" "$PROJECT_DIR/appcast.xml"
cd "$PROJECT_DIR"
git add appcast.xml
git commit -m "Update appcast for $VERSION"
git push origin HEAD
chmod +x scripts/build-and-notarize.sh
Create or update the app's Info.plist with Sparkle keys. The SUFeedURL should point to the raw appcast in the repo:
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/<owner>/<repo>/main/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>USER_MUST_PROVIDE_THIS</string>
<key>SUEnableInstallerLauncherService</key>
<true/>
Do NOT use INFOPLIST_KEY_ build settings for Sparkle keys — Xcode only recognizes Apple's own keys with that prefix. Custom keys are silently ignored.
Do NOT use the GitHub Atom feed (releases.atom) as SUFeedURL — it lacks the sparkle:version and enclosure metadata Sparkle needs.
Set INFOPLIST_FILE in build settings to point to this file, keeping GENERATE_INFOPLIST_FILE = YES so Xcode merges both.
If the app has ENABLE_APP_SANDBOX = YES, create an entitlements file with the Sparkle mach-lookup exceptions:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
</array>
</dict>
</plist>
Include any other entitlements the app already uses (check existing build settings like ENABLE_OUTGOING_NETWORK_CONNECTIONS, ENABLE_USER_SELECTED_FILES, etc.).
Tell the user to set CODE_SIGN_ENTITLEMENTS in build settings to point to this file.
Create a SwiftUI Commands struct:
import SwiftUI
import Sparkle
struct CheckForUpdatesCommand: Commands {
let updater: SPUUpdater
var body: some Commands {
CommandGroup(after: .appInfo) {
Button("Check for Updates…") {
updater.checkForUpdates()
}
.disabled(!updater.canCheckForUpdates)
}
}
}
Add SPUStandardUpdaterController to the main App struct and wire the CheckForUpdatesCommand into the window's .commands:
import Sparkle
// In the App struct:
private let updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
// Pass updater to the window/scene that has .commands:
CheckForUpdatesCommand(updater: updaterController.updater)
After generating all files, instruct the user to:
Add Sparkle SPM package in Xcode: File > Add Package Dependencies > https://github.com/sparkle-project/Sparkle (Up to Next Major from 2.9.0)
Generate EdDSA keys using the Sparkle tools:
./Sparkle-tools/bin/generate_keys
Then paste the public key into the Info.plist where it says USER_MUST_PROVIDE_THIS.
Set build settings in Xcode:
INFOPLIST_FILE → path to the Info.plist created aboveCODE_SIGN_ENTITLEMENTS → path to the entitlements file (if sandboxed)Set up notarization credentials (if not already done):
xcrun notarytool store-credentials notary --apple-id <APPLE_ID> --team-id TEAM_ID_HERE
Add build/ and Sparkle-tools/ to .gitignore
..., em-dashes, curly quotes) in generated shell scripts. Bash can parse them as part of variable names.MARKETING_VERSION, CURRENT_PROJECT_VERSION (in project.pbxproj), and CFBundleShortVersionString/CFBundleVersion (in Info.plist) must all be updated for each release. Always update Info.plist via PlistBuddy — this is what ends up in the built app and what generate_appcast reads.error() helper function that prints to stderr and exits. Use || error "description" on every critical command. Verify expected outputs exist after archive/export steps.SKIP_INSTALL, BUILD_LIBRARY_FOR_DISTRIBUTION, CODE_SIGN_IDENTITY, or -allowProvisioningUpdates to xcodebuild. These can break SPM dependency signing or cause export failures. Keep archive/export commands minimal — signing should be configured in the Xcode project.SPARKLE_TOOLS_DIR must be in the project root ($PROJECT_DIR/Sparkle-tools), not inside build/, since build/ is cleaned on each run..xcworkspace), use -workspace instead of -project in xcodebuild commands.xcodebuild -list.development
Extract timestamped transcripts from WWDC session videos. Use this skill whenever the user wants to read, search, or get the transcript of a WWDC session or Apple developer video — even if they just say something like "what did they say about concurrency in that talk" or "get me the transcript for session 230". Also use this when the user provides a developer.apple.com/videos URL and wants to know what the video covers.
testing
Download WWDC session videos in HD or SD quality. Use this skill whenever the user wants to download a WWDC video, save a session video to disk, or get the video file for a specific WWDC talk. Supports lookup by URL, session ID (e.g. "wwdc2025/230"), session number, or title. Also use when the user says things like "download the AlarmKit session" or "grab the HD video for session 287".
development
Fetch the WWDC session catalog from Apple's CDN by extracting the catalog URL embedded in one of the Developer.app's binaries. Use this skill whenever the user asks about WWDC sessions, WWDC videos, WWDC catalog, Apple developer conference content, or wants to browse, search, list, or look up any WWDC session or talk — even if they just say something like "what sessions are there about SwiftUI" or "find me that WWDC video about concurrency". Also use this when the user wants to download or work with WWDC session metadata.
tools
Generate a blog-post-style HTML page from a WWDC session, interleaving transcript text with slide screenshots. Use when the user wants to create a blog post, readable page, or visual summary from a WWDC talk — e.g. "create blog post from WWDC session 230", "generate HTML for that WWDC talk", "make a readable version of the AlarmKit session".