---
description: Build and upload iOS app to TestFlight for beta testing
---

## Release to TestFlight

Push a new iOS beta build to TestFlight. Always run every step in order.
The state file `mobile_app/ios/BUILD_STATE.md` is the source of truth for
build numbers — read it first, update it last.

---

## Fresh Mac Setup (from scratch)

### 1. Install tools

```bash
xcode-select --install
```

Then install full **Xcode** from the Mac App Store.

### 2. Sign in to Apple Developer

Xcode → Settings → Accounts → **+** → Apple ID → sign in.
Xcode auto-downloads provisioning profiles and signing certificates.

### 3. Install CocoaPods

```bash
brew install cocoapods
```

### 4. Clone and install pods

```bash
git clone https://github.com/saravanakumardb1/learning_voice_ai_agent.git
cd learning_voice_ai_agent/mobile_app/ios && pod install
```

---

## Pre-Flight Quality Gate

### 0a. Read current build state

// turbo
```bash
cat mobile_app/ios/BUILD_STATE.md
```

Note the **Current Build** number. The next release will be `current + 1`.

### 0b. Verify clean working tree

// turbo
```bash
git status && git diff --quiet && git diff --cached --quiet && echo "✅ Clean" || (echo "❌ ERROR: Uncommitted changes — commit or stash first" && exit 1)
```

If not clean, stop and commit pending changes before continuing.

### 0c. Compile all targets (catches missing files + type errors)

```bash
xcodebuild build \
  -workspace mobile_app/ios/LysnrAI.xcworkspace \
  -scheme LysnrAI \
  -configuration Debug \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  CODE_SIGN_IDENTITY=- \
  CODE_SIGNING_ALLOWED=NO \
  2>&1 | grep -E "error:|BUILD SUCCEEDED|BUILD FAILED" | head -20
```

Must end with `BUILD SUCCEEDED`. If it fails, fix errors before continuing.

### 0d. Verify pbxproj ↔ filesystem consistency

// turbo
```bash
for f in mobile_app/ios/LysnrKeyboard/*.swift; do
  fname=$(basename "$f")
  if ! grep -q "$fname" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj; then
    echo "❌ ERROR: $fname not in project.pbxproj" && exit 1
  fi
done && echo "✅ All keyboard .swift files are in the Xcode project"
```

### 0e. Verify BUILD_STATE.md matches project.pbxproj

// turbo
```bash
state_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}')
pbx_builds=$(grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj | awk '{print $3}' | tr -d ';' | sort -u)
if [ "$(echo "$pbx_builds" | wc -l | tr -d ' ')" -ne 1 ]; then
  echo "❌ ERROR: project.pbxproj has inconsistent build numbers:" && echo "$pbx_builds" && exit 1
fi
pbx_build=$(echo "$pbx_builds")
if [ "$state_build" != "$pbx_build" ]; then
  echo "❌ ERROR: BUILD_STATE ($state_build) does not match pbxproj ($pbx_build)" && exit 1
fi
echo "✅ BUILD_STATE and project.pbxproj are consistent (build $pbx_build)"
```

### 0f. Pending upload retry guard (quota saver)

If `BUILD_STATE.md` marks the current build as **Pending Upload Retry**, do NOT bump the build.
Retry **step 5 only** with the existing archive path `/tmp/LysnrAI_<current>.xcarchive`.

// turbo
```bash
if grep -q "Current / Pending Upload Retry" mobile_app/ios/BUILD_STATE.md; then
  current_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}')
  echo "⚠️ Pending upload retry detected for build $current_build"
  echo "➡️ Skip bump/archive and run export/upload only for /tmp/LysnrAI_${current_build}.xcarchive"
fi
```

---

## Build + Upload Steps

### 1. Bump build number in project.pbxproj

Cascade edits `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` — replace ALL
occurrences of `CURRENT_PROJECT_VERSION = N;` with `CURRENT_PROJECT_VERSION = N+1;`
(there are 6 occurrences: Debug+Release for LysnrAI, LysnrKeyboard, and Tests targets).

Verify the bump took effect on all 6:

// turbo
```bash
grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj
```

All 6 lines must show the same new build number.

### 2. Commit the build number bump

```bash
git add mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj
git commit -m "chore(ios): bump build number to <N> for TestFlight release"
git push origin main
```

Commit BEFORE archiving so the build number in git always matches what was uploaded.

