Skip to main content

max / goingson

Track the dist release scripts; move Apple credentials to the environment A build host driven by ops-agent gets its tree by checkout, so gitignored release scripts never arrive. They were hand-scp'd to mbp, which is why nothing noticed. Un-ignoring them is what lets an agent-driven release run at all. They were ignored for a real reason: release-macos.sh and release-ios.sh inlined the App Store Connect issuer and key id. Those are two thirds of the notary credential (the .p8 is the third), and this repo is mirrored to three remotes, so committing them as-is would have published credential material. Both now read APPLE_API_KEY_ID / APPLE_API_ISSUER_ID / APPLE_API_KEY_PATH from the environment, sourced from ~/.tauri/passwords.env, which is not in git. The scripts were untracked until now, so no key id ever entered history. release-macos.sh keeps them optional because --sign-only does not notarize, and its preflight now checks all three together: passing an empty issuer through to notarytool fails much deeper with a much worse message. Dropped retry-ios-export.sh: a one-off that slept 20 minutes waiting out an unaccepted Platform License Agreement, still pointing at AuthKey_MVR74793B9 -- the wrong-role key reissued as 35BZ6XF6GT in May and no longer installed in ~/.appstoreconnect/private_keys. audiofiles already had the !dist/*.sh exception and carries no credentials. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-16 14:57 UTC
Commit: 6ecd85eb27815828dd1758283faca71ef7ec8685
Parent: ce65792
5 files changed, +666 insertions, -2 deletions
M .gitignore +6 -2
@@ -14,10 +14,14 @@
14 14 .claude/
15 15 .mcp.json
16 16
17 - # Release artifacts (secret signing scripts stay Syncthing-only); Bento build
18 - # recipes are non-secret and tracked.
17 + # Release artifacts (dmg/app/ios output) stay out of git. The release scripts and
18 + # Bento recipes are tracked: they carry no credentials -- the App Store Connect
19 + # issuer/key id moved to ~/.tauri/passwords.env in 2026-07, so a build host gets
20 + # the scripts by checkout instead of a hand-scp. Keep it that way; never inline a
21 + # key id, issuer, or password here.
19 22 dist/*
20 23 !dist/recipes/
24 + !dist/*.sh
21 25
22 26 # Archive
23 27 _archive/
@@ -0,0 +1,179 @@
1 + #!/usr/bin/env bash
2 + # build-keychain.sh -- ephemeral Developer ID build keychain for headless signing.
3 + #
4 + # Why this exists: over SSH the Mac's login.keychain is locked
5 + # ("User interaction is not allowed"), so `security find-identity` reports
6 + # "0 valid identities found" and codesign silently produces an unsigned bundle.
7 + # This creates a throwaway keychain, imports a password-protected .p12, and runs
8 + # `set-key-partition-list` so codesign can use the key WITHOUT the GUI auth
9 + # prompt that otherwise hangs a non-interactive session forever.
10 + #
11 + # Team-agnostic on purpose: it signs with whatever identity the .p12 contains.
12 + # That means remote signing works BEFORE the Apple Developer org transfer to the
13 + # LLC is finished -- the team only matters at the notarization step, which is a
14 + # separate concern (see release-macos.sh --sign-only).
15 + #
16 + # This is the first concrete piece of Bento (the app-release orchestrator).
17 + # Bento's macOS recipe will `source` this; for now release-macos.sh does.
18 + #
19 + # --- Usage (sourced, the normal path) ---
20 + # source "$(dirname "$0")/build-keychain.sh"
21 + # bk_setup # create + import + unlock + put on search list
22 + # trap bk_teardown EXIT # ALWAYS tear down, even on failure
23 + # id="$(bk_identity_name)" # the "Developer ID Application: ... (TEAM)" string
24 + # ... codesign -s "$id" ... / ... APPLE_SIGNING_IDENTITY="$id" cargo tauri build ...
25 + # # teardown runs on exit
26 + #
27 + # --- Usage (standalone, for inspection/debugging) ---
28 + # ./dist/build-keychain.sh setup # leaves the keychain in place
29 + # ./dist/build-keychain.sh identity # print the Developer ID identity it holds
30 + # ./dist/build-keychain.sh teardown # remove it + restore the search list
31 + #
32 + # --- Required env (live in ~/.tauri/passwords.env on mbp; never in git) ---
33 + # BUILD_P12_PATH path to the exported Developer ID Application .p12
34 + # BUILD_P12_PASSWORD password protecting that .p12
35 + # BUILD_KEYCHAIN_PASSWORD password for the temp keychain (ephemeral; any value)
36 + #
37 + # One-time prerequisite (cannot be done over SSH -- needs a GUI Terminal on the
38 + # Mac with login.keychain unlocked) -- export the cert + private key to a .p12:
39 + # security find-identity -v -p codesigning | grep "Developer ID Application"
40 + # # then, in Keychain Access: right-click the "Developer ID Application" identity
41 + # # -> Export -> .p12 -> set a password -> save to ~/Code/_private/developer-id.p12
42 + # # (or CLI: security export -t identities -f pkcs12 \
43 + # # -P "<p12pw>" -o ~/Code/_private/developer-id.p12 )
44 + # # NOTE: omit -k. On modern macOS the Developer ID key lives in the
45 + # # data-protection keychain, not login.keychain-db; passing
46 + # # `-k login.keychain` finds nothing. Default search list works.
47 + # Then add BUILD_P12_PATH / BUILD_P12_PASSWORD / BUILD_KEYCHAIN_PASSWORD to
48 + # ~/.tauri/passwords.env.
49 +
50 + set -euo pipefail
51 +
52 + # A FULL PATH, not a bare name. Over SSH `security create-keychain` with a bare
53 + # name tries ~/Library/Keychains/ and fails "SecKeychainCreate ... Permission
54 + # denied"; a full path in $TMPDIR works headlessly (the CI-standard approach).
55 + # Override with BUILD_KEYCHAIN if you need a different location.
56 + : "${BUILD_KEYCHAIN:=${TMPDIR:-/tmp}/bento-build.keychain-db}"
57 +
58 + # Path used to stash the original keychain search list across setup/teardown so a
59 + # standalone teardown (separate process) can still restore it.
60 + _BK_STATE_FILE="${TMPDIR:-/tmp}/.bento-build-keychain.searchlist"
61 +
62 + _bk_require_env() {
63 + local missing=0
64 + for v in BUILD_P12_PATH BUILD_P12_PASSWORD BUILD_KEYCHAIN_PASSWORD; do
65 + if [ -z "${!v:-}" ]; then
66 + echo "build-keychain: required env $v is unset (source ~/.tauri/passwords.env)" >&2
67 + missing=1
68 + fi
69 + done
70 + if [ -n "${BUILD_P12_PATH:-}" ] && [ ! -f "$BUILD_P12_PATH" ]; then
71 + echo "build-keychain: BUILD_P12_PATH not found: $BUILD_P12_PATH" >&2
72 + echo " Run the one-time GUI .p12 export first (see header of this script)." >&2
73 + missing=1
74 + fi
75 + [ "$missing" -eq 0 ] || return 1
76 + }
77 +
78 + bk_setup() {
79 + _bk_require_env || return 1
80 +
81 + # If a stale keychain from a crashed run exists, remove it first.
82 + security delete-keychain "$BUILD_KEYCHAIN" 2>/dev/null || true
83 +
84 + echo "build-keychain: creating $BUILD_KEYCHAIN" >&2
85 + if ! security create-keychain -p "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN"; then
86 + echo "build-keychain: create-keychain FAILED. Over SSH this needs a full" >&2
87 + echo " keychain PATH (BUILD_KEYCHAIN=$BUILD_KEYCHAIN) and an active console" >&2
88 + echo " login session on the Mac." >&2
89 + return 1
90 + fi
91 + # No idle auto-lock during a long build; lock-on-sleep stays on.
92 + security set-keychain-settings -lut 21600 "$BUILD_KEYCHAIN"
93 + security unlock-keychain -p "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN"
94 +
95 + echo "build-keychain: importing $BUILD_P12_PATH" >&2
96 + security import "$BUILD_P12_PATH" -k "$BUILD_KEYCHAIN" -P "$BUILD_P12_PASSWORD" \
97 + -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productsign
98 +
99 + # Import the Apple intermediate(s). An isolated keychain holds only the leaf
100 + # cert; codesign rejects an identity whose chain it can't build ("no identity
101 + # found"). Match the leaf's issuer: this team's Developer ID Application cert is
102 + # issued by the ORIGINAL "Developer ID Certification Authority"
103 + # (OU=Apple Certification Authority) -> DeveloperIDCA.cer; G2 (OU=G2) is for
104 + # newer certs and will NOT chain it. Both are public; we fetch (cached) and
105 + # import both so either generation works. Apple Root CA is in the System trust
106 + # store, so System.keychain must stay on the search list (below).
107 + _bk_import_apple_intermediates
108 +
109 + # Pre-authorize the tools to use the imported key so macOS doesn't pop a GUI
110 + # prompt (apple-tool:/apple: is the Apple-standard partition set).
111 + security set-key-partition-list -S apple-tool:,apple: \
112 + -s -k "$BUILD_KEYCHAIN_PASSWORD" "$BUILD_KEYCHAIN" >/dev/null
113 +
114 + # Prepend our keychain to the user search list, KEEPING the existing ones
115 + # (login + System). Dropping System breaks root anchoring/lookups.
116 + local orig
117 + orig="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
118 + printf '%s\n' "$orig" > "$_BK_STATE_FILE"
119 + # shellcheck disable=SC2086 # word-splitting the list is intentional
120 + security list-keychains -d user -s "$BUILD_KEYCHAIN" $orig
121 +
122 + echo "build-keychain: ready -- identity: $(bk_identity_name || echo '??')" >&2
123 + # NOTE: keychain setup is now correct, but codesign STILL needs to run in the
124 + # console GUI (Aqua) security session to USE the private key. A pure SSH session
125 + # can enumerate the identity but not sign with it ("no identity found"). Drive
126 + # the build from a GUI Terminal, a user LaunchAgent, or `launchctl asuser <uid>`
127 + # (needs root). See bento/design.md "THE WALL" for the full analysis.
128 + }
129 +
130 + # Fetch (cached under the keychain dir) + import the public Apple Developer ID
131 + # intermediates so codesign can build the cert chain.
132 + _bk_import_apple_intermediates() {
133 + local dir cer url
134 + dir="$(dirname "$BUILD_KEYCHAIN")"
135 + for cer in DeveloperIDCA.cer DeveloperIDG2CA.cer; do
136 + url="https://www.apple.com/certificateauthority/$cer"
137 + if [ ! -f "$dir/$cer" ]; then
138 + curl -fsS "$url" -o "$dir/$cer" 2>/dev/null \
139 + || { echo "build-keychain: warn: could not fetch $cer" >&2; continue; }
140 + fi
141 + security import "$dir/$cer" -k "$BUILD_KEYCHAIN" >/dev/null 2>&1 || true
142 + done
143 + }
144 +
145 + bk_identity_name() {
146 + security find-identity -v -p codesigning "$BUILD_KEYCHAIN" \
147 + | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -1
148 + }
149 +
150 + bk_identity_hash() {
151 + security find-identity -v -p codesigning "$BUILD_KEYCHAIN" \
152 + | awk '/Developer ID Application/ {print $2; exit}'
153 + }
154 +
155 + bk_teardown() {
156 + # Restore the original search list if we saved one.
157 + if [ -f "$_BK_STATE_FILE" ]; then
158 + local orig
159 + orig="$(cat "$_BK_STATE_FILE")"
160 + if [ -n "$orig" ]; then
161 + # shellcheck disable=SC2086
162 + security list-keychains -d user -s $orig 2>/dev/null || true
163 + fi
164 + rm -f "$_BK_STATE_FILE"
165 + fi
166 + security delete-keychain "$BUILD_KEYCHAIN" 2>/dev/null || true
167 + echo "build-keychain: torn down $BUILD_KEYCHAIN" >&2
168 + }
169 +
170 + # When run directly (not sourced), act as a small CLI.
171 + if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
172 + case "${1:-}" in
173 + setup) bk_setup ;;
174 + identity) bk_identity_name ;;
175 + hash) bk_identity_hash ;;
176 + teardown) bk_teardown ;;
177 + *) echo "usage: $0 {setup|identity|hash|teardown}" >&2; exit 2 ;;
178 + esac
179 + fi
@@ -0,0 +1,95 @@
1 + #!/usr/bin/env bash
2 + set -euo pipefail
3 +
4 + # GoingsOn Android Release Script
5 + # Builds a signed release AAB for Google Play distribution
6 + #
7 + # Prerequisites:
8 + # - Android SDK + NDK installed (sdkmanager)
9 + # - Rust Android targets (aarch64-linux-android, etc.)
10 + # - Signing keystore at ~/.tauri/goingson-android.jks
11 + # - key.properties in src-tauri/gen/android/
12 + # - ANDROID_HOME, NDK_HOME, JAVA_HOME set
13 + #
14 + # Usage:
15 + # ./dist/release-android.sh # build signed release AAB
16 + # ./dist/release-android.sh --apk # build APK instead of AAB
17 + # ./dist/release-android.sh --debug # build debug APK
18 +
19 + cd "$(dirname "$0")/.."
20 +
21 + # --- Configuration ---
22 + ANDROID_HOME="${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}"
23 + NDK_HOME="${NDK_HOME:-$ANDROID_HOME/ndk/27.0.12077973}"
24 + JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}"
25 + NDK_BIN="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin"
26 + export ANDROID_HOME NDK_HOME JAVA_HOME
27 +
28 + # NDK cross-compilation toolchain (needed for vendored OpenSSL)
29 + export PATH="$NDK_BIN:$PATH"
30 + export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang"
31 + export AR_aarch64_linux_android="$NDK_BIN/llvm-ar"
32 + export RANLIB_aarch64_linux_android="$NDK_BIN/llvm-ranlib"
33 +
34 + # --- Parse args ---
35 + FORMAT="aab"
36 + DEBUG=false
37 + for arg in "$@"; do
38 + case "$arg" in
39 + --apk) FORMAT="apk" ;;
40 + --debug) DEBUG=true ;;
41 + -h|--help)
42 + echo "Usage: $0 [--apk | --debug]"
43 + echo " --apk Build APK instead of AAB"
44 + echo " --debug Build debug APK"
45 + exit 0
46 + ;;
47 + esac
48 + done
49 +
50 + # --- Read version from tauri.conf.json ---
51 + VERSION=$(python3 -c "import json; print(json.load(open('src-tauri/tauri.conf.json'))['version'])")
52 + echo "Version: $VERSION"
53 +
54 + # --- Verify prerequisites ---
55 + if [ ! -d "$NDK_HOME" ]; then
56 + echo "Error: NDK not found at $NDK_HOME"
57 + echo "Install with: sdkmanager 'ndk;27.0.12077973'"
58 + exit 1
59 + fi
60 +
61 + if [ ! -f "$JAVA_HOME/bin/java" ]; then
62 + echo "Error: Java not found at $JAVA_HOME"
63 + echo "Install with: brew install openjdk"
64 + exit 1
65 + fi
66 +
67 + # --- Build ---
68 + if [ "$DEBUG" = true ]; then
69 + echo ""
70 + echo "=== Building Android debug APK ==="
71 + cargo tauri android build --debug --target aarch64
72 + echo ""
73 + echo "Debug APK built. Look for output in:"
74 + echo " src-tauri/gen/android/app/build/outputs/apk/universal/debug/"
75 + else
76 + echo ""
77 + if [ "$FORMAT" = "apk" ]; then
78 + echo "=== Building Android release APK ==="
79 + cargo tauri android build --target aarch64 -- --apk
80 + else
81 + echo "=== Building Android release AAB ==="
82 + cargo tauri android build --target aarch64 -- --aab
83 + fi
84 +
85 + echo ""
86 + echo "Release $FORMAT built. Look for output in:"
87 + if [ "$FORMAT" = "apk" ]; then
88 + echo " src-tauri/gen/android/app/build/outputs/apk/universal/release/"
89 + else
90 + echo " src-tauri/gen/android/app/build/outputs/bundle/universalRelease/"
91 + fi
92 + fi
93 +
94 + echo ""
95 + echo "Done. GoingsOn Android v$VERSION ($FORMAT)"
@@ -0,0 +1,171 @@
1 + #!/usr/bin/env bash
2 + set -euo pipefail
3 +
4 + # GoingsOn iOS Release Script
5 + # Builds release archive, exports IPA, and uploads to TestFlight
6 + #
7 + # Prerequisites:
8 + # - Xcode with iOS SDK
9 + # - Apple Developer account with PLA accepted
10 + # - App ID registered for com.goingson.app
11 + # - App Store Connect API credentials in the environment: source
12 + # ~/.tauri/passwords.env (APPLE_API_KEY_ID / APPLE_API_ISSUER_ID /
13 + # APPLE_API_KEY_PATH). They are not in this file; see Configuration below.
14 + #
15 + # Usage:
16 + # ./dist/release-ios.sh # full flow: build + export + upload
17 + # ./dist/release-ios.sh --build-only # build archive only, skip upload
18 + # ./dist/release-ios.sh --upload-only # export + upload existing archive
19 +
20 + cd "$(dirname "$0")/.."
21 +
22 + # --- Configuration ---
23 + # App Store Connect API credentials come from the environment, never from this
24 + # file: this script is version-controlled and the repo is mirrored publicly.
25 + # Issuer + key id are two thirds of the notary credential (the .p8 is the third),
26 + # so they live in ~/.tauri/passwords.env, which is not in git. Source it first:
27 + # source ~/.tauri/passwords.env && ./dist/release-ios.sh
28 + API_KEY_ID="${APPLE_API_KEY_ID:?set APPLE_API_KEY_ID -- source ~/.tauri/passwords.env}"
29 + API_ISSUER_ID="${APPLE_API_ISSUER_ID:?set APPLE_API_ISSUER_ID -- source ~/.tauri/passwords.env}"
30 + API_KEY_PATH="${APPLE_API_KEY_PATH:?set APPLE_API_KEY_PATH -- source ~/.tauri/passwords.env}"
31 + ARCHIVE_PATH="src-tauri/gen/apple/build/goingson-desktop_iOS.xcarchive"
32 + EXPORT_DIR="dist/ios"
33 + EXPORT_PLIST="src-tauri/gen/apple/ExportOptions.plist"
34 + PROJECT_YML="src-tauri/gen/apple/project.yml"
35 +
36 + # --- Parse args ---
37 + BUILD=true
38 + UPLOAD=true
39 + for arg in "$@"; do
40 + case "$arg" in
41 + --build-only) UPLOAD=false ;;
42 + --upload-only) BUILD=false ;;
43 + -h|--help)
44 + echo "Usage: $0 [--build-only | --upload-only]"
45 + exit 0
46 + ;;
47 + esac
48 + done
49 +
50 + # --- Read version from tauri.conf.json ---
51 + VERSION=$(python3 -c "import json; print(json.load(open('src-tauri/tauri.conf.json'))['version'])")
52 + echo "Version: $VERSION"
53 +
54 + # --- Sync version to project.yml ---
55 + if grep -q "CFBundleShortVersionString:" "$PROJECT_YML"; then
56 + sed -i '' "s/CFBundleShortVersionString: .*/CFBundleShortVersionString: $VERSION/" "$PROJECT_YML"
57 + sed -i '' "s/CFBundleVersion: .*/CFBundleVersion: \"$VERSION\"/" "$PROJECT_YML"
58 + echo "Synced version $VERSION to $PROJECT_YML"
59 + fi
60 +
61 + # --- Verify API key ---
62 + if [ ! -f "$API_KEY_PATH" ]; then
63 + echo "Error: API key not found at $API_KEY_PATH"
64 + echo "Set APPLE_API_KEY_PATH to the correct path"
65 + exit 1
66 + fi
67 +
68 + # --- Flatten app icons (App Store rejects alpha) ---
69 + # `cargo tauri ios init` and ad-hoc icon regenerations have re-seeded the
70 + # appiconset with alpha-channel PNGs more than once (v0.3.1 shipped the Tauri
71 + # default logo; v0.3.4 upload first failed with "Invalid large app icon ...
72 + # can't be transparent or contain an alpha channel"). Re-flatten every build
73 + # from media/logo-go-icon.svg so this can't regress silently.
74 + ICON_SVG="media/logo-go-icon.svg"
75 + ICON_OUT="src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset"
76 + ICON_TMP="$(mktemp -t goingson-icon-XXXXXX).png"
77 + if [ "$BUILD" = true ] && [ -f "$ICON_SVG" ] && [ -d "$ICON_OUT" ]; then
78 + echo ""
79 + echo "=== Flattening app icons (no alpha) ==="
80 + magick -background white -density 600 "$ICON_SVG" \
81 + -resize 1024x1024 -alpha remove -alpha off "$ICON_TMP"
82 + icon_pairs=(
83 + "AppIcon-20x20@1x.png:20" "AppIcon-20x20@2x.png:40"
84 + "AppIcon-20x20@2x-1.png:40" "AppIcon-20x20@3x.png:60"
85 + "AppIcon-29x29@1x.png:29" "AppIcon-29x29@2x.png:58"
86 + "AppIcon-29x29@2x-1.png:58" "AppIcon-29x29@3x.png:87"
87 + "AppIcon-40x40@1x.png:40" "AppIcon-40x40@2x.png:80"
88 + "AppIcon-40x40@2x-1.png:80" "AppIcon-40x40@3x.png:120"
89 + "AppIcon-60x60@2x.png:120" "AppIcon-60x60@3x.png:180"
90 + "AppIcon-76x76@1x.png:76" "AppIcon-76x76@2x.png:152"
91 + "AppIcon-83.5x83.5@2x.png:167" "AppIcon-512@2x.png:1024"
92 + )
93 + for p in "${icon_pairs[@]}"; do
94 + name="${p%:*}"; size="${p#*:}"
95 + magick "$ICON_TMP" -resize "${size}x${size}" \
96 + -background white -alpha remove -alpha off "$ICON_OUT/$name"
97 + done
98 + rm -f "$ICON_TMP"
99 + echo "Flattened 18 icons from $ICON_SVG"
100 + fi
101 +
102 + # --- Build ---
103 + if [ "$BUILD" = true ]; then
104 + echo ""
105 + echo "=== Building iOS release archive ==="
106 + APPLE_API_KEY_PATH="$API_KEY_PATH" cargo tauri ios build --export-method app-store-connect
107 + echo "Archive: $ARCHIVE_PATH"
108 + fi
109 +
110 + # --- Upload ---
111 + if [ "$UPLOAD" = true ]; then
112 + mkdir -p "$EXPORT_DIR"
113 +
114 + # Pick the freshest IPA across both possible locations. Searching only one
115 + # location (or stopping at `head -1`) has uploaded stale IPAs in the past.
116 + IPA_PATH=$(find "$EXPORT_DIR" src-tauri/gen/apple/build \
117 + -name "*.ipa" -type f 2>/dev/null \
118 + -exec stat -f '%m %N' {} \; \
119 + | sort -rn | head -1 | cut -d' ' -f2- || true)
120 +
121 + # If the freshest IPA is older than the archive, the archive hasn't been
122 + # exported yet — export it now.
123 + if [ -z "$IPA_PATH" ] || [ "$ARCHIVE_PATH" -nt "$IPA_PATH" ]; then
124 + echo ""
125 + echo "=== Exporting IPA from archive ==="
126 + xcodebuild -exportArchive \
127 + -archivePath "$ARCHIVE_PATH" \
128 + -exportOptionsPlist "$EXPORT_PLIST" \
129 + -exportPath "$EXPORT_DIR" \
130 + -allowProvisioningUpdates \
131 + -authenticationKeyID "$API_KEY_ID" \
132 + -authenticationKeyPath "$API_KEY_PATH" \
133 + -authenticationKeyIssuerID "$API_ISSUER_ID"
134 + IPA_PATH=$(find "$EXPORT_DIR" -name "*.ipa" -type f \
135 + -exec stat -f '%m %N' {} \; \
136 + | sort -rn | head -1 | cut -d' ' -f2- || true)
137 + fi
138 +
139 + if [ -z "$IPA_PATH" ]; then
140 + echo "Error: No IPA found after export"
141 + exit 1
142 + fi
143 +
144 + echo ""
145 + echo "=== Uploading to TestFlight ==="
146 + echo "IPA: $IPA_PATH"
147 + # altool exits 0 even when it logs "ERROR:" / "Failed to upload" — grep
148 + # the output to confirm the success banner before declaring victory.
149 + set +e
150 + UPLOAD_OUTPUT=$(xcrun altool --upload-app \
151 + --type ios \
152 + --file "$IPA_PATH" \
153 + --apiKey "$API_KEY_ID" \
154 + --apiIssuer "$API_ISSUER_ID" 2>&1)
155 + UPLOAD_RC=$?
156 + set -e
157 + echo "$UPLOAD_OUTPUT"
158 +
159 + if [ $UPLOAD_RC -ne 0 ] || ! grep -q "UPLOAD SUCCEEDED" <<<"$UPLOAD_OUTPUT"; then
160 + echo ""
161 + echo "Error: altool upload failed (exit=$UPLOAD_RC). See output above."
162 + exit 1
163 + fi
164 +
165 + echo ""
166 + echo "Upload complete! Build will appear in TestFlight within ~30 minutes."
167 + echo "https://appstoreconnect.apple.com/apps"
168 + fi
169 +
170 + echo ""
171 + echo "Done. GoingsOn iOS v$VERSION"
@@ -0,0 +1,215 @@
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."