Push a TestFlight build
Automates the full TestFlight release cut: branch → build → upload → commit → PR → CI → merge.
The fastlane ios beta lane handles the heavy lifting: it queries App Store Connect for the next build number, bumps CURRENT_PROJECT_VERSION in project.pbxproj, archives and exports the .ipa, uploads it to TestFlight (without waiting for Apple to process it), and auto-commits the pbxproj change. This skill wraps that with branch discipline, the PR + CI wait, and the squash-merge.
Progress checklist
Print this unchecked at the start; re-print with - [x] as each step completes.
- Pre-flight — clean working tree, on
main, up-to-date withorigin - Branch — create
u/jimmyho/claude-code/testflight-build-{N+1}(approximate) - Build & upload —
fastlane ios betarunning in background (~10–15 min) - Verify commit — lane auto-committed
chore(release): set build number to N - Push — branch pushed to
origin - PR — chore PR opened; record PR number
- CI —
wait_ci.shrunning in background; re-invoked on completion - Merge — squash-merge; branch cleaned up
Recipe
1. Pre-flight
git status --porcelain
git branch --show-current
git fetch origin
git log HEAD..origin/main --oneline
- Dirty working tree (uncommitted or staged changes other than pbxproj): stop. The lane commits only the pbxproj; other uncommitted changes will be left behind and could confuse the reviewer. Ask the user to commit or stash first.
- Not on
main: stop. Confirm the user wants to cut a build from a non-main branch before proceeding. - Behind
origin/main: pull first:git pull --ff-only
2. Create a release branch
Read the current build number from project.pbxproj:
grep CURRENT_PROJECT_VERSION simple-recurring-budgets.xcodeproj/project.pbxproj | head -1
Extract the integer N. Create the branch before anything else — the lane's git_commit commits on whatever branch is checked out, so being on the feature branch is mandatory:
git checkout -b u/jimmyho/claude-code/testflight-build-{N+1}
The branch name uses
N+1as an approximation. The lane queries ASC for the actual next build number, which may be higher thanN+1if earlier uploads failed. The PR title is derived from the lane's auto-commit message and will carry the real number regardless.
3. Build & upload — fastlane ios beta
Run in the background (run_in_background: true):
fastlane ios beta 2>&1 | tee tmp/testflight-build.log
The lane (~10–15 min):
- Queries ASC → sets the real next build number
- Bumps
CURRENT_PROJECT_VERSIONinproject.pbxproj - Archives and exports the
.ipawith-allowProvisioningUpdates - Uploads to TestFlight (
skip_waiting_for_build_processing: true) - Auto-commits:
chore(release): set build number to <N>
Do not use the Monitor tool for ticks — it prompts on each re-arm. Track progress on 5-minute background sleep ticks instead (memory feedback_no_prompt_periodic_progress):
- Launch
sleep 300as a background Bash task (run_in_background: true). - On its completion notification: read the last 10 lines of
tmp/testflight-build.log, report one-line status, then re-arm with anothersleep 300. - Stop re-arming when the
fastlane ios betatask exits.
4. Verify the auto-commit
After the background task exits:
git log --oneline -3
tail -60 tmp/testflight-build.log
Branch on the result:
Auto-commit present (chore(release): set build number to N): Record the actual build number N from the commit subject. Proceed to step 5.
No auto-commit + log shows a build or upload error: Surface the last 80 lines of tmp/testflight-build.log and stop. The branch is clean (nothing committed), so the user can re-run after fixing the issue. Common causes and fixes:
- Signing failure — run
fastlane ios verify_authto check ASC API key auth; verify the distribution cert and provisioning profile are valid in Xcode / ASC. - Archive failure — the log will show a compiler error; fix the code and re-run from step 3.
- Upload failure (ASC HTTP 500) — transient; wait a few minutes and re-run
fastlane ios betaon the same branch. ASC will increment the build number again.
No auto-commit + log shows upload success but no commit (rare — fastlane's git_commit failed after a successful upload): the pbxproj is changed but not committed. Stage and commit it manually:
git add simple-recurring-budgets.xcodeproj/project.pbxproj
Read the actual build number from the file:
grep CURRENT_PROJECT_VERSION simple-recurring-budgets.xcodeproj/project.pbxproj | head -1
Write the commit message to tmp/commit-msg.txt with the Write tool:
chore(release): set build number to <N>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Then commit:
git commit -F tmp/commit-msg.txt
5. Push the branch
git push -u origin u/jimmyho/claude-code/testflight-build-{N+1}
6. Open the PR
Write the PR body to tmp/pr-body.md with the Write tool:
## What
* Bumped build number to <N> and uploaded build <N> to TestFlight.
## Why
New TestFlight build for internal testing.
## Test plan
- [ ] Build <N> appears in TestFlight within ~30 min of Apple processing the upload.
## Tools
- fastlane ios beta
---
## Cross-cutting concerns (PRD §6.8)
N/A — build infrastructure change only; no user-facing code.
- [x] Accessibility — N/A
- [x] Dark Mode — N/A
- [x] Localization — N/A
- [x] Mixpanel — N/A
- [x] UI test screen objects — N/A
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create the PR:
gh pr create --title "chore(release): set build number to <N>" --body-file tmp/pr-body.md
Record the PR number from the output.
7. Wait for CI
Give CI a moment to register the push, then get the run ID:
sleep 15
gh run list --branch u/jimmyho/claude-code/testflight-build-{N+1} --limit 1 --json databaseId --jq '.[0].databaseId'
Wait in the background (run_in_background: true):
bash scripts/wait_ci.sh <run-id> <pr-number>
The active CI gates are lint, secrets scan, and the i18n translation gate (the macOS build/test jobs are paused for the June 2026 Actions budget; see AGENTS.md). wait_ci.sh polls until the run completes, then prints the step breakdown and PR mergeability. The script times out after ~80 min.
8. Squash-merge and clean up
After wait_ci.sh exits and all checks passed:
gh pr merge <pr-number> --squash --delete-branch
git checkout main
git pull --ff-only
git branch -D u/jimmyho/claude-code/testflight-build-{N+1}
git fetch --prune
Report success: "Build is in TestFlight (may take ~30 min for Apple to process) and the PR is merged to main."
Commands reference
git status --porcelain
git branch --show-current
git fetch origin && git log HEAD..origin/main --oneline # check up to date
grep CURRENT_PROJECT_VERSION simple-recurring-budgets.xcodeproj/project.pbxproj | head -1
git checkout -b u/jimmyho/claude-code/testflight-build-{N+1}
fastlane ios beta 2>&1 | tee tmp/testflight-build.log # build + upload + auto-commit
git log --oneline -3 # verify auto-commit
git push -u origin u/jimmyho/claude-code/testflight-build-{N+1}
gh pr create --title "chore(release): set build number to <N>" --body-file tmp/pr-body.md
sleep 15
gh run list --branch ... --limit 1 --json databaseId --jq '.[0].databaseId'
bash scripts/wait_ci.sh <run-id> <pr-number>
gh pr merge <pr-number> --squash --delete-branch
git checkout main && git pull --ff-only
git branch -D u/jimmyho/claude-code/testflight-build-{N+1}
git fetch --prune