Life in the Fastlane

Phillip Connaughton
ASICS Digital
Published in
9 min readMay 4, 2016

Making builds at Runkeeper has always been a pain. In the very early days all of our builds were made by developers on their computer. That meant stopping whatever you were doing, making a build for 30 minutes, and then distributed to the team via TestFlight. We are talking old TestFlight, years before they were acquired by Apple. Eventually we grew up a little bit and moved our build system to sad closet in the back of the office known as the “Server Room.” The “Server Room” consisted of a Mac Mini, a wobbly chair, a light that flickers, and many spiders. Okay, so the room isn’t that bad, but it is a very sad small enclosed space that doesn’t see the light of day. It was basically punishment to have to fix the build system in there.

Problems were aplenty. A member of the team would get a new phone and want to try our latest build. This means adding a new device to our provisioning profile. Someone would draw the short straw and have to go manually update the profiles on that machine. Since provisioning profiles are the worst thing ever, it never worked the first time. Our builds would take 20–30 minutes, so after a failure or two you might find yourself spending the better part of the day alone in the closet. The cost of having an iOS developer working on this for a day or so every month can’t be underestimated. To have them fixing this machine for a day is time lost building new features and bettering the product.

Enter Fastlane

After Twitter acquired Fastlane last year we decided it was something we needed to start paying closer attention to. It promised green meadows, sunshine, and to remove us from our desolate closet. The promise was fulfilled. We are still new to Fastlane, but the time saved is already evident. It’s made nightly testing easier, builds spit out with a simple command, and provisioning profiles are a nightmare in the past. The following is a description of our transition that will hopefully guide you if you decided to make the move as well, which I encourage you to do. It’s worth noting that the Fastlane documentation is very good, however nothing ever works exactly how you want it to.

What is Fastlane? Fastlane is basically a collection of Ruby scripts meant to automate tasks that developers perform over and over again. Do you constantly find yourself making archives? Running unit tests? Taking and uploading screenshots? Fastlane automates all of these tasks with a few simple commands — Gym, Scan, Snapshot, and Pilot. There’s a lane for almost everything and if there isn’t a lane for what you want to do, you can write your own. It’s just Ruby. As someone with no prior Ruby experience, I was able to write a lane to parse a .ipa and upload some files to S3.

Let’s get started. Jump on over the terminal, navigate to your project directory (iPhone or Android) and enter:

gem install fastlane

Now that you have the tools installed, enter:

fastlane init

There you go, done! Fastfile created. What is this newly created Fastfile? The Fastfile is the file that Fastlane uses as its source. Each lane is defined as a Ruby function. We can start running commands by calling on the name of the lane:

fastlane nameOfLane

Pretty easy. Okay, but before we start walking through how we are using Fastlane, there is one more piece of setup that will make our lives easier. Since the Fastfile and other Fastlane files don’t have a file extension on them, it’s likely that our source editor won’t provide syntax highlighting. We’ll want to have that in place before editing the files.

Enter Sublime

Fastlane is great, but because it’s Ruby we need an editor that can interpret Ruby syntax. Xcode is not that editor. So when we work on our Fastfile we try to always use Sublime. We created a Sublime project for both our iPhone and Android repos to help with this. Within the .sublime-project file we added syntax override to Ruby for those files. You can download our profile file here, Example.sublime-project.

Cool, now all the setup is out of the way. Let’s walk through a lane of our Fastfile to give you a better understanding of how it works. We’ve attached our Fastfile for you to use as reference. The Fastlane community is strong and we were very reliant on other examples when getting up and running, so we are happy to share ours. Many more are available here.

iOS Internal

Time to open up the Fastfile and figure out what is going on. Start with the function ‘internal.’ This is our most used lane. This lane will

  • Download the latest Provisioning Profiles
  • Create an archive
  • Distribute the archive via Crashlytics Beta
  • Upload the .ipa to S3
  • Notify the team via Slack upon completion

All this done with one command. To run this lane all someone has to do is navigate to the repository on their computer and type:

fastlane internal

Looking into the lane itself, the first line is:

matchSetup(type: "adhoc")

This calls on our private lane ‘matchSetup’ which executes match on our three targets using the ‘adhoc’ profile. We have three targets because we have our main iOS app as well as a watchOS 2 app. You’ll need match command for each of your targets. Putting it into a function reduces code copying. This downloads the profiles from a private Git repository which contains all of our profiles and certs (more on this later). So now the machine is guaranteed to have the latest profiles. Gone are the days of Certs and Profiles that don’t match. The next line is:

increment_build_number(
build_number: ENV["BITRISE_BUILD_NUMBER"] + '-' + git_branch
)

This names the build number based on the number in our build system, Bitrise, and combines it with the name of the branch. Now we call on another private lane we’ve setup called ‘build’:

build(
scheme: "RunKeeper Pro (AdHoc)"
)

Build sets up our build info, configures the correct scheme, and kicks off ‘gym’ — the built-in Fastlane tool that actually creates the archive. Let’s take a step back and look at the first line of the build function, setupBuildInfo().

‘setupBuildInfo’ sets some properties in our Info.plist that can be read out at runtime. We are able to put the branch, last commit, last committer, and time into the plist. We do this using a Custom Action we wrote to add fields to a .plist.

