Skip to content

Ship a signed + notarized macOS release (no Gatekeeper warning on download) #1

Description

@yanyin1986

Goal: ship a downloadable macOS build that opens on a clean Mac without the "IPArchive is damaged and can't be opened. You should move it to the Trash." or "unidentified developer" dialogs.

That requires all three of: Developer ID signing → notarization → stapling. Missing any one of them brings the dialog back.

Prerequisite: an Apple Developer Program membership ($99/yr). A Developer ID certificate cannot be issued without it, and without that certificate notarization is impossible. Ad-hoc signing plus asking users to run xattr -dr com.apple.quarantine is exactly the experience this issue exists to avoid.

1. One-time setup

  • Join the Apple Developer Program
  • Create a Developer ID Application certificate (Xcode → Settings → Accounts). Note: not "Apple Distribution", which is for the Mac App Store.
  • Add the signing settings to project.yml:
    yaml DEVELOPMENT_TEAM: XXXXXXXXXX CODE_SIGN_IDENTITY: "Developer ID Application" ENABLE_HARDENED_RUNTIME: YES # already set
    ⚠️ Do not change PRODUCT_BUNDLE_IDENTIFIER — existing installs would lose their Keychain entry.
  • Add ExportOptions.plist with method = developer-id and the team ID
  • Store notarization credentials (requires an app-specific password from appleid.apple.com):
    bash xcrun notarytool store-credentials "IPArchive-NOTARY" \ --apple-id "<apple-id>" --team-id "XXXXXXXXXX" --password "xxxx-xxxx-xxxx-xxxx"

2. Per-release steps

  • Archive in Release configuration
    bash xcodegen generate xcodebuild -project IPAHelper.xcodeproj -scheme IPAHelper \ -configuration Release -archivePath build/IPArchive.xcarchive archive
  • Export with Developer ID
    bash xcodebuild -exportArchive -archivePath build/IPArchive.xcarchive \ -exportOptionsPlist ExportOptions.plist -exportPath build/export
  • Package for submission — must use ditto
    bash ditto -c -k --keepParent build/export/IPAHelper.app build/IPArchive.zip
  • Notarize
    bash xcrun notarytool submit build/IPArchive.zip --keychain-profile "IPArchive-NOTARY" --wait
  • Staple the ticket to the .app
    bash xcrun stapler staple build/export/IPAHelper.app
  • Re-package the stapled app — this is the artifact users download
    bash ditto -c -k --keepParent build/export/IPAHelper.app build/IPArchive-<version>.zip
  • Attach to a GitHub Release

3. Verification before publishing

  • xcrun stapler validate build/export/IPAHelper.appThe validate action worked!
  • spctl -a -vvv -t exec build/export/IPAHelper.appsource=Notarized Developer ID
  • codesign -dv --verbose=4 build/export/IPAHelper.appflags contains runtime
  • End-to-end: upload the zip, download it back through a browser (so it carries com.apple.quarantine), and confirm it opens on double-click with no dialog

4. Known pitfalls

Pitfall Consequence
Packaging with Finder "Compress" or zip -r Loses symlinks and extended attributes → "damaged, move to Trash" — the exact dialog this issue is about
Notarizing but not stapling Dialog reappears when the user is offline or Apple's service is slow
Building in Debug configuration Carries the get-task-allow entitlement → notarization is rejected
Shipping a DMG but only stapling the .app The DMG itself must also be signed, notarized and stapled
Missing --timestamp Notarization rejected (xcodebuild's export adds it automatically; manual codesign must pass it explicitly)

5. Follow-up: automate in CI

GitHub Actions provides free standard macOS runners for public repositories, so the whole flow above can run on tag push: build → notarize → create the GitHub Release → upload the artifact.

Secrets required: the Developer ID certificate as base64-encoded .p12 plus its password, DEVELOPMENT_TEAM, the Apple ID, and the app-specific password.

  • Decide: local release script (scripts/release.sh) vs. .github/workflows/release.yml

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions