Complete guide to set up code signing, notarization, and build a distributable DMG.
- macOS with Apple Silicon
- Xcode installed (with macOS 26 SDK)
- Apple Developer Program membership ($99/year)
brew install xcodegen create-dmg
xcodebuild -downloadComponent MetalToolchainYou need a Developer ID Application certificate to sign apps distributed outside the Mac App Store.
- Open Keychain Access (Applications > Utilities)
- Menu: Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority
- Fill in:
- User Email Address: your Apple ID email
- Common Name: your name or company name
- Request is: select Saved to disk
- Click Continue and save the
.certSigningRequestfile
- Go to Apple Developer > Certificates
- Click the + button
- Under Software, select Developer ID Application
- Click Continue
- Upload the
.certSigningRequestfile from step 2a - Click Continue, then Download the
.cerfile
- Double-click the downloaded
.cerfile — it opens in Keychain Access - It will be installed in your login keychain
security find-identity -v -p codesigning | grep "Developer ID"You should see output like:
1) ABCDEF123456... "Developer ID Application: Your Name (XXXXXXXXXX)"
The 10-character code in parentheses is your Team ID.
Apple notarization requires an app-specific password (not your regular Apple ID password).
- Go to appleid.apple.com
- Sign in, go to Sign-In and Security > App-Specific Passwords
- Click Generate an app-specific password
- Label it
Logue Notarize - Copy the generated password (format:
xxxx-xxxx-xxxx-xxxx)
xcrun notarytool store-credentials "Logue-Notarize" \
--apple-id "your@email.com" \
--team-id "XXXXXXXXXX" \
--password "xxxx-xxxx-xxxx-xxxx"Replace:
your@email.comwith your Apple IDXXXXXXXXXXwith your Team ID from step 2dxxxx-xxxx-xxxx-xxxxwith the app-specific password from step 3a
xcrun notarytool history --keychain-profile "Logue-Notarize"This should authenticate successfully (empty history is fine for a new account).
./scripts/build_release.sh --skip-notarize./scripts/build_release.sh./scripts/build_release.sh \
--version 1.0.0 \
--build 1 \
--team-id XXXXXXXXXX \
--keychain-profile "Logue-Notarize"| Flag | Description | Default |
|---|---|---|
--version X.Y.Z |
App version number | Read from project.yml |
--build N |
Build number | Read from project.yml |
--team-id XXXXXXXXXX |
Apple Team ID | Auto-detected from Keychain |
--keychain-profile NAME |
Notarization credential profile | Logue-Notarize |
--skip-notarize |
Skip notarization step | Off |
After a successful build, you'll find:
build/
Logue.xcarchive # Xcode archive
export/
Logue.app # Signed app bundle
Logue-1.0.0.dmg # Final distributable DMG (signed + notarized)
- Checks prerequisites (xcodegen, create-dmg, certificate)
- Generates Xcode project from
project.yml - Resolves Swift Package Manager dependencies
- Archives the app (Release, arm64)
- Exports the archive with Developer ID signing
- Deep-signs the app bundle with hardened runtime + entitlements
- Creates a styled DMG (drag-to-Applications installer)
- Signs the DMG
- Submits to Apple for notarization and waits for approval
- Staples the notarization ticket to the DMG
The release workflow at .github/workflows/release.yml automates the full pipeline. Push a version tag to trigger it:
git tag v1.0.0
git push origin v1.0.0Set these in Settings > Secrets and variables > Actions:
| Secret | How to Get It |
|---|---|
APPLE_CERTIFICATE_BASE64 |
Export cert from Keychain as .p12, then base64 -i cert.p12 | pbcopy |
APPLE_CERTIFICATE_PASSWORD |
Password you set when exporting the .p12 |
APPLE_TEAM_ID |
10-char Team ID from step 2d |
APPLE_ID |
Your Apple ID email |
APPLE_APP_PASSWORD |
App-specific password from step 3a |
- Open Keychain Access
- Find your Developer ID Application certificate
- Expand it to see the private key
- Select both the certificate and the private key
- Right-click > Export 2 items...
- Save as
.p12, set a strong password - Base64-encode it:
base64 -i certificate.p12 | pbcopy- Paste as the
APPLE_CERTIFICATE_BASE64secret in GitHub
Logue uses Sparkle 2 for automatic updates. Sparkle checks an appcast.xml feed, downloads the update ZIP, verifies its EdDSA signature, and installs it — all with built-in native UI.
- CI builds and codesigns the app, creates a ZIP
- CI signs the ZIP with an EdDSA private key (Sparkle's
sign_update) - CI publishes the ZIP + DMG to GitHub Releases and updates
appcast.xmlin the repo - The app reads the appcast from
raw.githubusercontent.comand downloads ZIPs straight from GitHub Releases — no backend - Sparkle in the app downloads the update and prompts the user to restart
Download Sparkle and run the key generator:
# Download Sparkle release
SPARKLE_VERSION="2.9.1"
curl -L -o /tmp/sparkle.tar.xz \
"https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
mkdir -p /tmp/sparkle-tools
tar xf /tmp/sparkle.tar.xz -C /tmp/sparkle-tools
# Generate key pair
/tmp/sparkle-tools/bin/generate_keysThis prints:
A] Public key to embed in your app's Info.plist as SUPublicEDKey:
<base64 public key>
B] Private key saved to ~/Library/Sparkle/ed25519.key
Back up this file! You'll need it to sign future updates.
⚠️ Never commit the private key. This is a public repo. Keep the private key in your password manager /~/Library/Sparkle/ed25519.keybackup only.
- Public key goes in
project.yml(SUPublicEDKey) — embedded inInfo.plistonxcodegen generate. - Private key must be added as a GitHub Actions secret:
- Go to repo Settings > Secrets and variables > Actions
- Create secret
SPARKLE_PRIVATE_KEYwith the private key value.
- If keys are regenerated, update both
project.yml(SUPublicEDKey) and the GitHub secret.
| Secret | Description |
|---|---|
APPLE_CERTIFICATE_BASE64 |
Developer ID cert (see section 7) |
APPLE_CERTIFICATE_PASSWORD |
Password for the .p12 |
APPLE_TEAM_ID |
10-char Apple Team ID |
APPLE_ID |
Apple ID email |
APPLE_APP_PASSWORD |
App-specific password for notarization |
SPARKLE_PRIVATE_KEY |
EdDSA private key from generate_keys |
- Build the app with a low version (e.g.
0.0.1) - Create a test
appcast.xmlwith a higher version pointing to a local/remote ZIP - Set
SUFeedURLto point to your test appcast (via env override or debug build) - Launch the app — Sparkle will detect the "update" and show its install dialog
The appcast.xml in the repo root is updated automatically by CI on every release. To manually add an entry:
python3 scripts/update_appcast.py \
--version 1.2.0 \
--build 42 \
--signature "BASE64_SIGNATURE" \
--length 12345678 \
--min-os 26.0 \
--notes "Bug fixes and performance improvements"Your certificate isn't installed. Follow step 2 above.
Re-run xcrun notarytool store-credentials with the correct app-specific password. Regular Apple ID passwords don't work.
Clean the package cache and retry:
rm -rf build/SourcePackages
./scripts/build_release.sh --skip-notarizeAdd a custom background image at scripts/dmg_background.png (660x400px). Without it, the DMG uses the default macOS style.
The EdDSA signature doesn't match the public key in the app. Ensure:
SUPublicEDKeyinproject.ymlmatches the private key used to sign- The
SPARKLE_PRIVATE_KEYGitHub secret is correct and not truncated - You haven't regenerated keys without updating both the secret and Info.plist
- Verify
SUFeedURLis reachable (trycurl https://raw.githubusercontent.com/bitwize-ai/Logue/main/appcast.xml) - Check the appcast has an
<item>with a version higher than the running app - Ensure the enclosure
urlpoints at an existing GitHub Release asset - Sparkle only checks once per session by default — restart the app to re-check