Files
Joe MattielloandClaude Opus 5.5 e652c916ed ci(macos): sign and notarize every macOS build (#989) (#990)
* ci(macos): sign and notarize every macOS build (#989)

Move the keychain import / codesign / notarytool logic into the composite
action .github/actions/macos-sign (wrapping app/packaging/macos/codesign.sh)
and use it for: the app package (now also on same-repo PRs), the App macOS
arm64 binary, the macOS core dylibs, cue2cdi-macos-universal and the macOS
chdman/jagcd tools. Bundles are stapled; bare Mach-O files are notarized
but unstapled. A notarization failure warns off-tag and fails on a tag.
Fork PRs get no secrets and keep the ad-hoc signature plus the warning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(macos): pass app.yml only the five APPLE_* secrets, not inherit

SIGNPATH_API_TOKEN would otherwise ride along with sign: true and switch on
the Windows SignPath steps for every PR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-15 23:40:51 -04:00

129 lines
6.2 KiB
Bash
Executable File

#!/bin/bash
# Code signing + notarization helpers (used by .github/workflows/app-package.yml).
#
# codesign.sh import-cert create a temp keychain from the P12 secrets
# codesign.sh sign-app <app> [adhoc] sign inside-out (dylibs, then the bundle)
# codesign.sh sign-file <file> [entitlements.plist] sign one bare Mach-O (dylib, CLI tool)
# codesign.sh notarize <file> submit (zip/pkg), wait, fail on Invalid
# (VJ_NOTARY_FAIL_LEVEL=warning turns the ::error:: annotation into
# a ::warning::; VJ_NOTARY_TIMEOUT overrides the 45m wait)
# codesign.sh staple <file> staple the ticket (bundles / dmg / pkg only; not bare Mach-O)
#
# CI drives all of this through the composite action .github/actions/macos-sign.
#
# Environment (GitHub secrets mapped by the workflow; see app/packaging/README.md):
# APPLE_DEVELOPER_ID_P12_BASE64, APPLE_DEVELOPER_ID_P12_PASSWORD Developer ID Application cert
# APPLE_NOTARY_KEY_ID, APPLE_NOTARY_ISSUER_ID, APPLE_NOTARY_KEY_P8_BASE64 App Store Connect API key
# Local use (see local-package.sh): VJ_SIGN_IDENTITY (name or SHA-1), and for notarize either
# VJ_NOTARY_PROFILE or APPLE_NOTARY_KEY_ID + APPLE_NOTARY_ISSUER_ID + APPLE_NOTARY_KEY_PATH.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
kc_name=vj-signing.keychain-db
# VJ_SIGN_IDENTITY=- signs ad hoc through the SAME code path (hardened runtime, nested code
# first, entitlements) -- used to exercise the script without a certificate.
identity() {
[ -n "${VJ_SIGN_IDENTITY:-}" ] && { echo "$VJ_SIGN_IDENTITY"; return; }
security find-identity -v -p codesigning "$kc_name" | sed -n 's/.*"\(Developer ID Application:.*\)"$/\1/p' | head -1
}
case "${1:-}" in
import-cert)
: "${APPLE_DEVELOPER_ID_P12_BASE64:?}" "${APPLE_DEVELOPER_ID_P12_PASSWORD:?}"
tmp="${RUNNER_TEMP:-/tmp}"
pw="$(uuidgen)"
echo "$APPLE_DEVELOPER_ID_P12_BASE64" | base64 --decode > "$tmp/devid.p12"
security create-keychain -p "$pw" "$kc_name"
security set-keychain-settings -lut 21600 "$kc_name"
security unlock-keychain -p "$pw" "$kc_name"
security import "$tmp/devid.p12" -k "$kc_name" -P "$APPLE_DEVELOPER_ID_P12_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s -k "$pw" "$kc_name" >/dev/null
security list-keychains -d user -s "$kc_name" login.keychain-db
rm -f "$tmp/devid.p12"
id="$(identity)"
[ -n "$id" ] || { echo "no 'Developer ID Application' identity in the P12" >&2; exit 1; }
echo "imported identity: $id"
;;
sign-app)
app="$2"; mode="${3:-}"
if [ "$mode" = adhoc ]; then
# Unsigned-by-us build (forks, PRs): an ad-hoc signature is still REQUIRED on
# Apple Silicon, or the app reports "damaged". Gatekeeper still refuses it
# (unidentified developer) until the user opens it via right-click > Open.
codesign --force --deep -s - "$app"
codesign --verify --deep --strict "$app"
exit 0
fi
id="$(identity)"; [ -n "$id" ] || { echo "no identity; run import-cert" >&2; exit 1; }
ts=--timestamp; [ "$id" = - ] && ts=--timestamp=none
# Inside-out: every nested Mach-O first, then the bundle with the entitlements.
while IFS= read -r -d '' f; do
if file -b "$f" | grep -q 'Mach-O' && [ "$f" != "$app/Contents/MacOS/virtualjaguar" ]; then
codesign --force --options runtime "$ts" --sign "$id" "$f"
fi
done < <(find "$app/Contents" -type f -print0)
codesign --force --options runtime "$ts" --entitlements "$here/entitlements.plist" \
--sign "$id" "$app"
codesign --verify --deep --strict --verbose=2 "$app"
codesign -dv --entitlements - "$app" 2>&1 | sed -n '1,25p'
;;
sign-file)
# A bare Mach-O (core dylib, CLI tool): hardened runtime + secure timestamp, same identity.
# It cannot be stapled; notarize a zip that holds it and Gatekeeper checks the ticket online.
f="$2"; ent="${3:-}"
[ -f "$f" ] || { echo "sign-file: $f is not a file" >&2; exit 1; }
id="$(identity)"; [ -n "$id" ] || { echo "no identity; run import-cert" >&2; exit 1; }
ts=--timestamp; [ "$id" = - ] && ts=--timestamp=none
if [ -n "$ent" ]; then
codesign --force --options runtime "$ts" --entitlements "$ent" --sign "$id" "$f"
else
codesign --force --options runtime "$ts" --sign "$id" "$f"
fi
codesign --verify --strict --verbose=2 "$f"
;;
notarize)
# Credentials, first match wins (CI uses the third; local builds the first two):
# VJ_NOTARY_PROFILE a `xcrun notarytool store-credentials` profile
# APPLE_NOTARY_KEY_ID/_ISSUER_ID/_KEY_PATH App Store Connect API key, .p8 on disk
# APPLE_NOTARY_KEY_ID/_ISSUER_ID/_KEY_P8_BASE64 the same key, base64 (GitHub secret)
tmp="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
p8=""; made_p8=""
if [ -n "${VJ_NOTARY_PROFILE:-}" ]; then
auth=(--keychain-profile "$VJ_NOTARY_PROFILE")
else
: "${APPLE_NOTARY_KEY_ID:?}" "${APPLE_NOTARY_ISSUER_ID:?}"
if [ -n "${APPLE_NOTARY_KEY_PATH:-}" ]; then
p8="$APPLE_NOTARY_KEY_PATH"
[ -f "$p8" ] || { echo "APPLE_NOTARY_KEY_PATH is not a file" >&2; exit 1; }
else
: "${APPLE_NOTARY_KEY_P8_BASE64:?}"
p8="$tmp/notary.p8"; made_p8=1
echo "$APPLE_NOTARY_KEY_P8_BASE64" | base64 --decode > "$p8"
fi
auth=(--key "$p8" --key-id "$APPLE_NOTARY_KEY_ID" --issuer "$APPLE_NOTARY_ISSUER_ID")
fi
log="$tmp/notarytool.json"
set +e
xcrun notarytool submit "$2" "${auth[@]}" --wait --timeout "${VJ_NOTARY_TIMEOUT:-45m}" --output-format json > "$log"
rc=$?
set -e
cat "$log"
status="$(sed -n 's/.*"status" *: *"\([^"]*\)".*/\1/p' "$log" | head -1)"
if [ "$rc" -ne 0 ] || [ "$status" != "Accepted" ]; then
subid="$(sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p' "$log" | head -1)"
[ -n "$subid" ] && xcrun notarytool log "$subid" "${auth[@]}" || true
[ -z "$made_p8" ] || rm -f "$p8"
echo "::${VJ_NOTARY_FAIL_LEVEL:-error}::notarization of $2 ended '$status'"
exit 1
fi
[ -z "$made_p8" ] || rm -f "$p8"
;;
staple)
xcrun stapler staple "$2"
xcrun stapler validate "$2"
;;
*)
sed -n '2,12p' "$0"; exit 2 ;;
esac