Automating TestFlight Builds with CircleCI – Part 1
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.
Our goal is to take our existing process and automate it with CircleCI. For this example, our current process is:
agvtool bump -allto update the build number. What’s
- 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.
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
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
Bump the build number
Let’s look at how we do build bumps first. In our
Fastfile, we added a new lane named
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:
- Here, we create
build_tagbased on the tags that we generated in the above lane. Note that this is the default output of
add_git_tagfrom that script. Most notably, while everything else can be configured, the
lanepart of the tag cannot. So if you named the above lane
bumplike we did, then the tag will have
iosbumpin the middle.
- 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.
- Here we get the credentials, make the build. Note that our scheme has to match the profile type configured in our
- Finally, upload the build to TestFlight. The
groupsvalue 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:
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.