Files
nucleic/scripts/notarize.sh
T
NucleicandClaude Opus 4.8 a5cf327bd3 notarize: build notarytool auth args without mapfile (macOS bash 3.2 compat)
mapfile is a bash 4 builtin; macOS ships bash 3.2, so 'mapfile -d "" AUTH < <(auth_args)' died with 'mapfile: command not found' (exit 127) at the notarize step. Build the AUTH array directly instead — which also fixes a latent bug: the old auth_args ran inside a process-substitution subshell, so its 'exit 1' on missing creds couldn't actually abort the script.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-26 18:23:23 -07:00

87 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Notarize + staple a macOS artifact (a .app or a .dmg) with Apple's notary service.
#
# scripts/notarize.sh <path-to-.app-or-.dmg>
#
# Notarization is Apple scanning the signed artifact and issuing a "ticket" that
# Gatekeeper trusts; stapling attaches that ticket so the artifact is trusted even
# offline. It REQUIRES the artifact to already be signed with a Developer ID identity,
# the hardened runtime, and a secure timestamp (scripts/package-app.sh does this when
# NUCLEIC_SIGN_ID is a real identity).
#
# notarytool needs a container, so a bare .app is zipped for submission, then the .app
# itself is stapled. A .dmg is submitted and stapled directly.
#
# Auth — two ways, resolved in this order (the API-key form is what CI uses):
#
# 1. Stored notary profile (recommended for local use). Create it once with:
# xcrun notarytool store-credentials nucleic-notary \
# --key AuthKey_XXXX.p8 --key-id <KeyID> --issuer <IssuerID>
# then run with NUCLEIC_NOTARY_PROFILE=nucleic-notary (the default below).
#
# 2. App Store Connect API key, passed by env (no keychain needed — for CI):
# NUCLEIC_ASC_KEY_PATH=/path/AuthKey_XXXX.p8
# NUCLEIC_ASC_KEY_ID=<KeyID>
# NUCLEIC_ASC_ISSUER_ID=<IssuerID>
set -euo pipefail
ARTIFACT="${1:?usage: notarize.sh <path-to-.app-or-.dmg>}"
[ -e "$ARTIFACT" ] || { echo "notarize: no such path: $ARTIFACT" >&2; exit 2; }
PROFILE="${NUCLEIC_NOTARY_PROFILE:-nucleic-notary}"
# Build the notarytool auth arguments from whichever credential is available. Built straight into
# an array (no `mapfile` — that's a bash 4 builtin and macOS ships bash 3.2; and no function +
# process substitution, so the `exit 1` below actually aborts the script rather than a subshell).
AUTH=()
if [ -n "${NUCLEIC_ASC_KEY_PATH:-}" ] && [ -n "${NUCLEIC_ASC_KEY_ID:-}" ] && [ -n "${NUCLEIC_ASC_ISSUER_ID:-}" ]; then
AUTH=(--key "$NUCLEIC_ASC_KEY_PATH" --key-id "$NUCLEIC_ASC_KEY_ID" --issuer "$NUCLEIC_ASC_ISSUER_ID")
elif xcrun notarytool history --keychain-profile "$PROFILE" >/dev/null 2>&1; then
AUTH=(--keychain-profile "$PROFILE")
else
cat >&2 <<EOF
notarize: no notary credentials found.
• Local: create a stored profile once, then set NUCLEIC_NOTARY_PROFILE (default 'nucleic-notary'):
xcrun notarytool store-credentials nucleic-notary --key AuthKey_XXXX.p8 --key-id <KeyID> --issuer <IssuerID>
• CI: export NUCLEIC_ASC_KEY_PATH / NUCLEIC_ASC_KEY_ID / NUCLEIC_ASC_ISSUER_ID
EOF
exit 1
fi
submit() { # submit <container> — submits and waits for Apple's verdict
echo "▸ Submitting $(basename "$1") to Apple notary service (this can take a few minutes)…"
xcrun notarytool submit "$1" "${AUTH[@]}" --wait
}
case "$ARTIFACT" in
*.app)
ZIP="${TMPDIR:-/tmp}/$(basename "$ARTIFACT" .app)-notarize.zip"
echo "▸ Zipping app for submission → $ZIP"
/usr/bin/ditto -c -k --keepParent "$ARTIFACT" "$ZIP"
submit "$ZIP"
rm -f "$ZIP"
echo "▸ Stapling ticket to $ARTIFACT"
xcrun stapler staple "$ARTIFACT"
;;
*.dmg|*.pkg)
submit "$ARTIFACT"
echo "▸ Stapling ticket to $ARTIFACT"
xcrun stapler staple "$ARTIFACT"
;;
*)
echo "notarize: unsupported artifact (expected .app, .dmg, or .pkg): $ARTIFACT" >&2
exit 2
;;
esac
echo "▸ Verifying Gatekeeper acceptance"
case "$ARTIFACT" in
*.app) spctl --assess --type exec --verbose=2 "$ARTIFACT" ;;
*.dmg) spctl --assess --type open --context context:primary-signature --verbose=2 "$ARTIFACT" ;;
esac
xcrun stapler validate "$ARTIFACT"
echo "✓ Notarized + stapled: $ARTIFACT"