### 3. Install CocoaPods dependencies

// turbo
```bash
cd mobile_app/ios && pod install && cd ../..
```

### 4. Archive the app

Use an absolute `/tmp` path for the archive to avoid cwd issues:

```bash
xcodebuild archive \
  -workspace mobile_app/ios/LysnrAI.xcworkspace \
  -scheme LysnrAI \
  -configuration Release \
  -archivePath /tmp/LysnrAI_<N>.xcarchive \
  -destination 'generic/platform=iOS' \
  2>&1 | tail -3
```

Must end with `** ARCHIVE SUCCEEDED **`.

### 5. Export + upload to App Store Connect

```bash
xcodebuild -exportArchive \
  -archivePath /tmp/LysnrAI_<N>.xcarchive \
  -exportPath mobile_app/ios/build/export<N> \
  -exportOptionsPlist scripts/ExportOptions.plist \
  2>&1 | tail -6
```

The `app-store-connect` export method auto-uploads the IPA.

**Expected output:**
```
Uploaded LysnrAI
** EXPORT SUCCEEDED **
```

**If you see `Upload limit reached`:** Apple enforces a daily upload limit per app.
Wait ~24 hours and re-run this step only (the archive at `/tmp/LysnrAI_<N>.xcarchive`
is still valid — do NOT re-archive or re-bump the build number).

**If you see `bundle version must be higher`:** The build number was already uploaded.
Go back to step 1 and bump again.

### 6. Update BUILD_STATE.md

After a successful upload, Cascade updates `mobile_app/ios/BUILD_STATE.md`:
- Set **Current Build** to the new number
- Add a row to the Build History table with the build number, status `Released`, and key changes
- Update Active Issues table if any were fixed or newly found

Then commit and push:

```bash
git add mobile_app/ios/BUILD_STATE.md
git commit -m "chore(ios): update BUILD_STATE.md for build <N>"
git push origin main
```

### 7. Auto-distribution

Internal testing group has auto-distribute enabled. After Apple finishes processing
(~5-15 min), the build appears automatically in TestFlight on testers' devices.

> **External Testing** (if needed): App Store Connect → TestFlight → add build to
> external group → fill in "What to Test" → submit for review (~24h).

---

## Alternative: Xcode GUI

1. Open `mobile_app/ios/LysnrAI.xcworkspace` (NOT `.xcodeproj`)
2. Scheme: **LysnrAI**, destination: **Any iOS Device**
3. Product → Archive → Organizer → Distribute App → App Store Connect → Upload

---

## Reference

### Key Files

| File | Purpose |
|------|---------|
| `mobile_app/ios/BUILD_STATE.md` | **Source of truth** — current build number, history, active issues |
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Build settings (6× CURRENT_PROJECT_VERSION) |
| `scripts/ExportOptions.plist` | Export options (method: app-store-connect, team: 748N7QPX7J) |
| `mobile_app/ios/Podfile` | CocoaPods deps (Azure Speech SDK) |
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Keyboard extension config + privacy keys |

### Build Identity

| Field | Value |
|-------|-------|
| Team ID | `748N7QPX7J` |
| Bundle ID | `com.bytelyst.LysnrAI` |
| Keyboard Bundle ID | `com.bytelyst.LysnrAI.keyboard` |
| App Group | `group.com.bytelyst.LysnrAI` |
| Signing | Automatic |
| Archive path pattern | `/tmp/LysnrAI_<N>.xcarchive` |

### Troubleshooting

| Problem | Fix |
|---------|-----|
| `BUILD FAILED` in step 0c | Fix compile errors; run `/mobile-code-quality` for full diagnostics |
| `.swift file not in pbxproj` | Add file to Xcode target Sources build phase |
| `LysnrTelemetry not found` | Verify `LysnrTelemetry.swift` is in LysnrKeyboard target Sources |
| `ARCHIVE FAILED` | Check full output: `2>&1 \| grep error:` |
| `bundle version must be higher` | Build number already uploaded — bump again from step 1 |
| `Upload limit reached` | Apple daily limit hit — wait ~24h, re-run step 5 only (archive is reusable) |
| `No signing certificate` | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
| `Provisioning profile` error | Xcode → Target → Signing → Enable "Automatically manage signing" |
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
| Build number drift (git vs pbxproj) | Read `BUILD_STATE.md` — it is the source of truth |
