Building Quality iOS Apps, Quickly
iOS MacAt Big Nerd Ranch, we take pride in building quality apps for our clients. Unfortunately, quality isn't the only concern when building an app...
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 -all
to update the build number. What’s agvtool
?
How does this translate to an automated system? We’ve got some kinks to work out.
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.
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.
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.
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.
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.
Fastfile
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.
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:
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.- Bump build number.
- Fixes errors and warnings caused by pod update.
- Updates MyPodDependency pod from 0.4.1 to 3.0.0.
.xcodeproj
file.groups
value matches a previously-added group in App Store Connect.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.
At this point, you can distribute builds from the terminal with two commands:
fastlane bump
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.
At Big Nerd Ranch, we take pride in building quality apps for our clients. Unfortunately, quality isn't the only concern when building an app...
How can you detect when the user connects their phone to their car’s stereo? This simple question that came up during a feature brainstorm...
Automate your build and deploy process with fastlane and CircleCI! We'll build on our fastlane commands from Part 1 and show how to configure...