Skip to content

Commit ab17c52

Browse files
Sign and notarize macOS app in CI
Import the Developer ID cert into a temporary keychain in build-macos.yml, switch the package step from --no-sign to --notarize, and add credential-based notarization to package-macos.sh (NOTARY_APPLE_ID/PASSWORD/TEAM_ID env vars) for the CI case where no local keychain profile exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent be008e0 commit ab17c52

4 files changed

Lines changed: 100 additions & 7 deletions

File tree

.github/workflows/build-macos.yml

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,62 @@ jobs:
149149
mkdir -p ../build/macos/Build/Products/Release
150150
cp -R "$DERIVED_APP" ../build/macos/Build/Products/Release/
151151
152+
- name: Import Developer ID certificate
153+
env:
154+
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
155+
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
156+
MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
157+
run: |
158+
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
159+
CERT_PATH="$RUNNER_TEMP/developer-id.p12"
160+
161+
echo "$MACOS_CERTIFICATE" | base64 --decode > "$CERT_PATH"
162+
163+
# Create a dedicated, unlocked keychain just for this build
164+
security create-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
165+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
166+
security unlock-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
167+
168+
# Import the Developer ID Application cert + private key
169+
security import "$CERT_PATH" -P "$MACOS_CERTIFICATE_PASSWORD" \
170+
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
171+
172+
# Allow codesign to use the key without an interactive prompt
173+
security set-key-partition-list -S apple-tool:,apple:,codesign: \
174+
-s -k "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
175+
176+
# Make this keychain the default + only one in the search list so
177+
# package-macos.sh's `security find-identity` resolves the identity
178+
security list-keychains -d user -s "$KEYCHAIN_PATH"
179+
security default-keychain -s "$KEYCHAIN_PATH"
180+
181+
rm -f "$CERT_PATH"
182+
183+
echo "Signing identities available:"
184+
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
185+
152186
- name: Package release (arm64)
153187
if: ${{ inputs.arch == 'arm64' || inputs.arch == 'both' }}
188+
env:
189+
NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
190+
NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
191+
NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
154192
run: |
155-
./Scripts/package-macos.sh --version "${{ inputs.version }}" --arch arm64 --skip-build --no-sign
193+
./Scripts/package-macos.sh --version "${{ inputs.version }}" --arch arm64 --skip-build --notarize
156194
157195
- name: Package release (x64)
158196
if: ${{ inputs.arch == 'x64' || inputs.arch == 'both' }}
197+
env:
198+
NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
199+
NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
200+
NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
201+
run: |
202+
./Scripts/package-macos.sh --version "${{ inputs.version }}" --arch x64 --skip-build --notarize
203+
204+
- name: Clean up signing keychain
205+
if: always()
159206
run: |
160-
./Scripts/package-macos.sh --version "${{ inputs.version }}" --arch x64 --skip-build --no-sign
207+
security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true
161208
162209
- name: Upload artifacts
163210
uses: actions/upload-artifact@v4

CLAUDE.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,11 +490,45 @@ Prerequisites: draft release must exist for the tag, `gh` CLI authenticated.
490490
5. **Test** — fresh install + upgrade test
491491
6. **Create GitHub releases** — deps release first (if changed, tag `deps-vX.Y.Z`), then app release (tag `vX.Y.Z`)
492492

