Ir al contenido

TestFlight Cheat Sheet

Overview

TestFlight is Apple’s official beta testing service for distributing pre-release versions of iOS, iPadOS, macOS, watchOS, tvOS, and visionOS applications to testers before App Store release. It allows developers to invite up to 100 internal testers (team members) and up to 10,000 external testers per app to install and test builds. TestFlight automatically collects crash reports, allows testers to submit feedback with screenshots, and provides build management through App Store Connect.

TestFlight handles the complexity of beta distribution by managing provisioning, installation, and update notifications. Internal testing builds are available immediately after processing, while external testing builds require a brief Beta App Review before distribution. Builds remain active for 90 days, and the TestFlight app on testers’ devices automatically notifies them of new builds. The service integrates with Xcode, fastlane, and CI/CD pipelines through the App Store Connect API.

Installation and Setup

# TestFlight is managed through App Store Connect
# https://appstoreconnect.apple.com

# Prerequisites:
# - Apple Developer Program membership ($99/year)
# - App registered in App Store Connect
# - Xcode with valid signing certificates

# Testers install the TestFlight app:
# iOS/iPadOS: App Store > TestFlight
# macOS: App Store > TestFlight (macOS 12+)

# Upload builds via Xcode
# Product > Archive > Distribute App > App Store Connect

# Upload via command line (altool - legacy)
xcrun altool --upload-app \
  --type ios \
  --file MyApp.ipa \
  --apiKey "YOUR_KEY_ID" \
  --apiIssuer "YOUR_ISSUER_ID"

# Upload via command line (notarytool for newer uploads)
xcrun notarytool submit MyApp.ipa \
  --apple-id developer@example.com \
  --team-id ABCDEF1234 \
  --password "@keychain:AC_PASSWORD"

# Upload via Transporter app (GUI)
# Download from Mac App Store

# Upload via fastlane (recommended for CI/CD)
fastlane pilot upload --ipa ./build/MyApp.ipa

App Store Connect API Setup

# Generate API key in App Store Connect:
# Users and Access > Integrations > App Store Connect API
# Download .p8 key file

# Set environment variables
export APP_STORE_CONNECT_API_KEY_ID="YOUR_KEY_ID"
export APP_STORE_CONNECT_ISSUER_ID="YOUR_ISSUER_ID"
export APP_STORE_CONNECT_API_KEY_PATH="/path/to/AuthKey_KEYID.p8"
# Using App Store Connect API with Python
import jwt
import time
import requests

# Generate JWT token
def generate_token(key_id, issuer_id, key_path):
    with open(key_path, 'r') as f:
        private_key = f.read()
    
    payload = {
        'iss': issuer_id,
        'iat': int(time.time()),
        'exp': int(time.time()) + 1200,  # 20 minutes
        'aud': 'appstoreconnect-v1'
    }
    
    headers = {
        'alg': 'ES256',
        'kid': key_id,
        'typ': 'JWT'
    }
    
    return jwt.encode(payload, private_key, algorithm='ES256', headers=headers)

token = generate_token(key_id, issuer_id, key_path)
headers = {
    'Authorization': f'Bearer {token}',
    'Content-Type': 'application/json'
}

# List builds
response = requests.get(
    'https://api.appstoreconnect.apple.com/v1/builds',
    headers=headers,
    params={'filter[app]': 'APP_ID', 'limit': 10}
)

Build Upload Methods

# Method 1: Xcode (GUI)
# Product > Archive > Distribute App > App Store Connect > Upload

# Method 2: xcodebuild + altool
# Step 1: Archive
xcodebuild archive \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -archivePath build/MyApp.xcarchive \
  -allowProvisioningUpdates

# Step 2: Export
xcodebuild -exportArchive \
  -archivePath build/MyApp.xcarchive \
  -exportPath build/export \
  -exportOptionsPlist ExportOptions.plist

# Step 3: Upload
xcrun altool --upload-app \
  --type ios \
  --file build/export/MyApp.ipa \
  --apiKey "$API_KEY_ID" \
  --apiIssuer "$ISSUER_ID"

# Method 3: fastlane
# Fastfile
lane :beta do
  build_app(
    workspace: "MyApp.xcworkspace",
    scheme: "MyApp",
    export_method: "app-store"
  )
  upload_to_testflight(
    api_key_path: "fastlane/api_key.json",
    skip_waiting_for_build_processing: false,
    distribute_external: true,
    groups: ["Beta Testers"],
    changelog: "Bug fixes and improvements"
  )
end

Tester Management

# Using fastlane pilot

# List testers
fastlane pilot list

# Add internal tester
fastlane pilot add \
  email:tester@example.com \
  first_name:John \
  last_name:Doe

# Add external tester to group
fastlane pilot add \
  email:external@example.com \
  first_name:Jane \
  last_name:Smith \
  group:"Beta Testers"

# Remove tester
fastlane pilot remove email:tester@example.com

# Export tester list
fastlane pilot export

# Create tester group
# Done via App Store Connect web interface or API

Fastlane Pilot Commands

