Apple macOS CI/CD & Auto-Updater
Expert workflow for shipping signed, notarized macOS apps via GitHub Actions with automatic updates. Grounded in the Cometrans (native Swift) and Cometode (Electron) repos in this workspace.
Reference Projects
| App | Stack | Release trigger | Auto-update |
|---|---|---|---|
| Cometrans | Swift CLI → .app + DMG |
workflow_dispatch with version input |
Sparkle-ready build numbers (not wired yet) |
| Cometode | Electron + Svelte | Push tag v* |
electron-updater + latest-mac.yml |
Read the reference files when implementing:
- Cometrans pattern
- Cometode pattern
- GitHub secrets
- Auto-updater guide
Thinking Process
Step 1: Identify App Type
Goal: Pick the correct release and updater stack.
Decision Matrix:
| App type | Build tool | CI runner | Updater |
|---|---|---|---|
| Native Swift / SwiftPM | build_dmg.sh or Xcode |
macos-26 (pin Xcode via DEVELOPER_DIR) |
Sparkle + appcast |
| Electron + electron-builder | pnpm build:mac |
macos-latest |
electron-updater + GitHub Releases |
Decision Point: State "This is a [native|electron] app; updater will use [Sparkle|electron-updater]."
Step 2: Apple Developer Prerequisites
Goal: Ensure signing and notarization credentials exist before touching CI.
Checklist:
- Apple Developer Program membership
- Developer ID Application certificate (not Mac App Store)
-
.p12exported and base64-encoded for CI - App-specific password at appleid.apple.com (for notarytool)
- Team ID from developer.apple.com
Actions:
- Run
bash /mnt/skills/user/apple-mac-cicd/scripts/validate-secrets.shto audit local env - Configure GitHub repo secrets (see github-secrets.md)
Step 3: Local Build Script
Goal: One script produces reproducible signed artifacts locally and in CI.
Native (Cometrans pattern):
build_dmg.shbuilds Swift release, assembles.app, codesigns with--options runtime, creates DMGCFBundleVersionderived asmajor*10000 + minor*100 + patchfor Sparkle semver comparisonNOTARIZE=1triggersscripts/notarize.shviaxcrun notarytool submit --wait+stapler staple
Electron (Cometode pattern):
electron-builder.yml:hardenedRuntime: true,notarize: true,publish: github- mac targets: dmg + zip (zip required for electron-updater)
- Entitlements in
build/entitlements.mac.plist(JIT, unsigned executable memory for Electron)
Step 4: GitHub Actions Release Workflow
Goal: CI builds, signs, notarizes, and publishes to GitHub Releases.
Native workflow shape (Cometrans .github/workflows/release.yml):
preparejob — resolve version, create/push tagbuildjob — import P12 to ephemeral keychain, run./build_dmg.sh, upload artifactsreleasejob — download artifacts,softprops/action-gh-releasewith DMG + sha256
Electron workflow shape (Cometode .github/workflows/release.yml):
- Trigger on
push: tags: ['v*'] - Sync
package.jsonversion from tag pnpm build:macwith signing env vars- Upload
dist/*.dmg,dist/*.zip,dist/*.blockmap,dist/latest-mac.yml(critical for auto-update)
Required secrets by stack:
| Secret | Native | Electron |
|---|---|---|
APPLE_DEVELOPER_ID_P12_BASE64 |
✓ | — |
APPLE_DEVELOPER_ID_P12_PASSWORD |
✓ | — |
KEYCHAIN_PASSWORD |
✓ | — |
DEVELOPER_ID_APPLICATION |
✓ | — |
CSC_LINK |
— | ✓ (base64 p12) |
CSC_KEY_PASSWORD |
— | ✓ |
APPLE_ID |
✓ | ✓ |
APPLE_APP_SPECIFIC_PASSWORD |
✓ | ✓ |
APPLE_TEAM_ID |
✓ | ✓ |
Step 5: Auto-Updater Integration
Goal: Users receive updates without manual DMG downloads.
Electron (Cometode — fully wired):
electron-updaterreadslatest-mac.ymlfrom GitHub Releaseselectron-builder.ymlpublish.provider: githubmust match repo owner/name- Main process:
autoUpdater.autoDownload = true, check on startup + every 4h - Release workflow must attach
latest-mac.yml,.zip, and.blockmap - Only enable in production (
if (!is.dev))
Native (Cometrans — build-number ready, Sparkle pending):
build_dmg.shalready setsCFBundleVersionfor Sparkle comparison- To complete: add Sparkle SPM dep,
SUFeedURLin Info.plist, EdDSA signing key - CI must generate/sign appcast XML and publish alongside DMG on each release
- See auto-updater.md
Step 6: CI Workflow for PRs
Goal: Catch build breaks before release.
Native: ci.yml on push/PR — swift build -c release && swift test on macos-26
Electron: Add ci.yml with pnpm typecheck && pnpm build (no signing needed for CI)
Step 7: Release & Verify
Goal: Ship and confirm auto-update works end-to-end.
Release checklist:
- Bump version in source (
version.txtorpackage.json) - Tag
vX.Y.Z(Electron) or run workflow_dispatch (Cometrans) - CI completes signing + notarization
- GitHub Release contains all required assets
- Install previous version, confirm updater finds new release
-
spctl -a -vv -t installpasses on downloaded artifact
Verify notarization locally:
xcrun stapler validate Cometrans-1.0.0.dmg
# or
spctl -a -vv -t install dist/cometode-1.0.0-arm64.dmg
Usage
Validate secrets are configured locally
bash /mnt/skills/user/apple-mac-cicd/scripts/validate-secrets.sh
Scaffold a release workflow
bash /mnt/skills/user/apple-mac-cicd/scripts/scaffold-release-workflow.sh \
--type electron \
--app-name MyApp \
--output .github/workflows/release.yml
Arguments:
--type—nativeorelectron(required)--app-name— artifact stem, e.g.Cometrans(required)--output— workflow file path (default:.github/workflows/release.yml)
Present Results to User
When helping set up macOS CI/CD, report:
- App type and chosen updater
- Secrets still missing (run validate script output)
- Files created/modified with paths
- Release steps — how to trigger first release
- Auto-update status — wired or remaining steps
Troubleshooting
"No signing certificate configured" in CI
- Verify
APPLE_DEVELOPER_ID_P12_BASE64/CSC_LINKsecret is set and decodes to valid P12
Notarization fails with invalid credentials
- Regenerate app-specific password; confirm
APPLE_TEAM_IDmatches certificate
Gatekeeper blocks app despite CI success
- Ensure
--options runtimeon codesign (native) orhardenedRuntime: true(Electron) - Confirm stapler ran:
xcrun stapler validate <artifact>
electron-updater finds no updates
- Release must include
latest-mac.yml,.zip,.blockmap publish.owner/publish.repoin electron-builder.yml must match GitHub repo- App must be built in production mode (not dev)
Sparkle shows wrong version order
- Use numeric
CFBundleVersion(Cometrans formula:major*10000 + minor*100 + patch) - Never use semver strings in
CFBundleVersion