493+
### macOS Code Signing & Notarization
494+
495+
The macOS app is signed with a **Developer ID Application** certificate and notarized by Apple so it launches without Gatekeeper warnings. Signing happens **in CI** (`build-macos.yml`) — the runner imports the cert into a temporary keychain, then `package-macos.sh --notarize` signs every Mach-O inner-out (dylibs → frameworks → `.so` → worker → app), signs the DMG, submits to the notary service, and staples the ticket. `ci-build-and-release.sh` just downloads the finished signed/notarized DMG and uploads it — no local signing step.
496+
497+
`package-macos.sh` also works locally: with a Developer ID cert in your keychain it signs, and `--notarize` uses a stored `notarytool` keychain profile (default name `VapourBox`). In CI there is no keychain profile, so it falls back to `NOTARY_APPLE_ID` / `NOTARY_PASSWORD` / `NOTARY_TEAM_ID` env vars. Use `--no-sign` for ad-hoc local test builds.
498+
499+
**Required GitHub repo secrets** (Settings → Secrets and variables → Actions):
500+
501+
| Secret | Value |
502+
|--------|-------|
503+
| `MACOS_CERTIFICATE` | Base64 of the exported Developer ID Application `.p12` |
504+
| `MACOS_CERTIFICATE_PASSWORD` | Password set when exporting the `.p12` |
505+
| `MACOS_KEYCHAIN_PASSWORD` | Any random string (temp keychain password) |
506+
| `MACOS_NOTARY_APPLE_ID` | Apple ID email for notarization |
507+
| `MACOS_NOTARY_PASSWORD` | App-specific password from appleid.apple.com |
508+
| `MACOS_NOTARY_TEAM_ID` | `PMXZ63S6YG` |
509+
510+
**One-time setup** (export the cert and load secrets via `gh`):
511+
512+
```bash
513+
# 1. Export the Developer ID Application cert + private key from Keychain Access
514+
# (right-click the identity → Export → .p12, set a password) to ~/devid.p12
515+
# 2. Base64-encode and store all secrets:
516+
gh secret set MACOS_CERTIFICATE < <(base64 -i ~/devid.p12)
517+
gh secret set MACOS_CERTIFICATE_PASSWORD # paste the .p12 export password
518+
gh secret set MACOS_KEYCHAIN_PASSWORD # paste any random string
519+
gh secret set MACOS_NOTARY_APPLE_ID # paste your Apple ID email
520+
gh secret set MACOS_NOTARY_PASSWORD # paste app-specific password
521+
gh secret set MACOS_NOTARY_TEAM_ID # paste PMXZ63S6YG
522+
rm ~/devid.p12
523+
```
524+
525+
Create the app-specific password at appleid.apple.com → Sign-In and Security → App-Specific Passwords. The Developer ID cert expires **2027-02-01**; re-export and update `MACOS_CERTIFICATE` before then.
526+
493527
### Cross-Platform Notes
494528

495529
- Flutter Windows can't build on macOS — use GitHub Actions
496530
- Windows deps can be zipped on macOS if `deps/windows-x64/` exists
497-
- CI builds are ad-hoc signed; sign locally for distribution
531+
- The macOS CI build (`build-macos.yml`) signs with the Developer ID cert and notarizes — see "macOS Code Signing & Notarization" below. Windows CI builds remain unsigned.
498532

499533
### Dependency Version History
500534

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Get the latest release for your platform:
3939
2. Open the DMG and drag **VapourBox** to your Applications folder
4040
3. On first launch, VapourBox will automatically download its processing dependencies (~180 MB)
4141

42-
> **Note**: You may need to right-click and choose "Open" the first time, since the app is not notarized.
42+
> **Note**: VapourBox is signed with an Apple Developer ID and notarized, so it opens normally — just drag it to Applications and launch.
4343
4444
### Windows
4545

Scripts/package-macos.sh

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,21 @@ if $NOTARIZE && ! $NO_SIGN; then
368368
echo "[$STEP/$TOTAL_STEPS] Notarizing with Apple..."
369369
echo " Submitting to notary service (this may take several minutes)..."
370370

371-
xcrun notarytool submit "$DMG_FILE" \
372-
--keychain-profile "$KEYCHAIN_PROFILE" \
373-
--wait
371+
# CI uses credentials passed via env vars (no local keychain profile exists on
372+
# the runner); locally we fall back to a stored notarytool keychain profile.
373+
if [ -n "$NOTARY_APPLE_ID" ] && [ -n "$NOTARY_PASSWORD" ]; then
374+
echo " Using Apple ID credentials from environment ($NOTARY_APPLE_ID)"
375+
xcrun notarytool submit "$DMG_FILE" \
376+
--apple-id "$NOTARY_APPLE_ID" \
377+
--password "$NOTARY_PASSWORD" \
378+
--team-id "${NOTARY_TEAM_ID:-$TEAM_ID}" \
379+
--wait
380+
else
381+
echo " Using notarytool keychain profile: $KEYCHAIN_PROFILE"
382+
xcrun notarytool submit "$DMG_FILE" \
383+
--keychain-profile "$KEYCHAIN_PROFILE" \
384+
--wait
385+
fi
374386

375387
echo " Stapling notarization ticket..."
376388
xcrun stapler staple "$DMG_FILE"

0 commit comments

Comments
 (0)