How to ship a signed, notarized, auto-updating DMG.
This document is the prerequisite list and runbook for actually shipping. Most of it is one-time setup; once the secrets are in place, releases are git tag v0.1.1 && git push --tags.
- Apple Developer account - $99 USD/year. https://developer.apple.com/
- Developer ID Application certificate - Xcode → Settings → Accounts → "Manage Certificates" →
+→ Developer ID Application. Export from Keychain Access as a.p12with a strong password. - App-specific password for notarization - sign in to https://appleid.apple.com → "Sign-In and Security" → "App-Specific Passwords" → generate one. Save it.
- Team ID - at https://developer.apple.com/account → Membership Details (10-char alphanumeric).
-
A public repo at
github.com/<owner>/<repo>matching thepublishblock of electron-builder.yml. (Currentlyjainath/devharbor- change it there if needed.) -
GitHub Actions enabled for that repo.
-
GitHub Secrets, set under Settings → Secrets and variables → Actions:
Secret Value MAC_CERTIFICATE_P12base64 -i Cert.p12 | pbcopy, then pasteMAC_CERTIFICATE_PASSWORDthe password you set on export APPLE_IDyour Apple developer email APPLE_APP_SPECIFIC_PASSWORDthe app-specific password from step 3 APPLE_TEAM_IDthe 10-char Team ID from step 4
GITHUB_TOKEN is provided automatically by Actions; nothing to set.
Drop a build/icon.icns (1024×1024 source preferred). Without one, electron-builder falls back to its default and the DMG looks unprofessional. Quick generation from a PNG:
mkdir build/icon.iconset
sips -z 16 16 icon.png --out build/icon.iconset/icon_16x16.png
sips -z 32 32 icon.png --out build/icon.iconset/icon_16x16@2x.png
sips -z 32 32 icon.png --out build/icon.iconset/icon_32x32.png
sips -z 64 64 icon.png --out build/icon.iconset/icon_32x32@2x.png
sips -z 128 128 icon.png --out build/icon.iconset/icon_128x128.png
sips -z 256 256 icon.png --out build/icon.iconset/icon_128x128@2x.png
sips -z 256 256 icon.png --out build/icon.iconset/icon_256x256.png
sips -z 512 512 icon.png --out build/icon.iconset/icon_256x256@2x.png
sips -z 512 512 icon.png --out build/icon.iconset/icon_512x512.png
cp icon.png build/icon.iconset/icon_512x512@2x.png
iconutil -c icns build/icon.iconset -o build/icon.icns
rm -rf build/icon.iconsetpnpm install
pnpm pack:macOutputs dist/devharbor-<version>-arm64.dmg (+ x64). The afterSign hook detects no credentials and skips notarization. Gatekeeper will warn on first run; right-click → Open to bypass.
Set the env vars locally and re-run pnpm pack:mac:
export APPLE_ID="you@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="ABCDE12345"
export CSC_LINK="/abs/path/to/cert.p12"
export CSC_KEY_PASSWORD="your-export-password"
pnpm pack:macNotarization stapling takes 2-15 minutes; you'll see the [notarize] done. line when it's complete. Verify:
spctl --assess --verbose dist/mac/App\ Manager.app
# → /path: accepted; source=Notarized Developer ID# Bump version in package.json, commit:
git add package.json && git commit -m "release: v0.1.1"
# Tag and push:
git tag v0.1.1
git push origin main --tagsGitHub Actions picks up the tag, runs .github/workflows/release.yml, builds + signs + notarizes both arches, and publishes to GitHub Releases. electron-updater picks up the new release on the next launch of any v0.1.0 install and prompts via the UpdateBanner.
- App boots.
Updater.start()runs ifauto_updatesetting is true. autoUpdater.checkForUpdates()fetcheshttps://github.com/<owner>/<repo>/releases/latest/download/latest-mac.yml.- If a newer version exists,
update-availablefires → UpdateBanner shows "Downloading…". - Download completes →
update-downloadedfires → UpdateBanner shows "Update vX.Y.Z ready · Quit & install". - User clicks →
update:installIPC →autoUpdater.quitAndInstall(true, true)→ app restarts on the new version.
The yaml file (latest-mac.yml) and the artifacts (.dmg, .zip, the .blockmap for delta updates) are all published in the same Release.
| Symptom | Likely cause | Fix |
|---|---|---|
| "App is damaged and can't be opened" | Unsigned/unnotarized download | Either right-click → Open, or finish notarization |
Notarization fails with Invalid credentials |
App-specific password not set, or set on the wrong Apple ID | Regenerate at appleid.apple.com, update the secret |
electron-updater logs 404 latest-mac.yml |
The Release isn't published (or repo publish config is wrong) |
Check the release exists in GitHub and the owner/repo in electron-builder.yml |
| Auto-update silently does nothing in dev | Expected - Skip checkForUpdates because application is not packed |
Test updates against a packaged DMG, not pnpm dev |
NODE_MODULE_VERSION mismatch on launch |
Native deps rebuilt against wrong ABI | pnpm rebuild runs electron-rebuild for better-sqlite3 + node-pty |
ModuleNotFoundError: No module named 'distutils' during pack |
Python 3.12 removed distutils from stdlib; bundled node-gyp 9.x still imports it |
python3 -m pip install --user --break-system-packages setuptools (or use a venv with setuptools installed) |
cannot find valid "Developer ID Application" identity |
No signing certificate in the Keychain (local unsigned build) | Expected for local dev; ship with cert in CI per the secrets table above |
| x64 arch missing from DMG | Default config builds arm64 only | CI workflow can add --x64 if both arches are needed; expect node-gyp issues to resurface for x64 on Apple Silicon |
Phase 5 acceptance (from 05-roadmap.md)
- Download DMG → drag to Applications → first launch passes Gatekeeper with no warnings
- App auto-updates from v0.1.0 → v0.1.1 with no user intervention beyond a quit & relaunch
Both checked when the first signed + notarized release lands in the GitHub feed and a v0.1.0 install can pick it up.