|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# release-notarize.sh - submit MicGuard.zip to Apple notarization, staple the |
| 4 | +# resulting ticket onto the .app, and re-zip so the published archive ships |
| 5 | +# with offline-verifiable notarization. |
| 6 | +# |
| 7 | +# Two authentication modes (pick one): |
| 8 | +# |
| 9 | +# 1. App Store Connect API key (recommended for CI): |
| 10 | +# --key PATH --key-id ID --issuer UUID |
| 11 | +# |
| 12 | +# 2. Keychain profile (recommended for local: run |
| 13 | +# `xcrun notarytool store-credentials NAME ...` once, then reference NAME): |
| 14 | +# --keychain-profile NAME |
| 15 | +# |
| 16 | +# notarytool's --wait flag polls until Apple returns a terminal status, so |
| 17 | +# this script does not implement its own polling loop. The submission log is |
| 18 | +# fetched on rejection so the failure reason ends up in CI output. |
| 19 | +# |
| 20 | +# Exit codes: |
| 21 | +# 0 notarized + stapled, zip refreshed |
| 22 | +# 1 notarization rejected by Apple, or stapler failed |
| 23 | +# 2 bad usage (missing/conflicting flags, file not found) |
| 24 | + |
| 25 | +set -euo pipefail |
| 26 | + |
| 27 | +usage() { |
| 28 | + cat <<'USAGE' |
| 29 | +Usage: |
| 30 | + release-notarize.sh --key PATH --key-id ID --issuer UUID [--app PATH] [--zip PATH] |
| 31 | + release-notarize.sh --keychain-profile NAME [--app PATH] [--zip PATH] |
| 32 | +
|
| 33 | +Submit a signed .app/.zip to Apple notarization, staple the ticket, and |
| 34 | +refresh the zip so the published archive carries the notarization offline. |
| 35 | +
|
| 36 | +Authentication (choose exactly one): |
| 37 | + --key PATH path to App Store Connect API key (.p8) |
| 38 | + --key-id ID 10-char Key ID from App Store Connect |
| 39 | + --issuer UUID issuer UUID from App Store Connect |
| 40 | + --keychain-profile NAME profile name set up via `notarytool store-credentials` |
| 41 | +
|
| 42 | +Optional: |
| 43 | + --app PATH path to the .app bundle (default: .build/MicGuard.app) |
| 44 | + --zip PATH path to the zip to submit (default: .build/MicGuard.zip) |
| 45 | + -h, --help show this help and exit |
| 46 | +
|
| 47 | +Examples: |
| 48 | + # CI / explicit credentials |
| 49 | + release-notarize.sh --key ./AuthKey_ABCDEF1234.p8 \ |
| 50 | + --key-id ABCDEF1234 --issuer 12345678-1234-1234-1234-123456789012 |
| 51 | +
|
| 52 | + # Local, after running `xcrun notarytool store-credentials MicGuard ...` |
| 53 | + release-notarize.sh --keychain-profile MicGuard |
| 54 | +USAGE |
| 55 | +} |
| 56 | + |
| 57 | +APP=".build/MicGuard.app" |
| 58 | +ZIP=".build/MicGuard.zip" |
| 59 | +KEY="" |
| 60 | +KEY_ID="" |
| 61 | +ISSUER="" |
| 62 | +KEYCHAIN_PROFILE="" |
| 63 | + |
| 64 | +while [[ $# -gt 0 ]]; do |
| 65 | + case "$1" in |
| 66 | + --app) APP="$2"; shift 2 ;; |
| 67 | + --zip) ZIP="$2"; shift 2 ;; |
| 68 | + --key) KEY="$2"; shift 2 ;; |
| 69 | + --key-id) KEY_ID="$2"; shift 2 ;; |
| 70 | + --issuer) ISSUER="$2"; shift 2 ;; |
| 71 | + --keychain-profile) KEYCHAIN_PROFILE="$2"; shift 2 ;; |
| 72 | + -h|--help) usage; exit 0 ;; |
| 73 | + *) echo "error: unknown flag: $1" >&2; usage >&2; exit 2 ;; |
| 74 | + esac |
| 75 | +done |
| 76 | + |
| 77 | +# Pick auth mode: keychain profile XOR (key + key-id + issuer). |
| 78 | +api_key_mode_count=0 |
| 79 | +[[ -n "$KEY" ]] && api_key_mode_count=$((api_key_mode_count + 1)) |
| 80 | +[[ -n "$KEY_ID" ]] && api_key_mode_count=$((api_key_mode_count + 1)) |
| 81 | +[[ -n "$ISSUER" ]] && api_key_mode_count=$((api_key_mode_count + 1)) |
| 82 | + |
| 83 | +if [[ -n "$KEYCHAIN_PROFILE" ]]; then |
| 84 | + if (( api_key_mode_count > 0 )); then |
| 85 | + echo "error: --keychain-profile cannot be combined with --key/--key-id/--issuer" >&2 |
| 86 | + usage >&2 |
| 87 | + exit 2 |
| 88 | + fi |
| 89 | + auth_args=(--keychain-profile "$KEYCHAIN_PROFILE") |
| 90 | +elif (( api_key_mode_count == 3 )); then |
| 91 | + [[ -f "$KEY" ]] || { echo "error: API key not found: $KEY" >&2; exit 2; } |
| 92 | + auth_args=(--key "$KEY" --key-id "$KEY_ID" --issuer "$ISSUER") |
| 93 | +elif (( api_key_mode_count == 0 )); then |
| 94 | + echo "error: missing authentication; pass --keychain-profile NAME or all of --key/--key-id/--issuer" >&2 |
| 95 | + usage >&2 |
| 96 | + exit 2 |
| 97 | +else |
| 98 | + echo "error: --key, --key-id, and --issuer must be passed together" >&2 |
| 99 | + usage >&2 |
| 100 | + exit 2 |
| 101 | +fi |
| 102 | + |
| 103 | +[[ -e "$APP" ]] || { echo "error: app bundle not found: $APP" >&2; exit 2; } |
| 104 | +[[ -f "$ZIP" ]] || { echo "error: zip not found: $ZIP" >&2; exit 2; } |
| 105 | + |
| 106 | +echo "==> submitting $ZIP to notarization (waiting for Apple)" |
| 107 | +submit_log=$(mktemp) |
| 108 | +trap 'rm -f "$submit_log"' EXIT |
| 109 | + |
| 110 | +if ! xcrun notarytool submit "$ZIP" \ |
| 111 | + "${auth_args[@]}" \ |
| 112 | + --wait \ |
| 113 | + --output-format plist \ |
| 114 | + > "$submit_log"; then |
| 115 | + echo "error: notarytool submit failed" >&2 |
| 116 | + cat "$submit_log" >&2 |
| 117 | + exit 1 |
| 118 | +fi |
| 119 | + |
| 120 | +status=$(/usr/libexec/PlistBuddy -c "Print :status" "$submit_log" 2>/dev/null || echo "") |
| 121 | +submission_id=$(/usr/libexec/PlistBuddy -c "Print :id" "$submit_log" 2>/dev/null || echo "") |
| 122 | + |
| 123 | +echo "==> notarytool status: ${status:-unknown} (submission ${submission_id:-unknown})" |
| 124 | + |
| 125 | +if [[ "$status" != "Accepted" ]]; then |
| 126 | + echo "error: notarization not accepted" >&2 |
| 127 | + if [[ -n "$submission_id" ]]; then |
| 128 | + echo "==> fetching submission log:" >&2 |
| 129 | + xcrun notarytool log "$submission_id" "${auth_args[@]}" >&2 || true |
| 130 | + fi |
| 131 | + exit 1 |
| 132 | +fi |
| 133 | + |
| 134 | +echo "==> stapling ticket onto $APP" |
| 135 | +xcrun stapler staple "$APP" |
| 136 | + |
| 137 | +echo "==> validating staple" |
| 138 | +xcrun stapler validate "$APP" |
| 139 | + |
| 140 | +echo "==> refreshing $ZIP with stapled bundle" |
| 141 | +zip_dir=$(dirname "$ZIP") |
| 142 | +zip_name=$(basename "$ZIP") |
| 143 | +app_name=$(basename "$APP") |
| 144 | +# Match the layout produced by `make zip` so the homebrew cask's `binary` |
| 145 | +# stanza pointing at `bin/mic-guard` keeps working after notarization. |
| 146 | +( |
| 147 | + cd "$zip_dir" |
| 148 | + rm -f "$zip_name" |
| 149 | + zip -ry "$zip_name" "$app_name" bin/mic-guard |
| 150 | +) |
| 151 | + |
| 152 | +echo "==> done: $ZIP is signed, notarized, stapled" |
0 commit comments