CommandDescription
pilot uploadUpload IPA to TestFlight
pilot buildsList builds
pilot listList all testers
pilot addAdd new tester
pilot removeRemove tester
pilot findFind tester by email
pilot importImport testers from CSV
pilot exportExport testers to CSV
# Upload with options
fastlane pilot upload \
  --ipa ./build/MyApp.ipa \
  --changelog "Version 2.1 - New features and bug fixes" \
  --distribute_external true \
  --groups "Beta Testers" \
  --notify_external_testers true \
  --beta_app_review_info '{
    "contact_email": "dev@example.com",
    "contact_first_name": "John",
    "contact_last_name": "Doe",
    "contact_phone": "555-0100",
    "demo_account_name": "demo@example.com",
    "demo_account_password": "demo123"
  }'

Build Management

# List recent builds
fastlane pilot builds

# Check build processing status
fastlane pilot builds --filter-build-number 42

# Expire a build
# Via App Store Connect: TestFlight > Build > Expire Build
# Or via API

# Set build metadata
# In upload:
fastlane pilot upload \
  --ipa ./MyApp.ipa \
  --changelog "What's new in this build:\n- Fixed login bug\n- Improved performance\n- New onboarding flow" \
  --beta_app_description "Beta version of MyApp for testing" \
  --beta_app_feedback_email "feedback@example.com"

Configuration

// fastlane/api_key.json
{
  "key_id": "YOUR_KEY_ID",
  "issuer_id": "YOUR_ISSUER_ID",
  "key": "-----BEGIN EC PRIVATE KEY-----\nMIGTAg...\n-----END EC PRIVATE KEY-----",
  "in_house": false
}
# Fastfile - Complete beta workflow
platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    # Ensure clean git state
    ensure_git_clean
    
    # Increment build number
    increment_build_number(xcodeproj: "MyApp.xcodeproj")
    
    # Sync code signing
    match(type: "appstore", readonly: true)
    
    # Build
    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      configuration: "Release",
      export_method: "app-store",
      output_directory: "./build",
      output_name: "MyApp.ipa",
      include_bitcode: false,
      include_symbols: true
    )
    
    # Upload to TestFlight
    upload_to_testflight(
      api_key_path: "fastlane/api_key.json",
      skip_submission: false,
      skip_waiting_for_build_processing: false,
      distribute_external: true,
      groups: ["Internal Team", "Beta Testers"],
      changelog: changelog_from_git_commits(
        commits_count: 10,
        merge_commit_filtering: "exclude_merges"
      ),
      beta_app_review_info: {
        contact_email: "dev@example.com",
        contact_first_name: "John",
        contact_last_name: "Doe",
        contact_phone: "+15550100"
      }
    )
    
    # Commit and tag
    commit_version_bump(
      message: "Bump build number for TestFlight [skip ci]",
      xcodeproj: "MyApp.xcodeproj"
    )
    add_git_tag(tag: "beta/#{get_build_number}")
    push_to_git_remote
    
    # Notify team
    slack(
      message: "New beta build uploaded to TestFlight!",
      channel: "#mobile-releases",
      payload: {
        "Build Number" => get_build_number,
        "Version" => get_version_number
      }
    )
  end
end

Advanced Usage

# Distribute to specific group programmatically
# Using App Store Connect API
curl -X POST \
  "https://api.appstoreconnect.apple.com/v1/betaGroups/GROUP_ID/relationships/builds" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "data": [
      { "type": "builds", "id": "BUILD_ID" }
    ]
  }'

# Submit for external beta review
curl -X POST \
  "https://api.appstoreconnect.apple.com/v1/betaBuildLocalizations" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "type": "betaBuildLocalizations",
      "attributes": {
        "whatsNew": "Bug fixes and performance improvements",
        "locale": "en-US"
      },
      "relationships": {
        "build": {
          "data": { "type": "builds", "id": "BUILD_ID" }
        }
      }
    }
  }'
# CI/CD GitHub Actions workflow
# .github/workflows/testflight.yml
name: TestFlight Deploy
on:
  push:
    branches: [develop]

jobs:
  deploy:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
      
      - name: Install certificates
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
        run: bundle exec fastlane match appstore --readonly
      
      - name: Build and upload
        env:
          APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_API_KEY }}
        run: bundle exec fastlane beta

TestFlight Limits

LimitValue
Internal testers100 per app
External testers10,000 per app
Tester groups200 per app
Build expiration90 days
Build size limit4 GB (iOS), 35 GB (tvOS)
Apps per accountUnlimited
Concurrent builds10 per app
Beta review timeUsually < 24 hours

Troubleshooting

IssueSolution
Build stuck “Processing”Wait up to 30 minutes; re-upload if longer; check email for errors
”Missing compliance”Add export compliance info in App Store Connect or Info.plist
Beta review rejectedCheck review notes; ensure demo account works; verify app doesn’t crash
”Invalid IPA” upload errorVerify code signing; check entitlements match provisioning profile
Tester can’t installCheck device compatibility; verify tester accepted invite; check build not expired
”No eligible devices”Tester needs matching OS version; check minimum deployment target
Public link not workingEnsure external testing is enabled and build passed beta review
Crash reports missingEnsure dSYM symbols are uploaded; check symbols in Xcode Organizer
Build number conflictIncrement build number; each upload must have unique build number
Notification not sentCheck TestFlight notification settings; testers must have TestFlight app