Upcoming and OnDemand Webinars View full list

Automating TestFlight Builds with CircleCI – Part 1

Chris Downie

Keeping the entire team on the same build of your in-progress app is a pain. There are a handful of automated steps, and long gaps of time while you wait for Apple servers to propagate your data. Let’s automate this process so you can set it once and forget about it.

The Goal

Our goal is to take our existing process and automate it with CircleCI. For this example, our current process is:

  • Run agvtool bump -all to update the build number. What’s agvtool?
  • Commit and push that version bump
  • In Xcode, archive the project and submit to TestFlight
  • Wait for the build to upload and process
  • In App Store Connect, distribute the build to our test team.

The Plan

How does this translate to an automated system? We’ve got some kinks to work out.

Adding commits triggers a build loop

We want this build to run on every commit to master. Part of our process is to add a git commit to master (the commit that increases the build number by one). This is a recipe for infinitely increasing your commit count. #protip

We’ll plan on checking the latest commit to make sure it’s not a build commit before adding a new one. We’ll also add a tag to this commit, which will be useful later.

We can’t submit a given build number more than once.

Our CI process will be invoked twice — once for the change to master, and once for the build bump commit. If we submit to TestFlight on both of those builds, the second one will always fail. That’s bad. The last thing we want to do is make the team comfortable with CI task failures.

We can use some of the job filtering that CircleCI provides us to make sure we only try to submit a build once, after we’ve committed our build bump.

We need Apple signing credentials to submit to TestFlight

While fastlane handles a lot of this work for us, it means the CI system needs access to two repositories — one with the code and one with our fastlane– managed signing credentials. Since GitHub prevents you from using a single deploy key more than once, this isn’t as easy as it sounds.

We’ll plan on making two deployment keys, one for each repo, and configuring the CI system to use the right one at the right time.

Distributing a build requires build notes

Obviously, you could just hardcode “bug fixes and improvements,” but we can do better. So we will! Let’s aggregate the commit messages since our last build into a short list as the notes to submit to TestFlight.

We can re-use the build tags we need above in order to make sure we get an accurate description of what’s changed since the last build we sent out.

First Step: Perform Major Actions with Fastlane

Let’s pause our CI ambitions for a moment and focus on reducing this process with multiple applications and websites into a sequence of Terminal commands. Fastlane can handle most of the heavy lifting here.

Configuration in the Fastfile

Bump the build number

Let’s look at how we do build bumps first. In our Fastfile, we added a new lane named bump:

desc "Bump and tag version"
lane :bump do
  ensure_git_status_clean
  bump_message = "Bump build number."

  # If our latest commit isn't a build bump, then bump the build.
  build_is_already_bumped = last_git_commit[:message].include? bump_message
  next if build_is_already_bumped

  increment_build_number
  commit_version_bump(
    message: bump_message,
    xcodeproj: "Project.xcodeproj"
  )
  add_git_tag
  push_to_git_remote
end

The tricky thing here is determining if the latest commit was a build bump. We had to do include? rather than a straight equality check, since the last_message value includes a newline at the end of it for some reason. Then we tag the commit with the build number and push to remote.

Submit to TestFlight

Now let’s see how we got the thing to submit to TestFlight.

desc "Submit latest versioned build to testflight"
lane :submit_to_testflight do |options|
  # 1. Do some math to get build tags
  build_number = get_build_number(xcodeproj: PROJECT_PATH)
  last_build_number = build_number.to_i - 1
  build_tag = "builds/iosbump/" + build_number
  last_build_tag = "builds/iosbump/" + last_build_number.to_s

  # 2. Generate a change log
  comments = changelog_from_git_commits(
      between: [last_build_tag, build_tag],
      pretty: "- %s",
      date_format: "short",
      match_lightweight_tag: false,
      merge_commit_filtering: "exclude_merges"
  )

  # 3. Build the app
  match(type: "appstore", readonly: true, skip_docs: true)
  target_scheme = options[:scheme] || "MyApp-Debug-Development"
  build_app(scheme: target_scheme)

  # 4. Upload it to testflight
  groups = options[:groups] || "All Builds"
  upload_to_testflight(
    changelog: comments,
    distribute_external: true,
    groups: groups
  )
end

Let’s look at each of these four sections more closely:

  1. Here, we create last_build_tag and build_tag based on the tags that we generated in the above lane. Note that this is the default output of add_git_tag from that script. Most notably, while everything else can be configured, the lane part of the tag cannot. So if you named the above lane bump like we did, then the tag will have iosbump in the middle.
  2. This compiles the commits between those two tags into a nicely formatted list. For example:
    - Bump build number.
    - Fixes errors and warnings caused by pod update.
    - Updates MyPodDependency pod from 0.4.1 to 3.0.0.
    
  3. Here we get the credentials, make the build. Note that our scheme has to match the profile type configured in our .xcodeproj file.
  4. Finally, upload the build to TestFlight. The groups value matches a previously-added group in App Store Connect.

One more thing

This won’t have any effect yet, but when CircleCI does run these commands later, it’ll complain. Even though it can download the credentials, it has nowhere to store them. You need to add a line that should run before every lane:

```ruby
before all do |lane|
  setup_circle_ci
end
```

“But wait,” you ask, “can’t I just do that at the top of our submit_to_testflight lane, since that’s the only place we need it?” Maybe. But you might write other lanes and forget this step later. The docs say to do it before every lane. I’d do what they say.

Good to go

At this point, you can distribute builds from the terminal with two commands:

  1. fastlane bump
  2. fastlane submit_to_testflight

Hooray! That’s way better!

However, our goal is to avoid even having to type these two commands. To accomplish that, we’ll have to configure CircleCI to run these commands in Part 2.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project