fix: optimize notarize script

This commit is contained in:
zhayujie
2026-07-05 20:28:02 +08:00
parent 2cf521e57e
commit 60aebf41a8
2 changed files with 53 additions and 52 deletions

View File

@@ -1,16 +1,19 @@
#!/usr/bin/env bash
#
# Notarize + staple a macOS dmg with retry, so a transient notarytool
# status-polling timeout (NSURLErrorDomain -1001) doesn't throw away an
# otherwise-good signed build.
# Notarize + staple a macOS dmg.
#
# electron-builder's inline notarize aborts the whole build on the first poll
# timeout. Here we retry the submit-and-wait, and before each retry we check
# whether a previous submission already reached a terminal state (Accepted),
# so a flaky poll on an already-successful submission still passes.
# Submits ONCE and then polls that same submission id until it reaches a
# terminal state. Never resubmits: a previous version resubmitted on every
# wait timeout, which piled up duplicate "In Progress" submissions in Apple's
# queue and made everything slower.
#
# Notarization is fully automated on Apple's side (malware scan, no humans);
# most submissions finish in minutes, but large bundles or Apple-side backlog
# can take much longer, so we poll patiently instead of failing fast.
#
# Usage: notarize-dmg.sh <path-to-dmg>
# Requires env: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID
# Tunables: NOTARIZE_MAX_WAIT_MINUTES (default 180), NOTARIZE_POLL_SECONDS (default 60)
set -euo pipefail
DMG="${1:?usage: notarize-dmg.sh <dmg>}"
@@ -19,7 +22,8 @@ DMG="${1:?usage: notarize-dmg.sh <dmg>}"
: "${APPLE_APP_SPECIFIC_PASSWORD:?APPLE_APP_SPECIFIC_PASSWORD not set}"
: "${APPLE_TEAM_ID:?APPLE_TEAM_ID not set}"
MAX_ATTEMPTS="${NOTARIZE_MAX_ATTEMPTS:-5}"
MAX_WAIT_MINUTES="${NOTARIZE_MAX_WAIT_MINUTES:-180}"
POLL_SECONDS="${NOTARIZE_POLL_SECONDS:-60}"
auth=(
--apple-id "$APPLE_ID"
@@ -27,57 +31,49 @@ auth=(
--team-id "$APPLE_TEAM_ID"
)
# Check whether the most recent submission for this app is already Accepted.
# Used to short-circuit retries when the failure was only a status-poll timeout
# on a submission that Apple actually accepted.
latest_status_is_accepted() {
local out
out="$(xcrun notarytool history "${auth[@]}" --output-format json 2>/dev/null || true)"
# Grab the status of the first (most recent) history entry.
echo "$out" | python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
hist = data.get("history", [])
if hist and hist[0].get("status") == "Accepted":
sys.exit(0)
except Exception:
pass
sys.exit(1)
' 2>/dev/null
json_field() {
# json_field <key> — read a top-level string field from JSON on stdin.
python3 -c "import json,sys; print(json.load(sys.stdin).get('$1',''))" 2>/dev/null
}
echo "==> Notarizing: $DMG"
echo "==> Submitting for notarization: $DMG"
submit_out="$(xcrun notarytool submit "$DMG" "${auth[@]}" --no-wait --output-format json)"
submission_id="$(echo "$submit_out" | json_field id)"
attempt=1
if [ -z "$submission_id" ]; then
echo "::error::could not parse submission id from notarytool output"
echo "$submit_out"
exit 1
fi
echo "==> Submission id: $submission_id (uploaded OK, polling status...)"
deadline=$(( $(date +%s) + MAX_WAIT_MINUTES * 60 ))
while :; do
echo "==> notarytool submit (attempt ${attempt}/${MAX_ATTEMPTS})"
if xcrun notarytool submit "$DMG" "${auth[@]}" --wait --timeout 30m; then
echo "==> Notarization Accepted"
break
fi
# info can fail transiently (network); treat as unknown and keep polling.
status="$(xcrun notarytool info "$submission_id" "${auth[@]}" --output-format json 2>/dev/null | json_field status || true)"
echo "==> [$(date '+%H:%M:%S')] status: ${status:-<query failed, will retry>}"
echo "::warning::notarytool submit failed on attempt ${attempt}"
case "$status" in
Accepted)
echo "==> Notarization Accepted"
break
;;
Invalid|Rejected)
echo "::error::Notarization ${status}; fetching log for diagnostics"
xcrun notarytool log "$submission_id" "${auth[@]}" || true
exit 1
;;
esac
# The submit may have been accepted but the status poll timed out; verify.
if latest_status_is_accepted; then
echo "==> Latest submission already Accepted (poll timed out); continuing"
break
fi
if [ "$attempt" -ge "$MAX_ATTEMPTS" ]; then
echo "::error::Notarization failed after ${MAX_ATTEMPTS} attempts"
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "::error::Notarization still not finished after ${MAX_WAIT_MINUTES} minutes (id: $submission_id)."
echo "::error::NOT resubmitting. Check later with: xcrun notarytool info $submission_id"
exit 1
fi
sleep_s=$(( attempt * 30 ))
echo "==> Retrying in ${sleep_s}s..."
sleep "$sleep_s"
attempt=$(( attempt + 1 ))
sleep "$POLL_SECONDS"
done
echo "==> Stapling ticket into dmg"
# Staple has its own transient failures; retry a few times.
staple_attempt=1
while :; do
if xcrun stapler staple "$DMG"; then