We as a company have been advocating Continuous Integration and Continuous Deployment. I mean, who hasn’t?

Achieving the goals of CI/CD are increasingly difficult in the following order: Web Development, Android Development, iOS/Mac Development. The increasing difficulty levels have to do with the variation of code signing and distributing to market. Oops, did I forget to mention Windows development? That’s okay, we’re going to talk about distributing iOS apps to Testflight.

Continuous Integration / Continuous Deployment

We started with small steps, implementing CI for iOS. We have been benefiting from using Github and Travis, ensuring anyone on the team:

  • Is able to clone the repo and build the app.
  • Gains a confidence boost by adding unit tests and UI tests.
  • Can deploy to Github Releases for a quick feedback loop.

CI and CD to testers have been extensively covered elsewhere, and we cut our teeth using Travis CI for iOS from objc.io’s Travis CI for iOS. This was a nice setup, and we were able to refine it for our needs, but we weren’t taking that created confidence directly to iTunes Connect until we started using Fastlane.


Fastlane has a variety of tools that can be used interactively from the command line. Some of which are:

  • Gym: a wrapper around xcodebuild, simplifying the creation of ipas.
  • Produce:  allows you to create new bundle IDs in the Developer Portal and new iOS apps on iTunes Connect.
  • Deliver: let’s you upload apps, metadata and screenshots directly to iTunes connect.
  • Pilot: allows you to upload an ipa to TestFlight and even automatically select it as the version to test.

We recommend testing the waters of Fastlane by integrating Pilot into an existing project that uses a build script to archive ipas. We’ll set that project up for Fastlane in general, then create a custom lane for distributing to TestFlight via Pilot. This is a convenient place to start because Pilot solves the CI/CD problem introduced when TestFlight was bought by Apple. Pilot uses Spaceship, a Ruby library that exposes all the operations available via the browser from Apple Developer Center and iTunes Connect. Fastlane installation instructions can be found on github.

Adding Fastlane to an existing project – Hello World

Create a file with the relative path to your project’s root directory “fastlane/Fastfile”. All of Fastlane’s tools are available to use in the fastfile. They are just a tad more convenient to use in the fastfile because they use Ruby rather than Bash.

fastlane_version "1.53.0"
default_platform :ios

platform :ios do

  desc "Hello world" #description used in console output
  lane :hello_world do #start of lane. "hello_world" will be the name
    puts "Hello World!" 


This just created a single lane. You can use it from the root directory of your project.

$ fastlane hello_world

You will see a lot of fastlane output, but notice your “Hello World!” will be there.

[02:28:52]: -------------------------------------------------
[02:28:52]: --- Step: Verifying required fastlane version ---
[02:28:52]: -------------------------------------------------
[02:28:52]: fastlane version valid
[02:28:52]: ------------------------------
[02:28:52]: --- Step: default_platform ---
[02:28:52]: ------------------------------
[02:28:52]: Driving the lane 'ios hello_world' ��
[02:28:52]: Hello World!

|                     fastlane summary                     |
| Step | Action                              | Time (in s) |
| 1    | Verifying required fastlane version | 0           |
| 2    | default_platform                    | 0           |

[02:28:52]: fastlane.tools finished successfully ��

Fastlane actions

Before writing our lane to upload to testflight, let’s checkout the documentation on Pilot

$ fastlane actions pilot

A lot of text flies out of the console, but there is a very helpful grid

|                                                                  pilot                                                                  |
| Key                        | Description                                                               | Env Var                        |
| * username                 | Your Apple ID Username                                                    | PILOT_USERNAME                 |
| app_identifier             | The bundle identifier of the app to upload or manage testers (optional)   | PILOT_APP_IDENTIFIER           |
| ipa                        | Path to the ipa file to upload                                            | PILOT_IPA                      |
| changelog                  | Provide the what's new text when uploading a new build                    | PILOT_CHANGELOG                |
| * skip_submission          | Skip the distributing action of pilot and only upload the ipa file        | PILOT_SKIP_SUBMISSION          |
| apple_id                   | The unique App ID provided by iTunes Connect                              | PILOT_APPLE_ID                 |
| * distribute_external      | Should the build be distributed to external testers?                      | PILOT_DISTRIBUTE_EXTERNAL      |
| first_name                 | The tester's first name                                                   | PILOT_TESTER_FIRST_NAME        |
| last_name                  | The tester's last name                                                    | PILOT_TESTER_LAST_NAME         |
| email                      | The tester's email                                                        | PILOT_TESTER_EMAIL             |
| testers_file_path          | Path to a CSV file of testers (default: './testers.csv')                  | PILOT_TESTERS_FILE             |
| * wait_processing_interval | Interval in seconds to wait for iTunes Connect processing (default: '30') | PILOT_WAIT_PROCESSING_INTERVAL |
| team_id                    | The ID of your team if you're in multiple teams                           | PILOT_TEAM_ID                  |
| team_name                  | The name of your team if you're in multiple teams                         | PILOT_TEAM_NAME                |
4 of the available parameters are required
They are marked with an asterisk *

More information can be found on GitHub. These keys are the same whether using Fastlane in the command line or in ruby scripts.

Using Pilot in a custom lane

Now to add a lane for Pilot, directly below the hello_world lane

desc "Distribute via pilot" 
lane :distribute do   
    username: ENV["ITUNES_CONNECT_USER"],
    wait_processing_interval: 30,
    distribute_external: false,
    skip_submission: true, #allows build script to end before processing the binary        
    ipa: "example.ipa" #path of the ipa generated by your build script

Run the newly created lane, feeding in an ITUNES_CONNECT_USER environment value:

$ ITUNES_CONNECT_USER=appleid fastlane distribute

You’ll notice that you are prompted to provide your password for iTunes Connect. We can fall back on a Fastlane tool fastlane-credentials to store the credentials where Pilot can find them… preventing the build script from being interactive.

#!/usr/bin/env bash

#code that generates an ipa

fastlane-credentials add --username $ITUNES_CONNECT_USER --password $ITUNES_CONNECT_PASSWORD
fastlane distribute
fastlane-credentials remove --username $ITUNES_CONNECT_USER

We set up the distribute lane to upload the ipa and exit with success prior to iTunes Connect processing of the build. This prevents the build script from timing out and allows other team members to push the build out to users with a click of a button.

Pro tips:

  • Encrypt your Apple credentials. Most cloud base CI services have solutions for encrypting sensitive information.
  • Continue to deploy ipas to cloud storage, such as Github Releases or S3. Pilot is subject to break because it is built on scraping Apple’s websites. If you are running your build script in the cloud, you still want easy access to the binary.
  • Supply the path to the ipa when calling pilot. Pilot will automatically find an ipa to upload, but that doesn’t scale well adding multiple build flavors to the script.
  • Skip submission. Having to click a button at the end is a good compromise to not tying up the build server while iTunes Connect is processing. Note you will only have to manually select the new build as the test version if the version number changed. iTunes Connect automatically releases new builds added to the existing test version.
Jesse Black

Software Architect at Stable Kernel

Leave a Reply

Your email address will not be published. Required fields are marked *