#!/usr/bin/env bash set -euo pipefail # ── ChronoMind — TestFlight Release Script ────────────────────────── # Run from: learning_ai_clock/ios/ # Prerequisites: Xcode 16+, xcodegen, App Store Connect API key # Usage: bash release-testflight.sh # bash release-testflight.sh --skip-build # re-upload existing archive SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$SCRIPT_DIR" # ── Configuration ──────────────────────────────────────────────────── SCHEME="ChronoMind" PROJECT="ChronoMind.xcodeproj" BUNDLE_ID="com.saravana.chronomind" TEAM_ID="748N7QPX7J" EXPORT_OPTIONS="ExportOptions.plist" # App Store Connect API Key (create at https://appstoreconnect.apple.com/access/integrations/api) ASC_KEY_ID="${ASC_KEY_ID:-PPATU9GL73}" ASC_ISSUER_ID="${ASC_ISSUER_ID:-1dbc2980-1621-4fb9-940b-e28257e6322c}" ASC_KEY_PATH="${ASC_KEY_PATH:-$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8}" # ── Helpers ────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' step() { echo -e "\n${CYAN}▸ $1${NC}"; } ok() { echo -e "${GREEN}✅ $1${NC}"; } warn() { echo -e "${YELLOW}⚠️ $1${NC}"; } fail() { echo -e "${RED}❌ $1${NC}"; exit 1; } # ── Parse args ─────────────────────────────────────────────────────── SKIP_BUILD=false for arg in "$@"; do case "$arg" in --skip-build) SKIP_BUILD=true ;; --help|-h) echo "Usage: bash release-testflight.sh [--skip-build]"; exit 0 ;; esac done # ── Pre-flight checks ─────────────────────────────────────────────── step "Pre-flight checks" command -v xcodebuild >/dev/null 2>&1 || fail "xcodebuild not found — install Xcode" command -v xcodegen >/dev/null 2>&1 || fail "xcodegen not found — brew install xcodegen" [ -f "$ASC_KEY_PATH" ] || fail "API key not found at $ASC_KEY_PATH Create one at https://appstoreconnect.apple.com/access/integrations/api Save as ~/.appstoreconnect/private_keys/AuthKey_.p8 Then export ASC_KEY_ID= ASC_ISSUER_ID=" [ -f "$EXPORT_OPTIONS" ] || fail "ExportOptions.plist not found in ios/" [ -f "project.yml" ] || fail "project.yml not found — are you in the ios/ directory?" # Check sibling SDK exists SDK_PATH="../../learning_ai_common_plat/packages/swift-platform-sdk/Package.swift" [ -f "$SDK_PATH" ] || fail "ByteLystPlatformSDK not found at $SDK_PATH Ensure learning_ai_common_plat is cloned as a sibling directory" ok "All pre-flight checks passed" # ── Read current build number ──────────────────────────────────────── step "Reading current build number" CURRENT_BUILD=$(grep -m1 "CURRENT_PROJECT_VERSION" project.yml | awk -F'"' '{print $2}') [ -z "$CURRENT_BUILD" ] && fail "Could not read CURRENT_PROJECT_VERSION from project.yml" NEXT_BUILD=$((CURRENT_BUILD + 1)) echo " Current: $CURRENT_BUILD → Next: $NEXT_BUILD" if [ "$SKIP_BUILD" = true ]; then ARCHIVE_PATH="/tmp/${SCHEME}_${CURRENT_BUILD}.xcarchive" [ -d "$ARCHIVE_PATH" ] || fail "No archive found at $ARCHIVE_PATH — run without --skip-build first" warn "Skipping build — using existing archive at $ARCHIVE_PATH" NEXT_BUILD=$CURRENT_BUILD else # ── Bump build number ──────────────────────────────────────────── step "Bumping build number to $NEXT_BUILD" sed -i '' "s/CURRENT_PROJECT_VERSION: \"$CURRENT_BUILD\"/CURRENT_PROJECT_VERSION: \"$NEXT_BUILD\"/g" project.yml ok "project.yml updated" # ── Regenerate Xcode project ───────────────────────────────────── step "Generating Xcode project" xcodegen generate ok "Xcode project generated" # ── Verify build ───────────────────────────────────────────────── step "Verifying debug build" xcodebuild build \ -project "$PROJECT" \ -scheme "$SCHEME" \ -configuration Debug \ -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \ CODE_SIGN_IDENTITY=- CODE_SIGNING_ALLOWED=NO \ 2>&1 | tail -1 # ── Archive ────────────────────────────────────────────────────── ARCHIVE_PATH="/tmp/${SCHEME}_${NEXT_BUILD}.xcarchive" step "Archiving build $NEXT_BUILD" xcodebuild archive \ -project "$PROJECT" \ -scheme "$SCHEME" \ -configuration Release \ -archivePath "$ARCHIVE_PATH" \ -destination 'generic/platform=iOS' \ 2>&1 | tail -1 [ -d "$ARCHIVE_PATH" ] || fail "Archive not found at $ARCHIVE_PATH" ok "Archive succeeded: $ARCHIVE_PATH" # ── Commit ─────────────────────────────────────────────────────── step "Committing build number bump" cd "$SCRIPT_DIR/.." git add ios/project.yml ios/ChronoMind.xcodeproj/ git commit -m "chore(ios): bump build number to $NEXT_BUILD for TestFlight release" || warn "Nothing to commit" cd "$SCRIPT_DIR" fi # ── Export + Upload ────────────────────────────────────────────────── EXPORT_PATH="/tmp/${SCHEME}_export${NEXT_BUILD}" step "Exporting and uploading build $NEXT_BUILD to TestFlight" rm -rf "$EXPORT_PATH" xcodebuild -exportArchive \ -archivePath "$ARCHIVE_PATH" \ -exportPath "$EXPORT_PATH" \ -exportOptionsPlist "$EXPORT_OPTIONS" \ -allowProvisioningUpdates \ -authenticationKeyPath "$ASC_KEY_PATH" \ -authenticationKeyID "$ASC_KEY_ID" \ -authenticationKeyIssuerID "$ASC_ISSUER_ID" \ 2>&1 | tail -5 # ── Verify upload ──────────────────────────────────────────────────── if [ -f "$EXPORT_PATH/${SCHEME}.ipa" ]; then ok "Upload succeeded! Build $NEXT_BUILD sent to TestFlight" else # Check if upload succeeded despite no local IPA (destination: upload) if xcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" -exportPath "$EXPORT_PATH" -exportOptionsPlist "$EXPORT_OPTIONS" 2>&1 | grep -q "Upload succeeded"; then ok "Upload succeeded! Build $NEXT_BUILD sent to TestFlight" fi fi # ── Update BUILD_STATE.md ──────────────────────────────────────────── step "Updating BUILD_STATE.md" BUILD_STATE="BUILD_STATE.md" if [ ! -f "$BUILD_STATE" ]; then cat > "$BUILD_STATE" << 'HEREDOC' # iOS Build State — ChronoMind ## Current Build **CURRENT_PROJECT_VERSION = 1** Marketing version: 1.0.0 --- ## Build History | Build | Status | Key Changes | |-------|--------|-------------| HEREDOC fi # Update current build number sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]*/CURRENT_PROJECT_VERSION = $NEXT_BUILD/" "$BUILD_STATE" ok "BUILD_STATE.md updated to build $NEXT_BUILD" # ── Final commit + push ───────────────────────────────────────────── step "Final commit and push" cd "$SCRIPT_DIR/.." git add ios/BUILD_STATE.md git commit -m "docs(ios): update BUILD_STATE.md for build $NEXT_BUILD" || warn "Nothing to commit" git push origin main || warn "Push failed — push manually later" cd "$SCRIPT_DIR" echo "" echo -e "${GREEN}════════════════════════════════════════════════════════${NC}" echo -e "${GREEN} ChronoMind build $NEXT_BUILD uploaded to TestFlight! ${NC}" echo -e "${GREEN} It should appear in ~15-30 minutes after processing. ${NC}" echo -e "${GREEN}════════════════════════════════════════════════════════${NC}" echo ""