iOS Continuous Deployment with Fastlane and Ansible – Part 1

Note : This post has been published on official Moonpig Engineering blog. Read here

At my previous role at Photobox Group, I have enjoyed working on DevOps practices for iOS apps by involving in setting up Continuous Integration servers for iOS deployments and provisioning them with Ansible. We used self-hosted CI solution using Mac Mini servers with TeamCity.  In this two-part post, we will see how we achieved Continuous Delivery of iOS apps using a combination of Fastlane and Ansible.

Continuous Delivery (CD) and  DevOps practices accelerate the delivery of new features to the end users. The fast-paced world of iOS app development may benefit from the DevOps practices to release an app quickly and easily to the production.

In the iOS world, releasing is difficult. It involves complicated steps like code signing and dealing with Apple development and distribution certificates. It can be an error-prone and time-consuming process. A manual release is done by building, testing, and archiving an iOS app from Xcode locally. The archived  .ipa file then needs to be uploaded to iTunes Connect. Ideally, we would want to automate this process using Continuous Integration (CI) which pushes every commit or merge to the main branch to Apple’s beta testing service TestFlight without any manual intervention. In this post, we’ll explain how we achieved Continuous Deployment using the build automation tool Fastlane.

Fastlane offers the most common business benefits of CD, but also offers benefits to other engineering teams including:

  • Eliminate DIY for DevOps and focus on building native iOS features.
  • New build is uploaded automatically to iTunes Connect after every merge.
  • Automates repetitive tasks of upgrade or setting up new  CI server machine.
  • Manage all the Infrastructure from source code.
  • Developer computers and CI server have the same configuration.

Challenges

Previously, releasing our app was a mission; deployment was a day long marathon. We followed traditional, time-consuming, manual releases from local machines. It was time-consuming to build, test, archive and upload an iOS app from Xcode. The most painful part of release involved downloading correct provisioning profiles associated with distribution certificates. It was even more painful to repeat the process if we found an issue in the build uploaded to iTunes Connect or in production.

In order to streamline our release process, we had to overcome the following challenges:

  • Automate the process of analysing, building, testing, archiving and uploading the iOS app to iTunes Connect
  • Set up a Continuous Integration Server to use automated builds
  • Automate the process of resetting the CI server setup whenever needed (for example, when the Xcode version changes, or when any of the Apple APIs change)
  • Drive the iOS CI infrastructure from code a.k.an Infrastructure as Code
  • Make sure the software configuration on an iOS developer machine and CI server are the same. Say No to It Works on My Machine

We solved these problems using a combination of Fastlane, TeamCity and Ansible tools. The code snippets are for the reference purpose only so Let’s start.

Build Automation with Fastlane

Apple command line developer tools like xcodebuild are a powerful way to script anything we want to automate. However, commands we were writing could be very lengthy and cumbersome. Fastlane is a wrapper around Apple command line tools to make the build automation easier. There is a collection of Fastlane tools available to automate various iOS development tasks, e.g Scan is used for running tests, Gym is used for building an app, Pilot is used to uploading an app to TestFlight.

Automating Swift Version Checking

The first step in our automated build process is to make sure that we start the CI build in a clean state using correct software versions e.g. Swift and Ruby. Fastlane provides a  before_all  step which runs before all other lanes. You can read more about advance Fastlane to configure this step. We wrote custom Fastlane actions to check the version of Swift check_swift_version and to check Swift Toolchain check_swift_toolchain. It’s pretty straightforward to write a custom Fastlane action. Our before_all  step looked like this:

This ensures that every build starts with a clean state and a correct version of tools; there are no leftovers from the previous build.

Automating XCTest

We used the  XCTest Framework by Apple to write unit and UI tests. Fastlane made it easy to configure the XCTest by wrapping all the options from xcodebuild in Fastlane’s Scan tool.  Xcode has new features like build-for-testing  and  test-without-building which mean we build once and test multiple times using the  .xctestrun file. We have a  Scanfile  where all the common configurations like scheme, workspace and build configuration are stored. Our example lane looks like this:

Automating Fabric Deployments

We use Fabric for testing debug or ad-hoc builds internally within the team. We don’t always need to test using Fabric or Crashlytics so we only make a build when needed. The build will be pushed to Crashlytics when a developer puts   #fabric  in the commit message. There is a fastlane action to push builds to Crashlytics, we just need to provide our API_TOKEN and GROUP. Our lane looks like this:

Automating TestFlight Deployments

Apple’s TestFlight is a great way of beta testing an iOS application. We use TestFlight to test release candidates to be shipped to the App Store.  The process of automating the TestFlight build involves various things:

  • Ensuring Xcode Automated Signing is disabled
  • Getting the current version and build number from the  Info.plist file
  • Incrementing the build number for the specific version
  • Uploading the build to TestFlight with the correct scheme, provisioning profiles and certificates
  • Committing back the build version bump to the main source repository
  • Creating and Push Tagging on Github
  • Creating Github Upload assets (.ipa) to Github Release
  • Sending a Slack notification that a new build is uploaded to TestFlight with release notes

It’s a lot to do!  Fortunately, Fastlane provides actions or plugins for each of these tasks. We get the current version of the app by using the get_version_number_from_plist plugin and current build number by using the Fastlane action latest_testflight_build_number. We use the increment_build_number action to increment the build number for the version and build our app with a ‘release’ configuration.  We then upload it to TestFlight using Pilot. One thing to note is that the  increment_build_number action changes the Info.plist file which we need to commit back to the source code for future build creation. In order to commit this file, we use the commit_version_bump action.  Gym can now be used to build the app. Once the build is successfully uploaded we generate a Slack message to the team.

Our sample TestFlight lane looks like this:

Automating Releases on Github

As part of our code review process, an engineer has to create a feature branch on GitHub and create a pull request against the ‘develop’ branch (main branch) once the feature is built. The pull request has to go through our SwiftLint rules and code review process. Once the pull request is merged to the main branch it will trigger our automated tests.  If all the tests pass the build will be uploaded to TestFlight with the incremented build number.

This means that every merge to the main branch goes to TestFlight, and any build can then be promoted for release. Once we select a release candidate to build, assets such as the  .ipa or .dSYM are uploaded to Github, storing artefacts from previous releases and changelogs for reference. We use the Fastlane action set_github_releases to automate this process.

With this process, we have almost achieved a fully automated CD process for iOS apps.

Next Challenge

Continuous Delivery isn’t possible without Continuous Integration.  All the above build automation scripts are automatically triggered from our TeamCity CI server.

In the next post, we will discuss what challenges we faced while maintaining the CI servers, and how we used Ansible to solve configuration management issues and speedy setup of iOS CI Server. Continue reading part 2