Lab 10.1 — Fastlane Pipeline
Goal: build a match + gym + pilot pipeline that uploads a fresh build to TestFlight with one command. By the end you’ll have a real, reusable Fastfile.
Time: 60–120 minutes (mostly Apple account setup if first time).
Prereqs: Paid Apple Developer account, a private GitHub repo for certs, an App Store Connect API key.
Setup
- Open or create a SwiftUI iOS app:
FastlaneLabwith bundle IDcom.yourname.fastlanelab(use your reverse-domain). - In App Store Connect, create the app record (My Apps → +) before automation can talk to it.
- Install fastlane:
When prompted: “Manual setup” (option 4) — we’ll write the Fastfile ourselves.gem install fastlane -NV --no-document cd FastlaneLab fastlane init - Create a private empty GitHub repo:
yourname/fastlane-lab-certs. Generate a fine-grained PAT scoped only to that repo (Contents: Read & Write). - App Store Connect → Users and Access → Keys → “+ Generate”. Role: App Manager. Download the
.p8. Note Key ID + Issuer ID.
Build (cert sync first, then upload lane)
Step 1 — Configure match
Create fastlane/Matchfile:
git_url("https://github.com/yourname/fastlane-lab-certs")
storage_mode("git")
type("development")
app_identifier(["com.yourname.fastlanelab"])
username("you@example.com") # only used for Apple Developer Portal API
team_id("ABCDE12345") # your Apple team ID
Run once locally to seed the repo:
# Pick a strong passphrase; record in 1Password as MATCH_PASSWORD
fastlane match development
fastlane match appstore
Watch the certs repo populate with encrypted artifacts.
Step 2 — Configure the App Store Connect API key in fastlane
mkdir -p fastlane
mv ~/Downloads/AuthKey_AAAA1111BB.p8 fastlane/AuthKey.p8
# Build a JSON wrapper fastlane expects
cat > fastlane/AuthKey.json <<EOF
{
"key_id": "AAAA1111BB",
"issuer_id":"69a6de70-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"key": "$(awk '{printf "%s\\n", $0}' fastlane/AuthKey.p8)",
"duration": 1200,
"in_house": false
}
EOF
# Make sure these are .gitignored
echo "fastlane/AuthKey.p8" >> .gitignore
echo "fastlane/AuthKey.json" >> .gitignore
Step 3 — Write the Fastfile
fastlane/Fastfile:
fastlane_version "2.220.0"
default_platform :ios
APP_ID = "com.yourname.fastlanelab"
SCHEME = "FastlaneLab"
PROJECT = "FastlaneLab.xcodeproj"
API_KEY = "fastlane/AuthKey.json"
platform :ios do
before_all do
setup_ci if is_ci
end
desc "Sync development certs/profiles"
lane :dev_certs do
match(type: "development", app_identifier: APP_ID, readonly: is_ci)
end
desc "Sync App Store certs/profiles"
lane :appstore_certs do
match(type: "appstore", app_identifier: APP_ID, readonly: is_ci)
end
desc "Build and upload to TestFlight"
lane :beta do
appstore_certs
increment_build_number(xcodeproj: PROJECT)
build_app(
project: PROJECT,
scheme: SCHEME,
export_method: "app-store",
export_options: {
signingStyle: "manual",
provisioningProfiles: { APP_ID => "match AppStore #{APP_ID}" }
},
output_directory: "build",
output_name: "FastlaneLab.ipa"
)
upload_to_testflight(
api_key_path: API_KEY,
ipa: "build/FastlaneLab.ipa",
skip_waiting_for_build_processing: false,
distribute_external: false, # internal only for first run
changelog: "Initial TestFlight build via Fastlane lab"
)
UI.success("Beta #{lane_context[SharedValues::BUILD_NUMBER]} live on TestFlight!")
end
error do |lane, exception|
UI.error("Lane #{lane} failed: #{exception.message}")
end
end
Step 4 — Run it
bundle exec fastlane beta # or: fastlane beta
Watch the output:
[12:34:01] ✓ match: 1 profile installed
[12:34:11] ✓ build_app: building...
[12:36:42] ✓ build_app: archived
[12:36:50] ✓ build_app: exported FastlaneLab.ipa
[12:36:55] ↑ upload_to_testflight: uploading...
[12:39:21] ✓ upload_to_testflight: build 2 visible in TestFlight
[12:39:21] 🎉 Beta 2 live on TestFlight!
Confirm: open App Store Connect → TestFlight; your build should be in Processing → Ready to Test.
Stretch
- External group — change
distribute_external: true, addgroups: ["Beta Wide"], submit for beta review automatically. - Changelog from Git — replace the static
changelog:string withchangelog: changelog_from_git_commits(commits_count: 10). - Slack hook — install
fastlane add_plugin slack, append aslack(message: ...)call after upload, store the webhook in.env.secret. - Git auto-bump commit — add
commit_version_bump(message: "chore: build [skip ci]")andpush_to_git_remote. - CI runner — port this Fastfile to the GitHub Actions workflow from chapter 7. Same lane, just running on
macos-15.
Notes
- If
matchcomplains about “Could not create another Distribution certificate” — you’ve hit the 2-cert-per-account limit. Revoke unused ones in the portal. increment_build_numberwrites to the.xcodeproj. Commit the change or setcommit_version_bumpto do it automatically.- The first TestFlight upload always takes longer than later ones — Apple builds your symbols index. Patience.
- If TestFlight processing fails, the email from Apple usually explains why. Common: missing
ITSAppUsesNonExemptEncryptionin Info.plist (set tofalsefor most apps).