--- name: testflight-release description: Ship a new TestFlight build of the iOS companion (TermioMobile) — resolve the next build number against App Store Connect, archive, sign, export, upload, write the What to Test notes, and distribute. Invoke when the user says 'ship a testflight build', 'update testflight', 'upload to testflight', 'new beta build', 'push the ios app to testers', '发一个 TestFlight', '更新 TestFlight', or '上传测试版'. --- # TestFlight release (iOS companion) Cuts a TestFlight build of `TermioMobile` by hand. Unlike the Mac app, **this is not automated**: `.github/workflows` has no upload step, so nothing happens when a PR merges. Every TestFlight build is a local archive + upload. Use the `asc` skill for anything beyond this path (crash triage, App Store submission, metadata). This skill is only the beta-build pipeline, with the repo-specific facts that `asc` cannot know. ## Constants | Thing | Value | | --- | --- | | App ID | `6790142237` (`sh.termio.mobile`, "Termio: Agentic Terminal") | | Bundle ID resource | `97KQ9JGYL8` | | Team | `5Y27G7B6D8`, from `ios/Signing.xcconfig` — **not** the project file | | Internal group | `7b62f012-b166-40df-ba19-e43c3a0b2c88` | | Public Beta group | `fe170244-643a-4c73-9c94-84ab44c254f9` (external, public link) | | Signing assets | `~/Library/Developer/asc-signing/termio/` (mode 700, outside the repo) | ## The build number in the repo is a lie `CURRENT_PROJECT_VERSION` in `ios/TermioMobile.xcodeproj/project.pbxproj` sits at an old value (285 at the time of writing) while App Store Connect is in the 700s. That is deliberate: the bump is applied for the archive and then thrown away. **Never commit the bump.** Resolve it against ASC, build, upload, revert: ```sh cd ios asc xcode version edit --next-build-number --app 6790142237 --platform IOS \ --project TermioMobile.xcodeproj --output json # ... archive, export, upload ... git checkout -- TermioMobile.xcodeproj/project.pbxproj ``` Uploading a build number at or below the highest one in ASC is rejected, so never guess it from the repo. ## Steps Run from a clean worktree on the commit you want to ship (normally `main`). 1. **Bump** — the command above. It reports the number it chose. 2. **Archive** (Release, signed for distribution): ```sh cd ios asc xcode archive \ --project TermioMobile.xcodeproj --scheme TermioMobile \ --configuration Release --clean \ --archive-path .asc/artifacts/TermioMobile.xcarchive \ --xcodebuild-flag=-destination --xcodebuild-flag=generic/platform=iOS \ --xcodebuild-flag=-allowProvisioningUpdates --output json ``` 3. **Export.** Automatic signing does *not* work on this Mac — Xcode's stored account token is broken (`missing Xcode-Token`), so `asc xcode export` fails with `No signing certificate "iOS Distribution" found`. Export manually against the profile instead, with `ios/.asc/ExportOptions.plist`: ```xml method app-store-connect teamID 5Y27G7B6D8 signingStyle manual signingCertificate iPhone Distribution: Jiwei Yuan (5Y27G7B6D8) provisioningProfiles sh.termio.mobileTermio iOS App Store ``` ```sh xcodebuild -exportArchive \ -archivePath .asc/artifacts/TermioMobile.xcarchive \ -exportPath .asc/artifacts/export \ -exportOptionsPlist .asc/ExportOptions.plist ``` It still logs the `DVTDeveloperAccountManager` credential error; with manual signing that is noise, not a failure. Check for `** EXPORT SUCCEEDED **`. 4. **Upload** and wait for processing: ```sh asc builds upload --app 6790142237 --ipa .asc/artifacts/export/TermioMobile.ipa --wait ``` 5. **What to Test** — required for external testers, good manners for internal: ```sh asc builds test-notes create --build-id "BUILD_ID" --locale en-US \ --whats-new "..." ``` Note the subcommand is `create`; `update` only edits notes that already exist. One paragraph, concrete, no AI attribution. 6. **Distribute.** - *Internal* needs no command. Internal groups pick up every processed build automatically — `asc builds add-groups` on the Internal group fails with "Cannot add internal group to a build." Confirm instead: `asc testflight distribution view --build-id "BUILD_ID"` → `Internal State: IN_BETA_TESTING`. - *Public Beta* is external and gated on Beta App Review. Read `externalBuildState` before doing anything: `READY_FOR_BETA_SUBMISSION` means it still needs submitting, `WAITING_FOR_BETA_REVIEW` means it is already in Apple's queue and there is nothing to do but wait, and `BETA_TESTING` means it is live on the public link. **Ask the user before submitting** — that ships to real testers. It takes **two** commands, and the first one alone looks like it worked: ```sh asc builds add-groups --build-id "BUILD_ID" --group fe170244-643a-4c73-9c94-84ab44c254f9 asc testflight review submit --build-id "BUILD_ID" --confirm ``` Adding the group reports success and changes nothing — `externalBuildState` stays `READY_FOR_BETA_SUBMISSION` until the build is actually submitted. This holds even for a marketing version that has already cleared review: 1.1 build 951 still had to be submitted on 2026-08-28, having been told here that it would skip the step. What that version buys is the *wait*, not the submission — the state went `WAITING_FOR_REVIEW` → `IN_BETA_TESTING` within a minute rather than hours. 7. **Revert** the pbxproj bump and delete `ios/.asc/` (untracked build artifacts, ~10 MB IPA plus the archive). ## Signing assets The account has one iOS Distribution certificate (`M92KPZPT43`, expires 2027-08-12) and the `Termio iOS App Store` profile (`R3N9335442`). Both were created with the `asc` CLI, and the identity is installed in the login keychain. If they are ever missing or expired, recreate them: ```sh mkdir -p ~/Library/Developer/asc-signing/termio && chmod 700 ~/Library/Developer/asc-signing/termio asc certificates create --certificate-type IOS_DISTRIBUTION --generate-csr \ --key-out ./dist.key --csr-out ./dist.csr --common-name "Jiwei Yuan" # save the base64 certificateContent from the response as dist.cer, then: openssl x509 -inform DER -in dist.cer -out dist.pem openssl pkcs12 -export -legacy -inkey dist.key -in dist.pem -out dist.p12 \ -name "iOS Distribution: Jiwei Yuan" security import dist.p12 -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign asc profiles create --name "Termio iOS App Store" --profile-type IOS_APP_STORE \ --bundle 97KQ9JGYL8 --certificate "CERT_ID" asc profiles download --id "PROFILE_ID" --output ./TermioAppStore.mobileprovision asc profiles local install --path ./TermioAppStore.mobileprovision ``` `-legacy` is not optional: OpenSSL 3 defaults to a PKCS#12 MAC that macOS cannot read, and `security import` fails with "MAC verification failed during PKCS12 import (wrong password?)" — which looks like a password problem and is not one. Certificates are account-level and limited in number. Creating or revoking one affects every machine that signs this app, so confirm with the user first. ## Notes - `ios/dev-run.sh` is the device-install loop for development; it has nothing to do with this path and never touches signing for distribution. ## Bumping `MARKETING_VERSION` vs reusing it Two different numbers move for two different reasons, and only one of them is free: | | `CURRENT_PROJECT_VERSION` (build) | `MARKETING_VERSION` | | --- | --- | --- | | Moves | every upload | rarely, deliberately | | Committed? | **never** (resolved against ASC, reverted after) | yes, its own commit | | Cost | none | **a Beta App Review round-trip** | **The rule: reuse the marketing version for iterative betas; bump it only when you are starting a new version train.** Bumping is not free. The *first* build of a new marketing version has to clear Apple's Beta App Review before any external tester can install it — it sits at `externalBuildState: WAITING_FOR_BETA_REVIEW`, typically hours. Every later build of that same version still has to be submitted — see step 6, the submission is never automatic — but clears in about a minute instead of hours. Internal testers are never affected either way; they get every processed build immediately (`internalBuildState: IN_BETA_TESTING`). So a bump costs one review wait and buys nothing on its own. Bump when the version is about to mean something — an App Store submission, or a batch of work you want to name — and take the review hit once, up front, so the builds that follow it ship instantly. Consequences worth knowing before you reach for it: - **Pushing another build will not shorten a pending review.** A rebuild of the same commit is identical code queued behind the same review. If external access is what you are waiting for, waiting *is* the action. - **Check state before building anything:** `asc testflight distribution view --build-id "BUILD_ID"`. If external already reads `WAITING_FOR_BETA_REVIEW`, there is nothing useful to upload. - Review needs the Beta App Review contact, review notes, and What to Test in every locale you ship. Missing any of them turns the wait into a rejection. Observed 2026-08-13 on 1.1 (build 947): internal `IN_BETA_TESTING` within minutes, external `WAITING_FOR_BETA_REVIEW` — the one-time cost of the 1.0 → 1.1 bump, paid so later 1.1 builds go straight out. An App Store *release* (as opposed to a beta train) is a separate decision — see `docs/RELEASING.md` and the `asc` skill's release-flow reference.