set_info_plist_value(
path: "Info.plist",
key: "fl_commit_message",
value: last_git_commit[:message]
)

Then at runtime we are able to pull this information out of the app and display it to the user. This means that someone testing the app can easily see which version they are testing.

It also eliminates the question of, “What made it into this build?”. Right from the app you can open to the commit history in Github and see exactly what made it in. This is done by opening https://github.com/ {path_to_repo}/commit/{current_commit_hash}. This example is for iPhone but check out our Android Fastfile to see how we replicated the functionality there.

Now the build has been made, next we upload that build to Crashlytics Beta. We are able to populate the notes field so that it’s obvious to testers which version to download. We also pass in the group that we want to receive the build.

crashlytics(
ipa_path: "../RunKeeper.ipa",
notes: git_branch + ' '+ commit[:author] + ' '+ commit[:message],
groups: ['internal+'],
api_token: "ENV['CRASHLYTICS_TOKEN']",
build_secret: "ENV['CRASHLYTICS_SECRET']"
)

We send some of our builds to Applause for additional testing. For these builds our QA team needs to have direct access to the .ipa or .apk file that has been created. We provide access to those builds by uploading them to S3, but only on machines with the proper credentials. Devs don’t have these credentials on their computer so we just skip this step. Our build machine has these credentials stored as environment variables (so that they aren’t checked into Git).

if ENV['S3_ACCESS_KEY'] && ENV['S3_SECRET_ACCESS_KEY']
s3_url = upload_s3_file(file: '../Runkeeper.ipa',
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
bucket: '',
path: 'builds/mobile/iPhone/internal/' + bundle_version,
s3_file_name: 'Runkeeper.ipa')
end

Last but certainly not least, we post a message to Slack saying the whole process is complete. The Slack message has an attachment of the file in S3 so it’s easy to access. 💥

Match

The most difficult part of the setup for us has been Match. Some of these things are obvious in hindsight but weren’t obvious to us when getting set up. Overall, Match has probably been the biggest time saver and most helpful part of the entire system. Match works like this. You have one developer account that is shared by your entire dev team. That account creates the profiles and certs in iTunesConnect. Those certs are then download into a local Git repository, which is then uploaded to a remote Git repository. Now each developer on the team needs to download those profiles from the Git repo to their machine. How do they do that? For us, we have a private lane

match downloadProfiles

This downloads all of our profiles by calling ‘match’ in read only mode. It updates the profiles on your computer and sets an environment variable that points to UUID for that profile. Then in Xcode, all you have to do is use that environment variable to access the profile. So never again do you have to figure out which profile to use in the Xcode dropdown. Game changer! To explain a little more — it means when new profiles are updated, they are saved behind the same environment variable. All that is well and good but how do you keep it secure? Glad you asked! Locally, each developer on our team uses Match with their Github account, which has access to the private repo. The build machine uses SSH keys. We set an environment variable on the build machine which the Matchfile keys off of to determine which URL to use to access the git repo.

When new devices need to be added the the profiles we just run:

match updateProfiles

This forces the profiles to be downloaded directly from iTunesConnect and then committed/pushed to the Git repo.

Bitrise

The last step to moving out of the dungeon was to find a good build system. We didn’t want devs to have to wait for these lanes to run locally, so we put it in the cloud! For that we’ve decided to use Bitrise. We shopped around for a while but landed with Bitrise because they were compatible with both iOS and Android and easy to use. Additionally, their customer support was top notch, which is always important when you are getting set up with a new product. We were able to communicate with them via Slack and then over a Google Hangout with their CTO while we were looking for some advice. To make things better, they have an integration that allows us to kick off builds right from Slack. We created a build channel in Slack and are able to enter:

/ios-build workflow:internal | b:master

or

/android-build w:internal | b:master | m:fridayBuild

This kicks off a workflow in Bitrise that targets a lane in Fastlane. In this case, workflow ‘internal’ runs the internal lane we stepped through above. Fastlane notifies us via Slack when the build is complete. The Fastlane Slack integration allows us to post to a specific channel. We post to our QA room when an internal build is complete. Additionally, we run nightly test builds via Bitrise, which post the Unit Test results to the iPhone and Android channels.

Someone better fix those tests!

For more info on how to set it up, look here. It’s also worth noting that we are able to do cool things like set a custom icon when a build is made. Like this little guy

Make that Android build!

How has it worked?

Is there room for improvement with our Fastlane integration? Absolutely. However, since we are all able to test the lanes from our computer and other builds are hosted in the cloud… It means no more time in the dungeon. #improvement. It’s sped up our workflow quite a bit and made it easy for anyone from QA to Product Managers to quickly get builds in their hands.

What’s next?

We are still working on getting screenshots fully automated. We are close but not there yet. For us screenshots can be very time consuming because we have to support 5 screen sizes and 13 different languages. That means 325 screenshots. If one of the screens we use in screenshots changes, it’s a huge undertaking. We are about about 90% of the way there but the last 10% is proving to be difficult.

Example Files

iOS

Fastfile, Matchfile, Custom Actions — upload_s3_file.rb

Android

Fastfile, Custom Actions — manifest_analyzer.rb, upload_s3_file.rb

Sublime Project

Example.sublime-project

--

--

Phillip Connaughton
ASICS Digital

Director of Engineering @RunKeeper. Life long runner. Lover of football and chili.