Skip to main content

max / goingson

9.6 KB · 216 lines History Blame Raw
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 # GoingsOn macOS Release Script
5 # Builds a release .dmg, codesigns with Developer ID, notarizes via the
6 # App Store Connect API key, staples the ticket, and verifies the result.
7 #
8 # Run on the Mac (mbp). Two signing modes:
9 # default -- GUI session, signs from the unlocked login.keychain.
10 # --keychain -- HEADLESS/REMOTE: builds an ephemeral Developer ID build
11 # keychain (dist/build-keychain.sh) so this works over SSH from
12 # fw13 without a GUI unlock. The signing identity is derived
13 # automatically from the imported .p12 -- no need to fill in
14 # SIGNING_IDENTITY.
15 #
16 # Org-transfer note: --keychain signs with whatever team the .p12 holds, so it
17 # works BEFORE the Apple Developer org transfer to the LLC. Notarization is the
18 # only step that needs the signature team to match the notary key's team, so
19 # pass --sign-only to defer notarization until the teams line up.
20 #
21 # Prerequisites:
22 # - Xcode command line tools (codesign, stapler, spctl, xcrun notarytool)
23 # - A Developer ID Application identity, either:
24 # * installed in login.keychain (default GUI mode), or
25 # * exported to a .p12 referenced by ~/.tauri/passwords.env (--keychain mode)
26 # - App Store Connect API key (.p8) at API_KEY_PATH -- only for notarization
27 # - Tauri updater signing key at ~/.tauri/goingson.key (for OTA artifacts)
28 #
29 # Usage:
30 # ./dist/release-macos.sh # GUI: build + notarize + verify
31 # ./dist/release-macos.sh --keychain # remote: build keychain + notarize
32 # ./dist/release-macos.sh --keychain --sign-only# remote: sign only, no notarize
33 # ./dist/release-macos.sh --no-verify # skip the post-build gate
34 #
35 # Remote invocation from fw13:
36 # ssh mbp 'cd ~/Code/Apps/goingson && source ~/.tauri/passwords.env && \
37 # ./dist/release-macos.sh --keychain --sign-only'
38
39 cd "$(dirname "$0")/.."
40
41 # --- Configuration ---
42 # Fill SIGNING_IDENTITY from the exact output of (run in a GUI Terminal):
43 # security find-identity -v -p codesigning | grep "Developer ID Application"
44 # The 10-char team in parens MUST match the notary key's team (93C54W92UP),
45 # or notarization rejects the signature for a team mismatch.
46 SIGNING_IDENTITY="${APPLE_SIGNING_IDENTITY:-Developer ID Application: FILL_ME (93C54W92UP)}"
47
48 # App Store Connect API credentials come from the environment, never from this
49 # file: this script is version-controlled and the repo is mirrored publicly.
50 # Issuer + key id are two thirds of the notary credential (the .p8 is the third),
51 # so they live in ~/.tauri/passwords.env, which is not in git. Only notarization
52 # reads them, so --sign-only does not require them.
53 API_KEY_ID="${APPLE_API_KEY_ID:-}"
54 API_ISSUER_ID="${APPLE_API_ISSUER_ID:-}"
55 API_KEY_PATH="${APPLE_API_KEY_PATH:-}"
56
57 DMG_GLOB="target/release/bundle/dmg/*.dmg"
58 APP_GLOB="target/release/bundle/macos/GoingsOn.app"
59 DIST_DIR="${HOME}/Dist/goingson/macos"
60
61 # --- Parse args ---
62 VERIFY=true
63 USE_KEYCHAIN=false
64 NOTARIZE=true
65 for arg in "$@"; do
66 case "$arg" in
67 --no-verify) VERIFY=false ;;
68 --keychain) USE_KEYCHAIN=true ;;
69 --sign-only) NOTARIZE=false ;;
70 -h|--help)
71 echo "Usage: $0 [--keychain] [--sign-only] [--no-verify]"
72 exit 0
73 ;;
74 esac
75 done
76
77 # --- Read version from tauri.conf.json ---
78 VERSION=$(python3 -c "import json; print(json.load(open('src-tauri/tauri.conf.json'))['version'])")
79 echo "Version: $VERSION"
80
81 # --- Signing identity: ephemeral build keychain (remote) or login.keychain (GUI) ---
82 if [ "$USE_KEYCHAIN" = true ]; then
83 # shellcheck source=dist/build-keychain.sh
84 source "$(dirname "$0")/build-keychain.sh"
85 trap bk_teardown EXIT
86 bk_setup
87 SIGNING_IDENTITY="$(bk_identity_name)"
88 if [ -z "$SIGNING_IDENTITY" ]; then
89 echo "Error: no Developer ID Application identity in the build keychain."
90 echo "Check BUILD_P12_PATH points at a Developer ID Application .p12."
91 exit 1
92 fi
93 echo "Build-keychain identity: $SIGNING_IDENTITY"
94 fi
95
96 # --- Preflight ---
97 if [[ "$SIGNING_IDENTITY" == *FILL_ME* ]]; then
98 echo "Error: SIGNING_IDENTITY is still the placeholder."
99 echo "Either pass --keychain (derives it from the .p12) or set APPLE_SIGNING_IDENTITY"
100 echo "from: security find-identity -v -p codesigning | grep 'Developer ID Application'"
101 exit 1
102 fi
103 if [ "$NOTARIZE" = true ]; then
104 # All three are required to notarize, and all three now come from the
105 # environment. Check them together: passing an empty issuer/key id through to
106 # Tauri fails deep inside notarytool with a far less obvious message.
107 missing=""
108 [ -n "$API_KEY_ID" ] || missing="$missing APPLE_API_KEY_ID"
109 [ -n "$API_ISSUER_ID" ] || missing="$missing APPLE_API_ISSUER_ID"
110 [ -n "$API_KEY_PATH" ] || missing="$missing APPLE_API_KEY_PATH"
111 if [ -n "$missing" ]; then
112 echo "Error: notarization needs:$missing"
113 echo "Source the env file first (source ~/.tauri/passwords.env), or pass --sign-only."
114 exit 1
115 fi
116 if [ ! -f "$API_KEY_PATH" ]; then
117 echo "Error: API key not found at $API_KEY_PATH (check APPLE_API_KEY_PATH, or pass --sign-only)"
118 exit 1
119 fi
120 fi
121 if [ "$USE_KEYCHAIN" != true ] && ! security find-identity -v -p codesigning | grep -qF "$SIGNING_IDENTITY"; then
122 echo "Error: identity not found in keychain: $SIGNING_IDENTITY"
123 echo "Are you in a GUI session with login.keychain unlocked? (or pass --keychain for remote signing)"
124 exit 1
125 fi
126 if [ -z "${GOINGSON_TAURI_PASSWORD:-}" ]; then
127 echo "Note: GOINGSON_TAURI_PASSWORD unset -- source ~/.tauri/passwords.env first if you want OTA updater artifacts signed."
128 fi
129
130 # --- Build + sign (+ notarize + staple) ---
131 # Setting APPLE_API_ISSUER + APPLE_API_KEY + APPLE_API_KEY_PATH is what flips
132 # Tauri from "sign only" to "sign + notarize + staple" in one pass. With
133 # --sign-only those are omitted, so Tauri signs but does not notarize -- the
134 # org-transfer-deferral path (no team match needed to sign).
135 echo ""
136 if [ "$NOTARIZE" = true ]; then
137 echo "=== Building, signing, and notarizing macOS release ==="
138 else
139 echo "=== Building and signing macOS release (--sign-only, no notarization) ==="
140 fi
141 build_env=(
142 "APPLE_SIGNING_IDENTITY=$SIGNING_IDENTITY"
143 "TAURI_SIGNING_PRIVATE_KEY=${TAURI_SIGNING_PRIVATE_KEY:-$HOME/.tauri/goingson.key}"
144 "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-${GOINGSON_TAURI_PASSWORD:-}}"
145 )
146 if [ "$NOTARIZE" = true ]; then
147 build_env+=(
148 "APPLE_API_ISSUER=$API_ISSUER_ID"
149 "APPLE_API_KEY=$API_KEY_ID"
150 "APPLE_API_KEY_PATH=$API_KEY_PATH"
151 )
152 fi
153 # `app` MUST be in the bundle list for the updater artifact (.app.tar.gz + .sig)
154 # to be produced — `updater` alone with only `dmg` builds nothing updater-enabled
155 # ("no updater-enabled targets were built"). The .app is also what OTA ships.
156 env "${build_env[@]}" cargo tauri build --bundles app dmg updater
157
158 DMG_PATH=$(ls -t $DMG_GLOB 2>/dev/null | head -1 || true)
159 if [ -z "$DMG_PATH" ]; then
160 echo "Error: no .dmg produced under target/release/bundle/dmg/"
161 exit 1
162 fi
163 echo "Built: $DMG_PATH"
164
165 # --- Verify (the real release gate) ---
166 # Tauri/codesign can exit 0 with a bundle that Gatekeeper still rejects, so
167 # verify explicitly instead of trusting the build's exit code.
168 if [ "$VERIFY" = true ]; then
169 echo ""
170 echo "=== Verifying signature ==="
171 # Tauri removes the intermediate .app after bundling the DMG, so it may be
172 # gone by now — the DMG (stapler + spctl below) is the real gate.
173 if [ -d "$APP_GLOB" ]; then
174 echo "--- codesign (app) ---"
175 codesign --verify --deep --strict --verbose=2 "$APP_GLOB"
176 else
177 echo "--- codesign (app): skipped, .app cleaned after bundling ---"
178 fi
179 if [ "$NOTARIZE" = true ]; then
180 # Tauri notarizes + staples the .app but NOT the .dmg, so the disk image
181 # itself has no ticket. Notarize + staple the DMG so it is Gatekeeper-clean
182 # on mount (not just the app inside it).
183 echo "--- notarize + staple (dmg) ---"
184 xcrun notarytool submit "$DMG_PATH" \
185 --key "$API_KEY_PATH" --key-id "$API_KEY_ID" --issuer "$API_ISSUER_ID" --wait
186 xcrun stapler staple "$DMG_PATH"
187 xcrun stapler validate "$DMG_PATH"
188 echo "--- spctl (Gatekeeper) ---"
189 # Expect: "accepted" with "source=Notarized Developer ID".
190 if ! spctl -a -vvv -t install "$DMG_PATH" 2>&1 | tee /dev/stderr | grep -q "source=Notarized Developer ID"; then
191 echo "Error: DMG is not Gatekeeper-accepted as Notarized Developer ID."
192 exit 1
193 fi
194 else
195 # Sign-only: confirm the signature exists and report the signing team, but
196 # do NOT assert Gatekeeper acceptance -- an un-notarized DMG is rejected on
197 # other Macs by design (right-click-open works for hand-picked beta testers).
198 echo "--- codesign (signing authority / team) ---"
199 codesign -dvvv "$APP_GLOB" 2>&1 | grep -E 'Authority|TeamIdentifier' || true
200 echo "Note: --sign-only -- DMG is signed but NOT notarized; Gatekeeper will"
201 echo " flag it on other Macs until you re-run without --sign-only post-transfer."
202 fi
203 fi
204
205 # --- Stage artifacts ---
206 mkdir -p "$DIST_DIR"
207 cp "$DMG_PATH" "$DIST_DIR/"
208 # Updater artifacts (.app.tar.gz + .sig) for OTA, if produced.
209 find target/release/bundle/macos -maxdepth 1 \( -name '*.app.tar.gz' -o -name '*.app.tar.gz.sig' \) \
210 -exec cp {} "$DIST_DIR/" \; 2>/dev/null || true
211
212 echo ""
213 echo "Done. GoingsOn macOS v$VERSION"
214 echo "DMG staged at: $DIST_DIR/$(basename "$DMG_PATH")"
215 echo "Next: clean-account DMG smoke test, then upload updater artifacts via the MNW dashboard."
216