Skip to main content

Overview

pilot makes it easy to manage your TestFlight builds and testers. Upload builds, invite testers, and manage beta testing for your iOS apps through the App Store Connect API.
pilot is the best way to automate TestFlight deployments and manage beta testers.

Installation

pilot is part of fastlane. Install it with:
fastlane pilot

Quick Start

1

Upload a build

fastlane pilot upload
2

Add testers

3

List testers

fastlane pilot list

CLI Commands

fastlane pilot upload

Uploading Builds

Basic Upload

lane :beta do
  build_app
  pilot
end

Upload with Changelog

lane :beta do
  build_app
  pilot(
    changelog: "Bug fixes and performance improvements"
  )
end

Upload and Skip Distribution

pilot(
  skip_submission: true,
  skip_waiting_for_build_processing: true
)

Key Options

username
string
Your Apple ID username.
pilot(username: "[email protected]")
app_identifier
string
The bundle identifier of your app.
pilot(app_identifier: "com.example.app")
ipa
string
Path to the IPA file to upload.
pilot(ipa: "./build/MyApp.ipa")
changelog
string
Provide the ‘What to Test’ text when uploading a new build.
pilot(changelog: "Added dark mode support")
skip_submission
boolean
default:"false"
Skip the distributing action and only upload the IPA file.
pilot(skip_submission: true)
skip_waiting_for_build_processing
boolean
default:"false"
Skip waiting for build processing to complete.
pilot(skip_waiting_for_build_processing: true)
distribute_external
boolean
default:"false"
Distribute the build to external testers. Requires groups option.
pilot(
  distribute_external: true,
  groups: ["Beta Testers"]
)
notify_external_testers
boolean
Should external testers be notified?
pilot(notify_external_testers: true)
groups
array
Associate testers to groups by group name.
pilot(
  groups: ["Beta Testers", "Internal Team"]
)
beta_app_review_info
hash
Beta app review information for contact info and demo account.
pilot(
  beta_app_review_info: {
    contact_email: "[email protected]",
    contact_first_name: "John",
    contact_last_name: "Doe",
    contact_phone: "+1 555-5555",
    demo_account_name: "[email protected]",
    demo_account_password: "password"
  }
)
beta_app_description
string
Provide the ‘Beta App Description’ when uploading a new build.
pilot(beta_app_description: "This app helps you manage your tasks")
beta_app_feedback_email
string
Beta app feedback email.
pilot(beta_app_feedback_email: "[email protected]")
localized_build_info
hash
Localized ‘What to Test’ text by locale.
pilot(
  localized_build_info: {
    "en-US" => { whats_new: "Bug fixes" },
    "de-DE" => { whats_new: "Fehlerbehebungen" }
  }
)
app_version
string
The version number to distribute.
pilot(app_version: "1.2.0")
build_number
string
The build number to distribute.
pilot(build_number: "42")
expire_previous_builds
boolean
default:"false"
Expire previous builds.
pilot(expire_previous_builds: true)
wait_processing_interval
integer
default:"30"
Interval in seconds to wait for App Store Connect processing.
pilot(wait_processing_interval: 60)

Managing Testers

Add Individual Testers

fastlane pilot add [email protected] -g "Beta Testers"
Or in your Fastfile:
lane :add_tester do
  pilot(
    email: "[email protected]",
    first_name: "John",
    last_name: "Doe",
    groups: ["Beta Testers"]
  )
end

Import Testers from CSV

Create a testers.csv file:
First Name,Last Name,Email,Groups
John,Doe,[email protected],"Beta Testers"
Jane,Smith,[email protected],"Internal Team,Beta Testers"
Then import:
fastlane pilot import

Export Testers to CSV

fastlane pilot export

List All Testers

fastlane pilot list -a com.example.app

Find Specific Tester

fastlane pilot find [email protected]

Remove Tester

fastlane pilot remove [email protected]

Distribute Existing Build

Distribute a previously uploaded build without uploading a new one:
lane :distribute_beta do
  pilot(
    distribute_only: true,
    app_version: "1.2.0",
    build_number: "42",
    groups: ["Beta Testers"],
    notify_external_testers: true
  )
end

Using with fastlane

Complete Beta Lane

lane :beta do
  increment_build_number
  build_app(scheme: "MyApp")
  
  pilot(
    skip_waiting_for_build_processing: false,
    changelog: "Bug fixes and improvements",
    distribute_external: true,
    groups: ["Beta Testers"],
    notify_external_testers: true,
    beta_app_review_info: {
      contact_email: "[email protected]",
      contact_first_name: "John",
      contact_last_name: "Doe",
      contact_phone: "+1 555-5555"
    }
  )
end

Upload Only Lane

lane :upload_to_testflight do
  build_app
  pilot(
    skip_submission: true,
    skip_waiting_for_build_processing: true
  )
end

Distribute Later Lane

lane :distribute_testflight do
  pilot(
    distribute_only: true,
    build_number: ENV["BUILD_NUMBER"],
    groups: ["Beta Testers"],
    notify_external_testers: true,
    changelog: "Ready for testing!"
  )
end

App Store Connect API

pilot supports the App Store Connect API for authentication:
pilot(
  api_key_path: "./AuthKey.json"
)
Or use a hash:
pilot(
  api_key: {
    key_id: "ABC123",
    issuer_id: "XYZ789",
    key_filepath: "./AuthKey_ABC123.p8"
  }
)

Environment Variables

PILOT_USERNAME
string
Your Apple ID username
PILOT_APP_IDENTIFIER
string
The bundle identifier of your app
PILOT_IPA
string
Path to the IPA file
PILOT_CHANGELOG
string
The ‘What to Test’ text
PILOT_SKIP_SUBMISSION
boolean
Skip distributing the build
PILOT_DISTRIBUTE_EXTERNAL
boolean
Distribute to external testers
PILOT_GROUPS
string
Comma-separated list of group names

Tips & Best Practices

Use groups: Organize your testers into groups for easier management.
External testing requires review: External TestFlight builds must go through Apple’s beta review process.
Skip waiting on CI: Use skip_waiting_for_build_processing: true on CI to save time.
Provide changelog: Always include a changelog to help testers know what to test.

Troubleshooting

Build not found

Make sure the build has finished processing in App Store Connect before trying to distribute it.

Tester already exists

You can safely add existing testers - pilot will just associate them with the specified groups.

Wrong team selected

Specify the team explicitly:
pilot(
  team_id: "123456789"
)
  • gym - Build your iOS app
  • deliver - Upload to App Store
  • match - Sync certificates and profiles

Build docs developers (and LLMs) love