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.
4 stars
0 votes
0 copies
1 view
Added September 3, 2026
developmentgorubyshellbashgitapi
Works with
api
Security analysis
A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Ship Fastlane?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/ronenmars-ship-fastlane-threadbase-mobile)
---
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`](../../../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
1. **Ruby + bundler** — System Ruby on macOS is fine. `gem list bundler` should show something.
2. **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).
3. **`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`
4. **`bundle install`** — once per fresh clone. Installs fastlane into `vendor/bundle/` (gitignored).
5. **`ios/` directory present** — either persistent or generated by a prior `npx expo prebuild --platform ios`. The lane does **not** run prebuild.
## Daily command
```bash
bundle exec fastlane beta
```
That's the whole thing. Don't pass extra flags unless you know why.
## What the lane does (in order)
1. **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 [`.claude/skills/_shared/pre-ship-checks.md`](../_shared/pre-ship-checks.md) for the rationale behind these checks. They mirror the gates `./scripts/ship-ios.sh` runs.
2. **Authenticate to ASC** — `app_store_connect_api_key` with the three `ASC_*` env vars. No Apple ID prompt, no 2FA.
3. **Determine next build number** — `latest_testflight_build_number` for the current marketing version, then `+ 1`. ASC is the source of truth.
4. **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).
5. **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.
6. **Upload to TestFlight** — `upload_to_testflight` with `skip_waiting_for_build_processing: true`. Returns as soon as the upload completes; ASC processing finishes asynchronously.
7. **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:
```bash
git checkout main
git pull --ff-only
```
Then re-run the lane. This is intentional — see [`_shared/pre-ship-checks.md`](../_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):
```bash
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
- [`docs/deployment.md`](../../../docs/deployment.md) — full comparison of ship paths (this skill is Path B).
- [`fastlane/Fastfile`](../../../fastlane/Fastfile) — the lane source.
- [`fastlane/.env.example`](../../../fastlane/.env.example) — env var template.
- [`_shared/pre-ship-checks.md`](../_shared/pre-ship-checks.md) — canonical pre-ship policy.
- `/expo-local-ship` — full pipeline (signing bootstrap, polling, production submission).
- `/ship-expo-cloud` — EAS cloud builds (opt-in, costs money).