| name | ship-fastlane |
| description | Vanilla fastlane TestFlight pipeline. ONLY invoke this skill when the user explicitly types /ship-fastlane. Never trigger automatically on "ship", "TestFlight", or general release requests — those go to /expo-local-ship instead. Use when shipping from a machine without the maintainer signing env vars, when wiring up CI, or as a vanilla fallback if /expo-local-ship is misbehaving. Before running `bundle exec fastlane beta`, confirm with the user and wait for explicit approval.
|
Ship via Fastlane
Standard fastlane setup for archiving the iOS app and uploading to TestFlight.
TestFlight only — no App Store submission, no polling until VALID, no
custom signing bootstrap. The lane is intentionally minimal; if you need the
full pipeline, use /expo-local-ship.
When this skill applies
Only when the user explicitly types /ship-fastlane.
Do not invoke on:
- "ship", "ship it", "ship to TestFlight" — those go to
/expo-local-ship.
- "build", "archive", "upload" — same.
- "fastlane" appearing in conversation context — wait for the explicit slash command.
If a user asks "should I ship?" or "how do I ship?", point them at
docs/deployment.md and let them pick a path.
Project quick reference
| Key | Value |
|---|
| Workspace | ios/$APP_SCHEME.xcworkspace |
| Scheme | from $APP_SCHEME env var |
| Bundle ID | from $APP_BUNDLE_ID env var |
| Marketing version | read from app.json → expo.version |
| Build number | derived from App Store Connect on every run |
| Signing | automatic, via ASC API key |
| Output | TestFlight (no App Store submission) |
Prerequisites
- Ruby + bundler — System Ruby on macOS is fine.
gem list bundler should show something.
- App Store Connect API key —
.p8 file with the App Manager role, downloaded once from https://appstoreconnect.apple.com/access/integrations/api. Store outside the repo (~/.appstoreconnect/keys/ recommended).
fastlane/.env — local, gitignored. Copy from fastlane/.env.example and fill in:
APP_BUNDLE_ID
APP_SCHEME
APPLE_TEAM_ID
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH
bundle install — once per fresh clone. Installs fastlane into vendor/bundle/ (gitignored).
ios/ directory present — either persistent or generated by a prior npx expo prebuild --platform ios. The lane does not run prebuild.
Daily command
bundle exec fastlane beta
That's the whole thing. Don't pass extra flags unless you know why.
What the lane does (in order)
-
Pre-ship checks — shells out to ./scripts/git-sync-check.sh. Fails fast if:
app.json has uncommitted edits (we don't ship from a dirty tree).
- Local
main is behind origin/main (another machine may have pushed a build-number bump).
- On a non-
main branch, prints a warning but proceeds.
See .Codex/skills/_shared/pre-ship-checks.md for the rationale behind these checks. They mirror the gates ./scripts/ship-ios.sh runs.
-
Authenticate to ASC — app_store_connect_api_key with the three ASC_* env vars. No Apple ID prompt, no 2FA.
-
Determine next build number — latest_testflight_build_number for the current marketing version, then + 1. ASC is the source of truth.
-
Write build number into app.json (for Info.plist regeneration after a future expo prebuild) and directly into ios/$APP_SCHEME/Info.plist (used by the in-flight archive).
-
Archive + sign + export — build_app with the inline export_options hash (teamID, signingStyle: automatic, symbols), passing ASC API key flags via xcargs so xcodebuild can refresh provisioning profiles non-interactively.
-
Upload to TestFlight — upload_to_testflight with skip_waiting_for_build_processing: true. Returns as soon as the upload completes; ASC processing finishes asynchronously.
-
Restore app.json — git restore app.json. The build number that was written in step 4 was derived data for this archive only; committing it would create churn. Working tree returns to clean.
What the lane does NOT do
| Missing | What to use instead |
|---|
| Maintainer signing env bootstrap | ./scripts/ship-ios.sh (Path A in docs/deployment.md) |
expo prebuild if ios/ is missing | Run npx expo prebuild --platform ios first, or use ship-ios.sh |
| Poll ASC until the build is VALID | Check TestFlight manually, or use ship-ios.sh |
| Submit for App Store review | ship-ios.sh --target production |
| Auto-install npm dependencies | Run npm install (or your package manager) first |
After a successful run
Nothing to commit. app.json was restored by step 7. git status will be clean.
The uploaded build appears on TestFlight after ASC processing finishes — usually 5–30 minutes. You can refresh the TestFlight tab in App Store Connect, or query the ASC API.
Troubleshooting
Error packaging up the application
You're missing one or more of the export config pieces. Verify:
APPLE_TEAM_ID is set in fastlane/.env.
ASC_KEY_PATH points at a .p8 file that actually exists (ls $ASC_KEY_PATH).
- The Xcode project's signing is set to Automatic (the lane doesn't override this — whatever the project says, applies).
Error setting value 'app-store-connect' for option 'export_method'
This means someone changed export_method: in the Fastfile to app-store-connect. Fastlane only accepts the legacy app-store. Revert to export_method: "app-store".
Lane refuses to run because main is behind origin/main
Another machine (or you, on another laptop) pushed commits since you last pulled. Run:
git checkout main
git pull --ff-only
Then re-run the lane. This is intentional — see _shared/pre-ship-checks.md.
Lane refuses to run because app.json is dirty
Commit or stash the changes first. If your dirty app.json is just the lane's own previous write that wasn't restored (e.g. the lane crashed before the cleanup step):
git restore app.json
Missing env var
ENV.fetch("APP_BUNDLE_ID") (or similar) raised KeyError. Check fastlane/.env against fastlane/.env.example — make sure every required field is filled in.
Anti-patterns
- Don't pass
--no-build or --no-upload flags to skip parts of the lane. Either the whole thing runs or you're not actually shipping.
- Don't commit
app.json after a run. The lane already restored it; if you see "modified: app.json" in git status after the lane finished, something went wrong with step 7 — fix that rather than committing the bump.
- Don't add new lanes for "staging," "preview," etc. without a clear scope. The simplicity is the point. Use
fastlane/.env.production / .env.staging for environment switching, not new lanes.
- Don't auto-fire on "ship" — that's
/expo-local-ship's job. Read the trigger boundary at the top of this file.
See also