Continuous Integration (CI) and Continuous Delivery (CD) are both relatively fresh practices in our, iOS developers’ that is, toolkits. This is mostly because we didn’t really have good tools to facilitate their proper deployment until recently. Gone are the days of breaking the build or tests without even noticing. Forget about archiving and sending builds manually to your testers or clients. Build servers are here to save us from the mundane toil.
The process of setting up our build server will consist of two steps:
- Continuous Integration: First, we’ll set up Travis CI to run our test suite on each pull request. This will allow us to be sure that neither a build nor tests will break after merging with the main branch (
developin our case). One cool thing here: Travis CI doesn’t actually use the branch a pull request is based on. Instead, it merges it with
developfirst and then uses this temporary branch.
- Continuous Delivery: Then, we’ll set up Travis CI to build, archive, and push a build to HockeyApp after each commit is pushed to (or merged to)
develop. This will allow all interested parties to always be able to get the latest build of the app. We’ll also show how to set up the same capability on our local machine.
The knowledge collected and presented in this article is derived from other sources available online. Given some of the most common reactions 👏 in our team after one of us manages to set up CI & CD, I came to the conclusion that there’s real value in getting every little detail about their deployment covered in a single piece.
We’re going to use GitHub, fastlane, Travis CI, and HockeyApp, as that’s what we use in most cases here at Macoscope. A sample project with the history (mostly) matching the one described in this article is available on GitHub: ContinuousIntegrationExample.
As mentioned above, we’re going to use fastlane, which is one level of abstraction above build tools and web interfaces of Apple (and other) services. It was created by Felix Krause. Let’s start by following the fastlane guide and installing Xcode’s command line tools:
and accepting the Terms of Service:
sudo xcodebuild -license accept
Now, we can install fastlane itself through RubyGems:
sudo gem install fastlane
This would be a good time to move in the terminal to the directory our project is located in. We could use fastlane’s configuration assistant (
fastlane init) to start the setup, but since we want to thoroughly understand all parts of the configuration, let’s do everything by hand. To make our app testable, we only have to create a
fastlane/Fastfile file with the following contents:
fastlane_version "1.47.0" default_platform :ios platform :ios do before_all do ensure_git_status_clean end desc "Run all the tests" lane :test do scan(device: "iPhone 6s (9.2)") end end
fastlane uses the concept of lanes, somewhat similar to Unix pipelines. Here, we have a lane that runs the tests, called, wouldn’t you know it,
test. It’s not the best example of a pipeline, however, as there’s only one operation here. Stay tuned, though, as we’ll see a more powerful example later on. Aside from the
test lane, we have a
before_all block that executes before each lane and makes sure that our working directory is clean.
We can already use fastlane to perform tests on our local machine by executing:
from the command line. The app should be built, tests should be completed, and we should get a long output ending with:
fastlane.tools finished successfully 🎉
With everything working locally, we’re ready to move on to setting up Travis CI to run our tests on pull requests.
Travis CI is a distributed continuous integration service intended for building and testing projects hosted at GitHub. It comes in two versions: a) a free one at travis-ci.org for open source projects, and b) a paid version for closed source projects that can be found at travis-ci.com.
We start by logging in to our Travis CI account, selecting our organization from the menu on the left, and checking the checkbox next to our repository’s name:
Travis CI has to be informed somehow that it should run tests for all pull requests. Let’s create
fastlane/travis.sh file with the following content:
#!/bin/sh if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then fastlane test exit $? fi
The environment variable
$TRAVIS_PULL_REQUEST will contain the pull request’s number if a build is performed for the pull request, and
"false" otherwise. If the condition is true, we run our tests and exit with a status returned from fastlane. This file has to be executable, so let’s change its permissions with:
chmod a+x fastlane/travis.sh
We also have to let Travis CI know that it should execute the above script. We can do that by creating a
.travis.yml file in the root directory of our repository:
language: objective-c osx_image: xcode7.2 script: - ./fastlane/travis.sh
There’s one more thing left to do: locally, Xcode automatically creates a scheme for us. This doesn’t happen on a build server. So, we have to open our project, click on the target name, select
Manage Schemes... and check the
Shared checkbox next to the scheme name, as shown on the image below:
That’s it, we’re done with setting up automatic tests on Travis CI. When we create a new pull request, we should see Travis CI start running our tests:
After they’ve finished successfully this peacefully green output should appear:
Now comes the harder, but also the more interesting part.
I think it’s good practice to have a separate bundle identifier for each configuration. There are at least two advantages to this approach:
- it’s possible to have both development and App Store versions of the app installed simultaneously,
- it’s easier to know which environment crash reports are coming from (it’s hard to admit, but I haven’t seen an app that didn’t crash at least once).
In our case, we’ll have:
- Ad Hoc:
net.macoscope.ci-example.ad-hoc(used for builds distributed through HockeyApp)
- App Store:
To reach this state, we have to:
- select the project file in Project Navigation in Xcode,
- choose our project (“CIExample”) under the “PROJECT” header,
- under “Configurations”, rename “Release” to “App Store”,
- duplicate “App Store” and rename it to “Ad Hoc”,
- now choose the main target under the “TARGETS” header,
- go to “Build Settings”,
- search for “product bundle identifier”,
- set correct product bundle identifier per configuration, as listed above,
- and finally, under “Code Signing”, change “Ad Hoc” and “App Store” values to “iOS Distribution”.
To distribute an app either through Ad Hoc or the App Store, it needs to be signed. fastlane comes with the cert tool, but I’m not a big fan of it, as it constantly revokes and recreates certificates. There’s also match, introduced fairly recently. To keep the article concise we’ll use a simpler approach: we’ll keep our certificate and the private key pair in the same repository as we do our app.
We’ll assume that we already have a distribution certificate imported into our Keychain. (A new one can be created by tapping the “+” button in Certificates, Identifiers & Profiles in the Member Center.)
To be able to access our certificate from Travis, we have to export it first. We can do that straight from Keychain.app. Let’s choose our certificate and the private key pair:
and export it in a
.p12 format (it contains both a certificate and a private key) to
fastlane/Certificates/distribution.p12. Travis CI will have to know about the password we chose during the export. We wouldn’t want it to be stored in plaintext anywhere. Thankfully, Travis CI provides safe storage for confidential information, called encryption keys. To add the encrypted password to the
.travis.yml file we have to execute the following:
travis encrypt "KEY_PASSWORD=foo" --add
Since our repository is public, it’s best to encrypt the
.p12 file too, with the below command:
openssl aes-256-cbc -k "bar" -in fastlane/Certificates/distribution.p12 -out fastlane/Certificates/distribution.p12.enc -a
and then add the password used here to
travis encrypt "ENCRYPTION_PASSWORD=bar" --add
Oh, and please use some stronger passwords than
bar 🙃. Finally, we have to decrypt that file on each build. We can do that by adding a command to
.travis.yml that executes before the script:
before_script: - openssl aes-256-cbc -k $ENCRYPTION_PASSWORD -in fastlane/Certificates/distribution.p12.enc -d -a -out fastlane/Certificates/distribution.p12
We’ll use HockeyApp to distribute our builds to testers and clients. fastlane supports other ways of distribution, for example TestFlight, but HockeyApp is our tool of choice, so we’ll be focusing on it exclusively.
Create a new app with the following information once you’re logged in to your HockeyApp account:
- Platform: iOS
- Release Type: beta
- Title: CI Example Ad Hoc
- Bundle Identifier: net.macoscope.ci-example.ad-hoc
Then go to API Tokens and create a new token with “Full Access” permissions.
Now, let’s encrypt that token along with HockeyApp’s App ID using:
travis encrypt "HOCKEY_API_TOKEN=api_token" --add travis encrypt "HOCKEY_APP_ID=app_id" --add
As you may have noticed, we created a new distribution certificate but didn’t create any provisioning profiles yet. That’s because fastlane will do it for us 🎉. We have to provide it with proper credentials to Apple’s Member Center, so let’s add our Apple ID password to Travis’s encryption keys:
travis encrypt "FASTLANE_PASSWORD=apple_password" --add
Before we can push our builds to HockeyApp, we have to create
fastlane/Appfile with some meta information:
app_identifier "net.macoscope.ci-example.app-store" apple_id "firstname.lastname@example.org" for_platform :ios do for_lane :test do app_identifier "net.macoscope.ci-example.development" end for_lane :beta do app_identifier "net.macoscope.ci-example.ad-hoc" end end
Nothing really interesting here. We specify an app identifier per lane, since fastlane isn’t currently able to get this information from the project file.
We’re now ready to add a
beta lane that will be responsible for building and pushing our app to HockeyApp. Here are its contents:
desc "Submit a new Beta build to Hockey App" lane :beta do keychain_name = "ci-example-certs" create_keychain( name: keychain_name, default_keychain: true, unlock: true, timeout: 3600, lock_when_sleeps: true, password: SecureRandom.base64 ) # Import distribution certificate import_certificate( certificate_path: "fastlane/Certificates/distribution.p12", certificate_password: ENV["KEY_PASSWORD"], keychain_name: keychain_name ) # Fetch provisioning profile sigh( adhoc: true, username: "email@example.com", team_id: "XA8U8K5RRK", provisioning_name: "CI Example Ad Hoc", cert_id: "2T3HB2838A" ) increment_build_number(build_number: number_of_commits) # Build gym( configuration: "Ad Hoc", sdk: "iphoneos9.2", clean: true, include_bitcode: false, include_symbols: true, use_legacy_build_api: true, export_method: "enterprise" ) # Push to Hockey hockey( api_token: ENV["HOCKEY_API_TOKEN"], public_identifier: ENV["HOCKEY_APP_ID"], notify: '0', status: '2', notes: last_git_commit[:message] + "n(Uploaded automatically via fastlane)" ) delete_keychain( name: keychain_name ) end
Let’s go through each step of the process:
- we create a new keychain with
create_keychain, set it as the default, and then we import our distribution certificate into it,
- we use sigh to create (or download, if it already exists) a provisioning profile,
- we set the build number to the number of commits in git. This will make sure that the builds in HockeyApp have the correct order,
- we build the app using gym,
- we push the app to HockeyApp with hockey,
- we delete the previously created keychain.
We won’t be going through each parameter of these tools, but I encourage you to read the documentation from the command line, e.g. the documentation for sigh is available via
sigh --help and
fastlane action sigh.
The one last thing we have to do is to instrument Travis CI to execute the
beta lane on each merge with
develop. We can do that using:
if [[ "$TRAVIS_BRANCH" == "develop" ]]; then # Travis CI fetches a shallow clone. We use commit count until HEAD for build number. In order to assure that the count is correct we have to unshallow the clone. git fetch --unshallow fastlane beta exit $? fi
It turns out, though, that there is an issue with SSL connections in the Ruby environment provided by Travis CI in the
xcode7.2 OS X image. We get this error:
[15:57:26]: Starting login with user 'firstname.lastname@example.org' [15:57:27]: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
The only fix for this issue that I found is to reinstall Ruby on each build. The updated version of that part of the
travis.sh script looks like this:
if [[ "$TRAVIS_BRANCH" == "develop" ]]; then # Travis CI fetches a shallow clone. We use commit count until HEAD for build number. In order to assure that the count is correct we have to unshallow the clone. git fetch --unshallow # We need this because otherwise there are issues with SSL in Ruby in xcode7.2 OS X image rvm reinstall 2.0.0-p643 --disable-binary fastlane beta exit $? fi
We make SSL connections only from our
beta lane, so we don’t have to do this reinstallation for the
beta lane locally is a little harder than it was with the
test lane. That is because a portion of the information is stored in the
.travis.yml file which we aren’t able to execute on our local machine. Don’t worry, though, as we can simulate the Travis CI environment with one simple script. Create a
fastlane/local_config.sh file with the following contents:
#!/bin/sh export FASTLANE_PASSWORD=apple_password export KEY_PASSWORD=foo export ENCRYPTION_PASSWORD=bar export HOCKEY_API_TOKEN=token export HOCKEY_APP_ID=token export TRAVIS_BRANCH=develop export TRAVIS_PULL_REQUEST=false openssl aes-256-cbc -k $ENCRYPTION_PASSWORD -in fastlane/Certificates/distribution.p12.enc -d -a -out fastlane/Certificates/distribution.p12
Add it to
.gitignore (as we don’t want any confidential information to be stored in the repository) and make it executable with:
chmod a+x fastlane/local_config.sh
should lead to the same output as produced by Travis CI. If you encounter any issues, make sure that you have matching versions of tools (ruby, fastlane, etc.) installed locally.
This was the last step of our setup. I encourage you to check out the repository on GitHub here along with the history of changes.
Conclusions and Future Considerations
We demonstrated how to set up continuous integration and delivery in a real-world setting. We made our code reviews safer by making Travis CI run our test suite automatically on each pull request. And finally, we automated pushing builds to testers and clients, thus saving developers from having to do it manually.
From our experience, this setup works fine for teams up to about 10 developers. Larger teams working on bigger projects may need to tweak some things here and there. If you’re interested in improving other parts of your development process, here are some ideas for the future:
- research match and decide whether it suits your team (UPDATE: we already did and decided to transition to match),
- add a lane for pushing new versions of your app to the App Store along with automated screenshot taking,
- add a lane for pushing your app to testers using TestFlight (try leveraging pilot for this),
- add Slack notifications after each build.