13 KiB
Fastlane TestFlight Setup for CI/CD
This document describes how to configure Fastlane for automatic iOS beta deployments to TestFlight from GitHub Actions.
Overview
The CI/CD pipeline automatically builds and uploads iOS beta builds to TestFlight for every push to main, develop, or release tags. This provides:
- Automatic TestFlight distribution for beta testers
- Version management with build number increments
- Code signing handled automatically in CI
- Release notes from git commits
Prerequisites
Before setting up the CI/CD pipeline, ensure you have:
-
Apple Developer Account with:
- Paid Apple Developer Program membership ($99/year)
- App registered in App Store Connect
- TestFlight enabled for your app
-
App Store Connect Access:
- Admin or App Manager role
- App-specific password generated
-
Code Signing Certificates:
- Distribution certificate (
.p12file) - Ad Hoc or App Store provisioning profile
- Distribution certificate (
Required GitHub Secrets
Configure these secrets in your GitHub repository settings (Settings → Secrets and variables → Actions):
Certificate & Provisioning
| Secret Name | Description | How to Obtain |
|---|---|---|
IOS_P12_BASE64 |
Base64-encoded distribution certificate | See Exporting Certificate |
IOS_P12_PASSWORD |
Password for the .p12 certificate | Password you set when exporting |
IOS_PROVISION_PROFILE_BASE64 |
Base64-encoded provisioning profile | See Exporting Provisioning Profile |
Apple Account Authentication
| Secret Name | Description | How to Obtain |
|---|---|---|
FASTLANE_USER |
Apple ID email address | Your Apple Developer account email (e.g., hey@dz0ny.dev) |
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD |
App-specific password | See Generating App-Specific Password |
FASTLANE_SESSION |
(Optional) Fastlane session for 2FA | See Handling 2FA |
Step-by-Step Setup
1. Exporting Certificate
Export Distribution Certificate from Keychain
- Open Keychain Access on macOS
- Select login keychain in the left sidebar
- Select Certificates category
- Find your Apple Distribution certificate
- Look for "Apple Distribution: Your Name (Team ID)"
- Ensure it has a valid private key (arrow to expand)
- Right-click → Export "Apple Distribution: ..."
- Save as:
distribution.p12 - Set a strong password (you'll need this for
IOS_P12_PASSWORD) - Click Save
Convert to Base64
# Encode the .p12 file to base64
base64 -i distribution.p12 | pbcopy
The base64 string is now in your clipboard. Add it as IOS_P12_BASE64 secret.
Security Note: Delete distribution.p12 after encoding!
2. Exporting Provisioning Profile
Download from Apple Developer Portal
- Log in to Apple Developer Portal
- Navigate to Certificates, Identifiers & Profiles
- Click Profiles in the left sidebar
- Find your App Store or Ad Hoc provisioning profile
- Must match your app's bundle identifier:
com.meshcore.sar.meshcoreSarApp
- Must match your app's bundle identifier:
- Click the profile → Download
- Save as:
profile.mobileprovision
Convert to Base64
# Encode the provisioning profile to base64
base64 -i profile.mobileprovision | pbcopy
The base64 string is now in your clipboard. Add it as IOS_PROVISION_PROFILE_BASE64 secret.
Security Note: Delete profile.mobileprovision after encoding!
3. Generating App-Specific Password
App-specific passwords are required for App Store Connect API access when 2FA is enabled.
- Log in to Apple ID Account
- Navigate to Sign-In and Security → App-Specific Passwords
- Click Generate an app-specific password
- Enter a label:
GitHub Actions Fastlane - Click Create
- Copy the generated password (format:
xxxx-xxxx-xxxx-xxxx)- This will only be shown once!
- Add it as
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDsecret
4. Configure GitHub Secrets
-
Go to your GitHub repository
-
Navigate to Settings → Secrets and variables → Actions
-
Click New repository secret for each of the following:
IOS_P12_BASE64:
Paste the base64-encoded certificate from step 1IOS_P12_PASSWORD:
The password you set when exporting the .p12 certificateIOS_PROVISION_PROFILE_BASE64:
Paste the base64-encoded provisioning profile from step 2FASTLANE_USER:
hey@dz0ny.devFASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD:
xxxx-xxxx-xxxx-xxxx
5. Handling 2FA (Optional)
If your Apple account has two-factor authentication enabled, Fastlane may require a session token for long-running CI jobs.
Generate Fastlane Session
On your local machine:
# Install Fastlane if not already installed
gem install fastlane
# Generate session token
fastlane spaceauth -u hey@dz0ny.dev
You'll be prompted for:
- Apple ID password
- 2FA code (sent to your device)
Fastlane will output a session cookie. Copy the entire cookie string and add it as FASTLANE_SESSION secret.
Note: Session tokens expire after ~30 days. You'll need to regenerate periodically.
Alternative: If you don't set FASTLANE_SESSION, Fastlane will use the app-specific password, which works for most cases.
Workflow Configuration
The workflow is already configured in .github/workflows/build-multiplatform.yml:
- Triggers: Pushes to
main,develop, or version tags (v*) - Runs on: macOS runner (required for iOS builds)
- Fastlane Lane:
beta(builds IPA and uploads to TestFlight)
What the Workflow Does
- Checks out code and downloads versioned
pubspec.yaml(for tags) - Sets up Flutter and installs dependencies
- Generates localizations (
flutter gen-l10n) - Installs CocoaPods dependencies
- Sets up Ruby and installs Fastlane
- Imports code signing certificate into temporary keychain
- Imports provisioning profile
- Runs Fastlane beta lane:
- Increments build number
- Builds signed IPA
- Uploads to TestFlight
- Cleans up keychain (security)
- Uploads logs if build fails (for debugging)
Fastlane Configuration
The Fastlane configuration is in ios/fastlane/:
Fastfile
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
upload_to_testflight
end
end
Appfile
Contains your app configuration:
- Bundle identifier:
com.meshcore.sar.meshcoreSarApp - Apple ID:
hey@dz0ny.dev - Team IDs for App Store Connect and Developer Portal
Testing the Setup
-
Push a commit to
mainordevelopbranch:git checkout main git commit --allow-empty -m "Test TestFlight deployment" git push origin main -
Monitor the workflow:
- Go to Actions tab in GitHub
- Click on the running workflow
- Watch the Deploy iOS Beta to TestFlight job
-
Check TestFlight:
- Log in to App Store Connect
- Navigate to My Apps → MeshCore SAR → TestFlight
- You should see a new build processing (takes 5-15 minutes)
-
Verify build:
- Once processing completes, the build is available for testing
- Add internal testers in TestFlight
- Testers will receive notification to download via TestFlight app
Troubleshooting
"Invalid certificate" error
Cause: Certificate doesn't match the provisioning profile or is expired.
Solutions:
- Verify certificate is Distribution type (not Development)
- Check certificate expiration date in Keychain Access
- Ensure provisioning profile includes the certificate
- Re-export and re-encode both certificate and profile
"Invalid provisioning profile" error
Cause: Provisioning profile doesn't match app bundle ID or is expired.
Solutions:
- Verify bundle ID:
com.meshcore.sar.meshcoreSarApp - Check profile type: App Store or Ad Hoc (not Development)
- Regenerate profile in Apple Developer Portal
- Ensure profile includes all required devices (for Ad Hoc)
"Authentication failed" error
Cause: Apple ID credentials are incorrect or expired.
Solutions:
- Verify
FASTLANE_USERmatches your Apple ID email - Regenerate
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD - If using 2FA, regenerate
FASTLANE_SESSIONtoken - Check Apple Developer account is active (membership paid)
"Build number conflict" error
Cause: Build number already exists in TestFlight.
Solutions:
- Fastlane auto-increments build number, but may fail if out of sync
- Manually increment version in
pubspec.yaml - Or modify Fastfile to use timestamp-based build numbers:
increment_build_number( build_number: Time.now.to_i.to_s, xcodeproj: "Runner.xcodeproj" )
"Session expired" error
Cause: FASTLANE_SESSION token expired (lasts ~30 days).
Solutions:
- Regenerate session token:
fastlane spaceauth -u hey@dz0ny.dev - Update
FASTLANE_SESSIONsecret in GitHub - Or remove the secret and rely on app-specific password only
Workflow doesn't run
Cause: Conditional check failed (wrong branch or secrets missing).
Solutions:
- Verify you pushed to
mainordevelopbranch - Check all required secrets are configured in GitHub
- Review workflow logs for conditional evaluation
Advanced Configuration
Custom Release Notes
Add release notes from git commits:
Edit ios/fastlane/Fastfile:
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
# Generate changelog from git
changelog = changelog_from_git_commits(
pretty: "- %s",
merge_commit_filtering: "exclude_merges"
)
upload_to_testflight(
changelog: changelog,
skip_waiting_for_build_processing: true
)
end
Selective TestFlight Groups
Upload to specific tester groups:
upload_to_testflight(
groups: ["Internal Testers", "Beta Team"],
distribute_external: false,
skip_waiting_for_build_processing: true
)
Automatic Screenshot Upload
Generate and upload screenshots for App Store:
lane :screenshots do
snapshot
end
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
upload_to_testflight
upload_to_app_store(
screenshots_path: "./fastlane/screenshots",
skip_binary_upload: true,
skip_metadata: true
)
end
Security Best Practices
- Never commit secrets to your repository
- Rotate certificates before expiration (annually)
- Regenerate app-specific passwords periodically
- Use temporary keychains in CI (already implemented)
- Delete certificates after encoding to base64
- Limit GitHub Actions secrets to repository scope
- Review workflow logs for sensitive data leaks
- Enable branch protection for
mainanddevelop
Cost Considerations
Apple Developer Program
- $99/year for individual account
- $299/year for enterprise account
- Required for TestFlight distribution
GitHub Actions
- Free tier: 2,000 minutes/month for private repos
- macOS runners: 10x multiplier (1 min = 10 mins)
- Typical iOS build: ~15-20 minutes (~150-200 minutes counted)
- Monthly estimate: ~10 builds = ~2,000 minutes (free tier limit)
Tip: Use caching and selective triggers to minimize build minutes.
Additional Resources
- Fastlane Documentation
- Fastlane TestFlight Guide
- Apple Developer Portal
- App Store Connect
- GitHub Actions for iOS
- Fastlane Best Practices
Support
For issues with:
- Fastlane: Check Fastlane Docs or GitHub Issues
- GitHub Actions: Check workflow logs
- Code signing: Check Apple's Code Signing Guide
- TestFlight: Check App Store Connect Help