From 02787b567467451a63e870c24066f8f7bb3ca7f1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 21 Dec 2025 12:33:45 +0100 Subject: [PATCH] build(mac): add notarize flow for release artifacts --- docs/mac/release.md | 13 +++++++ docs/mac/signing.md | 1 + scripts/codesign-mac-app.sh | 24 +++++++++++- scripts/notarize-mac-artifact.sh | 65 ++++++++++++++++++++++++++++++++ scripts/package-mac-dist.sh | 13 +++++++ 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100755 scripts/notarize-mac-artifact.sh diff --git a/docs/mac/release.md b/docs/mac/release.md index bb1c4b5c6..4e110cd62 100644 --- a/docs/mac/release.md +++ b/docs/mac/release.md @@ -12,6 +12,7 @@ This app now ships Sparkle auto-updates. Release builds must be Developer ID–s ## Prereqs - Developer ID Application cert installed (`Developer ID Application: Peter Steinberger (Y5PE65HELJ)` is expected). - Sparkle private key path set in the environment as `SPARKLE_PRIVATE_KEY_FILE`; key lives in `/Users/steipete/Library/CloudStorage/Dropbox/Backup/Sparkle` (same key as Trimmy; public key baked into Info.plist). +- Notary credentials (keychain profile or API key) for `xcrun notarytool` if you want Gatekeeper-safe DMG/zip distribution. - `pnpm` deps installed (`pnpm install --config.node-linker=hoisted`). - Sparkle tools are fetched automatically via SwiftPM at `apps/macos/.build/artifacts/sparkle/Sparkle/bin/` (`sign_update`, `generate_appcast`, etc.). @@ -31,6 +32,18 @@ ditto -c -k --sequesterRsrc --keepParent dist/Clawdis.app dist/Clawdis-0.1.0.zip # Optional: also build a styled DMG for humans (drag to /Applications) scripts/create-dmg.sh dist/Clawdis.app dist/Clawdis-0.1.0.dmg +# Recommended: build + notarize/staple zip + DMG +# First, create a keychain profile once: +# xcrun notarytool store-credentials "clawdis-notary" \ +# --apple-id "" --team-id "" --password "" +NOTARIZE=1 NOTARYTOOL_PROFILE=clawdis-notary \ +BUNDLE_ID=com.steipete.clawdis \ +APP_VERSION=0.1.0 \ +APP_BUILD=0.1.0 \ +BUILD_CONFIG=release \ +SIGN_IDENTITY="Developer ID Application: Peter Steinberger (Y5PE65HELJ)" \ +scripts/package-mac-dist.sh + # Optional: ship dSYM alongside the release ditto -c -k --keepParent apps/macos/.build/release/Clawdis.app.dSYM dist/Clawdis-0.1.0.dSYM.zip ``` diff --git a/docs/mac/signing.md b/docs/mac/signing.md index 02e68807e..6837216c0 100644 --- a/docs/mac/signing.md +++ b/docs/mac/signing.md @@ -10,6 +10,7 @@ This app is usually built from `scripts/package-mac-app.sh`, which now: - sets a stable debug bundle identifier: `com.steipete.clawdis.debug` - writes the Info.plist with that bundle id (override via `BUNDLE_ID=...`) - calls `scripts/codesign-mac-app.sh` to sign the main binary, bundled CLI, and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). Defaults to ad‑hoc; set `SIGN_IDENTITY="Developer ID Application: …"` to use a real cert. +- uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds). - injects build metadata into Info.plist: `ClawdisBuildTimestamp` (UTC) and `ClawdisGitCommit` (short hash) so the About pane can show build, git, and debug/release channel. - reads `SIGN_IDENTITY` from the environment. Add `export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"` (or your Developer ID Application cert) to your shell rc to always sign with your cert; otherwise signing falls back to ad‑hoc. diff --git a/scripts/codesign-mac-app.sh b/scripts/codesign-mac-app.sh index 1d82d2798..7705e1721 100755 --- a/scripts/codesign-mac-app.sh +++ b/scripts/codesign-mac-app.sh @@ -3,6 +3,7 @@ set -euo pipefail APP_BUNDLE="${1:-dist/Clawdis.app}" IDENTITY="${SIGN_IDENTITY:-}" +TIMESTAMP_MODE="${CODESIGN_TIMESTAMP:-auto}" ENT_TMP_BASE=$(mktemp -t clawdis-entitlements-base) ENT_TMP_APP=$(mktemp -t clawdis-entitlements-app) ENT_TMP_APP_BASE=$(mktemp -t clawdis-entitlements-app-base) @@ -47,6 +48,25 @@ fi echo "Using signing identity: $IDENTITY" +timestamp_arg="--timestamp=none" +case "$TIMESTAMP_MODE" in + 1|on|yes|true) + timestamp_arg="--timestamp" + ;; + 0|off|no|false) + timestamp_arg="--timestamp=none" + ;; + auto) + if [[ "$IDENTITY" == *"Developer ID Application"* ]]; then + timestamp_arg="--timestamp" + fi + ;; + *) + echo "ERROR: Unknown CODESIGN_TIMESTAMP value: $TIMESTAMP_MODE (use auto|on|off)" >&2 + exit 1 + ;; +esac + cat > "$ENT_TMP_BASE" <<'PLIST' @@ -118,12 +138,12 @@ xattr -cr "$APP_BUNDLE" 2>/dev/null || true sign_item() { local target="$1" local entitlements="$2" - codesign --force --options runtime --timestamp=none --entitlements "$entitlements" --sign "$IDENTITY" "$target" + codesign --force --options runtime "$timestamp_arg" --entitlements "$entitlements" --sign "$IDENTITY" "$target" } sign_plain_item() { local target="$1" - codesign --force --options runtime --timestamp=none --sign "$IDENTITY" "$target" + codesign --force --options runtime "$timestamp_arg" --sign "$IDENTITY" "$target" } # Sign main binary diff --git a/scripts/notarize-mac-artifact.sh b/scripts/notarize-mac-artifact.sh new file mode 100755 index 000000000..083800fa5 --- /dev/null +++ b/scripts/notarize-mac-artifact.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Notarize a macOS artifact (zip/dmg/pkg) and optionally staple the app bundle. +# +# Usage: +# STAPLE_APP_PATH=dist/Clawdis.app scripts/notarize-mac-artifact.sh +# +# Auth (pick one): +# NOTARYTOOL_PROFILE keychain profile created via `xcrun notarytool store-credentials` +# NOTARYTOOL_KEY path to App Store Connect API key (.p8) +# NOTARYTOOL_KEY_ID API key ID +# NOTARYTOOL_ISSUER API issuer ID + +ARTIFACT="${1:-}" +STAPLE_APP_PATH="${STAPLE_APP_PATH:-}" + +if [[ -z "$ARTIFACT" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi +if [[ ! -e "$ARTIFACT" ]]; then + echo "Error: artifact not found: $ARTIFACT" >&2 + exit 1 +fi + +if ! command -v xcrun >/dev/null 2>&1; then + echo "Error: xcrun not found; install Xcode command line tools." >&2 + exit 1 +fi + +auth_args=() +if [[ -n "${NOTARYTOOL_PROFILE:-}" ]]; then + auth_args+=(--keychain-profile "$NOTARYTOOL_PROFILE") +elif [[ -n "${NOTARYTOOL_KEY:-}" && -n "${NOTARYTOOL_KEY_ID:-}" && -n "${NOTARYTOOL_ISSUER:-}" ]]; then + auth_args+=(--key "$NOTARYTOOL_KEY" --key-id "$NOTARYTOOL_KEY_ID" --issuer "$NOTARYTOOL_ISSUER") +else + echo "Error: Notary auth missing. Set NOTARYTOOL_PROFILE or NOTARYTOOL_KEY/NOTARYTOOL_KEY_ID/NOTARYTOOL_ISSUER." >&2 + exit 1 +fi + +echo "🧾 Notarizing: $ARTIFACT" +xcrun notarytool submit "$ARTIFACT" "${auth_args[@]}" --wait + +case "$ARTIFACT" in + *.dmg|*.pkg) + echo "📌 Stapling artifact: $ARTIFACT" + xcrun stapler staple "$ARTIFACT" + xcrun stapler validate "$ARTIFACT" + ;; + *) + ;; +esac + +if [[ -n "$STAPLE_APP_PATH" ]]; then + if [[ -d "$STAPLE_APP_PATH" ]]; then + echo "📌 Stapling app: $STAPLE_APP_PATH" + xcrun stapler staple "$STAPLE_APP_PATH" + xcrun stapler validate "$STAPLE_APP_PATH" + else + echo "Warn: STAPLE_APP_PATH not found: $STAPLE_APP_PATH" >&2 + fi +fi + +echo "✅ Notarization complete" diff --git a/scripts/package-mac-dist.sh b/scripts/package-mac-dist.sh index 6339a389f..d872644eb 100755 --- a/scripts/package-mac-dist.sh +++ b/scripts/package-mac-dist.sh @@ -21,6 +21,16 @@ fi VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$APP/Contents/Info.plist" 2>/dev/null || echo "0.0.0") ZIP="$ROOT_DIR/dist/Clawdis-$VERSION.zip" DMG="$ROOT_DIR/dist/Clawdis-$VERSION.dmg" +NOTARY_ZIP="$ROOT_DIR/dist/Clawdis-$VERSION.notary.zip" +NOTARIZE="${NOTARIZE:-0}" + +if [[ "$NOTARIZE" == "1" ]]; then + echo "📦 Notary zip: $NOTARY_ZIP" + rm -f "$NOTARY_ZIP" + ditto -c -k --sequesterRsrc --keepParent "$APP" "$NOTARY_ZIP" + STAPLE_APP_PATH="$APP" "$ROOT_DIR/scripts/notarize-mac-artifact.sh" "$NOTARY_ZIP" + rm -f "$NOTARY_ZIP" +fi echo "📦 Zip: $ZIP" rm -f "$ZIP" @@ -29,3 +39,6 @@ ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP" echo "💿 DMG: $DMG" "$ROOT_DIR/scripts/create-dmg.sh" "$APP" "$DMG" +if [[ "$NOTARIZE" == "1" ]]; then + "$ROOT_DIR/scripts/notarize-mac-artifact.sh" "$DMG" +fi