max / goingson
5 files changed,
+666 insertions,
-2 deletions
| @@ -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." |