#!/bin/bash # TestFlight upload via App Store Connect API key (headless, no GUI). # Expects: ~/AuthKey.p8 (or AUTH_KEY_PATH) # Requires: KEY_ID, ISSUER_ID (env or --key-id / --issuer-id) # Optional: KEYCHAIN_PASS to unlock login keychain set -euo pipefail IOS_DIR="${IOS_DIR:-$HOME/Code/mediacluster/apps/ios}" AUTH_KEY_PATH="${AUTH_KEY_PATH:-$HOME/AuthKey.p8}" EXPORT_OPTIONS="${EXPORT_OPTIONS:-$IOS_DIR/ExportOptions.plist}" ARCHIVE_PATH="${ARCHIVE_PATH:-$IOS_DIR/build/VMC.xcarchive}" EXPORT_PATH="${EXPORT_PATH:-$IOS_DIR/build/export}" SCHEME="${SCHEME:-VMC}" CONFIGURATION="${CONFIGURATION:-Release}" TEAM_ID="${TEAM_ID:-X25NYX3K49}" BUNDLE_ID="${BUNDLE_ID:-nl.vonas.mediacluster.ios}" SKIP_ARCHIVE=0 SKIP_EXPORT=0 IPA_PATH="" usage() { cat <<'EOF' Usage: upload-testflight.sh [options] --key-id ID App Store Connect API Key ID (or env KEY_ID) --issuer-id UUID App Store Connect Issuer ID (or env ISSUER_ID) --auth-key PATH Path to .p8 (default: ~/AuthKey.p8) --ipa PATH Skip archive/export; upload existing IPA --skip-archive Reuse existing xcarchive --skip-export Reuse existing IPA in export path -h, --help Show help Env: KEY_ID, ISSUER_ID, AUTH_KEY_PATH, KEYCHAIN_PASS, IOS_DIR EOF } while [[ $# -gt 0 ]]; do case "$1" in --key-id) KEY_ID="${2:-}"; shift 2 ;; --issuer-id) ISSUER_ID="${2:-}"; shift 2 ;; --auth-key) AUTH_KEY_PATH="${2:-}"; shift 2 ;; --ipa) IPA_PATH="${2:-}"; SKIP_ARCHIVE=1; SKIP_EXPORT=1; shift 2 ;; --skip-archive) SKIP_ARCHIVE=1; shift ;; --skip-export) SKIP_EXPORT=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; esac done KEY_ID="${KEY_ID:-}" ISSUER_ID="${ISSUER_ID:-}" # Derive Key ID from AuthKey_XXXXX.p8 filename if missing if [[ -z "$KEY_ID" && -f "$AUTH_KEY_PATH" ]]; then base="$(basename "$AUTH_KEY_PATH")" if [[ "$base" =~ ^AuthKey_([A-Z0-9]+)\.p8$ ]]; then KEY_ID="${BASH_REMATCH[1]}" echo "KEY_ID derived from filename: $KEY_ID" fi fi # Also accept ~/AuthKey_XXXXX.p8 if ~/AuthKey.p8 missing if [[ ! -f "$AUTH_KEY_PATH" ]]; then for cand in "$HOME"/AuthKey_*.p8; do if [[ -f "$cand" ]]; then AUTH_KEY_PATH="$cand" base="$(basename "$cand")" if [[ -z "$KEY_ID" && "$base" =~ ^AuthKey_([A-Z0-9]+)\.p8$ ]]; then KEY_ID="${BASH_REMATCH[1]}" fi echo "Using API key file: $AUTH_KEY_PATH" break fi done fi die() { echo "ERROR: $*" >&2; exit 1; } [[ -n "$KEY_ID" ]] || die "KEY_ID ontbreekt (env KEY_ID of --key-id, of AuthKey_XXXXX.p8)" [[ -n "$ISSUER_ID" ]] || die "ISSUER_ID ontbreekt (env ISSUER_ID of --issuer-id)" [[ -f "$AUTH_KEY_PATH" ]] || die "API key niet gevonden: $AUTH_KEY_PATH (scp AuthKey*.p8 naar ~ op de Mac)" # Private keys for altool/transporter must live in a predictable API Keys folder API_KEYS_DIR="$HOME/.appstoreconnect/private_keys" mkdir -p "$API_KEYS_DIR" API_KEY_DEST="$API_KEYS_DIR/AuthKey_${KEY_ID}.p8" if [[ "$(cd "$(dirname "$AUTH_KEY_PATH")" && pwd)/$(basename "$AUTH_KEY_PATH")" != "$API_KEY_DEST" ]]; then cp -f "$AUTH_KEY_PATH" "$API_KEY_DEST" chmod 600 "$API_KEY_DEST" fi # Also keep canonical ~/AuthKey.p8 symlink/copy for convenience if [[ ! -f "$HOME/AuthKey.p8" ]]; then cp -f "$AUTH_KEY_PATH" "$HOME/AuthKey.p8" chmod 600 "$HOME/AuthKey.p8" fi echo "=== VMC TestFlight upload ===" echo "KEY_ID=$KEY_ID" echo "ISSUER_ID=$ISSUER_ID" echo "AUTH_KEY=$AUTH_KEY_PATH" echo "BUNDLE_ID=$BUNDLE_ID" echo "TEAM_ID=$TEAM_ID" # Unlock keychain if password provided (headless codesign) if [[ -n "${KEYCHAIN_PASS:-}" ]]; then echo "Unlocking login keychain..." security unlock-keychain -p "$KEYCHAIN_PASS" "$HOME/Library/Keychains/login.keychain-db" || \ security unlock-keychain -p "$KEYCHAIN_PASS" "$HOME/Library/Keychains/login.keychain" || true security set-keychain-settings -t 3600 -u "$HOME/Library/Keychains/login.keychain-db" 2>/dev/null || true fi security list-keychains -s "$HOME/Library/Keychains/login.keychain-db" /Library/Keychains/System.keychain || true cd "$IOS_DIR" mkdir -p build "$EXPORT_PATH" store # Ensure ExportOptions.plist for App Store if [[ ! -f "$EXPORT_OPTIONS" ]]; then cat > "$EXPORT_OPTIONS" < method app-store teamID ${TEAM_ID} signingStyle automatic uploadSymbols destination export manageAppVersionAndBuildNumber PLIST echo "Created $EXPORT_OPTIONS" fi if [[ "$SKIP_ARCHIVE" -eq 0 ]]; then echo "=== Archive (Release / generic iOS) ===" rm -rf "$ARCHIVE_PATH" xcodebuild \ -project VMC.xcodeproj \ -scheme "$SCHEME" \ -configuration "$CONFIGURATION" \ -destination 'generic/platform=iOS' \ -archivePath "$ARCHIVE_PATH" \ -allowProvisioningUpdates \ DEVELOPMENT_TEAM="$TEAM_ID" \ CODE_SIGN_STYLE=Automatic \ archive echo "ARCHIVE_OK: $ARCHIVE_PATH" fi if [[ "$SKIP_EXPORT" -eq 0 ]]; then [[ -d "$ARCHIVE_PATH" ]] || die "Archive ontbreekt: $ARCHIVE_PATH" echo "=== Export IPA ===" rm -rf "$EXPORT_PATH" mkdir -p "$EXPORT_PATH" xcodebuild -exportArchive \ -archivePath "$ARCHIVE_PATH" \ -exportPath "$EXPORT_PATH" \ -exportOptionsPlist "$EXPORT_OPTIONS" \ -allowProvisioningUpdates IPA_PATH="$(find "$EXPORT_PATH" -name '*.ipa' -type f | head -1)" [[ -n "$IPA_PATH" && -f "$IPA_PATH" ]] || die "Geen IPA na export in $EXPORT_PATH" VER="$(/usr/libexec/PlistBuddy -c 'Print :ApplicationProperties:CFBundleShortVersionString' "$ARCHIVE_PATH/Info.plist" 2>/dev/null || echo '0.5.0')" BUILD="$(/usr/libexec/PlistBuddy -c 'Print :ApplicationProperties:CFBundleVersion' "$ARCHIVE_PATH/Info.plist" 2>/dev/null || echo 'unknown')" cp -f "$IPA_PATH" "$IOS_DIR/store/VMC-${VER}-${BUILD}.ipa" echo "IPA_OK: $IPA_PATH (copied to store/VMC-${VER}-${BUILD}.ipa)" fi if [[ -z "${IPA_PATH:-}" ]]; then IPA_PATH="$(find "$EXPORT_PATH" -name '*.ipa' -type f 2>/dev/null | head -1 || true)" fi [[ -n "${IPA_PATH:-}" && -f "$IPA_PATH" ]] || die "Geen IPA om te uploaden" echo "=== Upload via altool (API key) ===" # Prefer modern notary/upload path; altool still works for ASC API key upload set +e xcrun altool --upload-app \ --type ios \ --file "$IPA_PATH" \ --apiKey "$KEY_ID" \ --apiIssuer "$ISSUER_ID" \ 2>&1 | tee "$IOS_DIR/build/testflight-upload.log" UPLOAD_EC=${PIPESTATUS[0]} set -e if [[ "$UPLOAD_EC" -ne 0 ]]; then echo "altool failed (exit $UPLOAD_EC); trying iTMSTransporter..." TRANSPORTER="/Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/itms/bin/iTMSTransporter" if [[ -x "$TRANSPORTER" ]]; then "$TRANSPORTER" -m upload \ -assetFile "$IPA_PATH" \ -apiKey "$KEY_ID" \ -apiIssuer "$ISSUER_ID" \ -v informational 2>&1 | tee -a "$IOS_DIR/build/testflight-upload.log" else die "Upload mislukt (altool exit $UPLOAD_EC) en iTMSTransporter niet gevonden" fi fi echo "UPLOAD_OK $(date)" echo "IPA=$IPA_PATH" echo "Log: $IOS_DIR/build/testflight-upload.log"