Continuous Integration for iOS Projects

Continuous Integration, or CI, is a practice which gives us more confidence in our work. By automatically building our project as we make changes, we can run our unit and integration tests against work in progress and check its suitability for deployment.

It's easy to take it for granted, but there's a lot of value in knowing that your tests still pass, and or knowing exactly what was built for a release. Detecting problems early makes them easier to fix, whether you forgot to commit a file, or hadn't anticipated some changes upstream. Automation reduces the potential for human error, and frees up time for other tasks.

On YPlan's mobile team, we host our code on GitHub, and use Travis CI to build pull requests, pushed branches and tags.

This post will describe how we currently use CI for our iOS projects, and how you can set up a similar pipeline of your own.

Philosophy

Although we currently use Travis CI, we might want to change provider or add other CI services in future. In general, the scripts we write to build our project are flexible and service agnostic. Conveniently, these same scripts can be used by developers to bootstrap the project, build and run tests, or create a release build.

Getting started with Travis is described here and is straightforward – you grant access to your GitHub repositories, add a .travis.yml configuration file to your repository, commit and push. This post assumes basic familiarity with Travis CI, but provides scripts which will work on any CI provider.

Bootstrapping

Builds on Travis run in isolated VMs, so no state is persisted between builds. This means we need a way to initialise our project, fetch dependencies, and perform any other tasks that might be required to provide a working environment to build our project. For this we add a bootstrap script – a single command.

We use Carthage to manage dependencies, so our bootstrap script is simple:

#!/bin/bash

carthage bootstrap --platform iOS

Alternatively, if you use CocoaPods, your bootstrap script would run pod install.

We add the bootstrap script to our project repository in the script directory and make it executable:

chmod +x script/bootstrap

Building the project

As with bootstrapping, we have a single script that builds the schemes in our project. We use Justin Spahr-Summers' cibuild script as a basis for ours. This generic script finds a project or workspace, checks for required tools (xctool), and finally invokes xctool to build and test.

If you'd rather write your own cibuild script from scratch, at a minimum, you would want to invoke xcodebuild and run your tests.

#!/bin/bash
xcodebuild test -workspace MyApp.xcworkspace -scheme 'MyApp' -destination 'platform=iOS Simulator,name=iPhone 6,OS=9.3'

If the cibuild script exits with a zero status code, the Travis build completes with success. At this point GitHub will reflect the successful integration on any relevant pull request, indicating to reviewers that the code builds and passes any automated tests.

Our .travis.yml looks like this:

language: objective-c
osx_image: xcode7.3
script: script/cibuild

Deployment (or building a Release IPA)

If the build step completes successfully, Travis can run a deployment step. Various providers are supported for deployment, included GitHub Releases, known within Travis as releases. This uploads our built IPA directly to GitHub and attaches it to a tagged Release.

We can use the before_deploy step to build, archive and export a signed IPA for Release, and the deploy step to upload the IPA to GitHub. From GitHub, we'll download it and upload it to iTunes Connect. This is what we'll do at YPlan, and I'll explain how to set it up below.

Importing provisioning and signing assets to Travis

First of all, we need to provide Travis with our provisioning profile and signing identity. Save a copy of your app's iOS Distribution provisioning profile to script/profile/distribution.mobileprovision. From Keychain Access, select your iPhone Distribution certificate and private key, right click, and export 2 items. Enter a secure password and make a note of it – you'll need to supply it to Travis to import the signing identity. Save the resulting .p12 file as script/certificates/distribution.p12. Commit both files with Git.

Use the Travis command line client to add the password as a secure environment variable:

travis encrypt "DIST_KEY_PASSWORD={password}" --add

This encrypts the password and adds it to the .travis.yml file so it can only be read by Travis. When Travis starts the build VM, it will make the password available as an environment variable.

Building the release

Now we add the before_deploy step to the Travis configuration to actually build our IPA:

before_deploy: ./script/distribution-deploy

Next we add the distribution-deploy script to our script directory and make it executable:

#!/bin/bash

KEYCHAIN=ios-build.keychain

local password=cibuild

# Create a temporary keychain for code signing.
security create-keychain -p "$password" "$KEYCHAIN"
security default-keychain -s "$KEYCHAIN"
security unlock-keychain -p "$password" "$KEYCHAIN"
security set-keychain-settings -t 3600 -l "$KEYCHAIN"

# Download the certificate for the Apple Worldwide Developer Relations
# Certificate Authority.
local certpath="$SCRIPT_DIR/apple_wwdr.cer"
curl 'https://developer.apple.com/certificationauthority/AppleWWDRCA.cer' > "$certpath"
security import "$certpath" -k "$KEYCHAIN" -T /usr/bin/codesign

# Import our distribution certificate, using the secure environment variable
if [ -n "$DIST_KEY_PASSWORD" ]
then
  security import "$SCRIPT_DIR/certificates/distribution.p12" -k "$KEYCHAIN" -P "$DIST_KEY_PASSWORD" -T /usr/bin/codesign
fi

# Import our distribution provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "./script/profile/distribution.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/

# Archive MyApp to MyApp.xcarchive
xcodebuild archive -workspace MyApp.xcworkspace -scheme "MyApp" -sdk iphoneos -configuration 'Release' -archivePath MyApp.xcarchive | xcpretty

# Export an IPA for iOS App Store Distribution
xcodebuild -exportArchive -exportOptionsPlist ./script/exportOptions.plist -archivePath MyApp.xcarchive -exportPath ./

# Zip the archive for upload to GitHub releases
zip -r MyApp.xcarchive.zip MyApp.xcarchive

Note that the xcodebuild -exportArchive command requires an exportOptionsPlist argument referencing a plist.

Add the following to your repository as script/exportOptions.plist, replacing the teamID with your signing team ID and commit:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>teamID</key>
        <string>your-team-id</string>
        <key>method</key>
        <string>app-store</string>
        <key>uploadSymbols</key>
        <true/>
        <key>compileBitcode</key>
        <false/>
        <key>uploadBitcode</key>
        <false/>
</dict>
</plist>

You can set uploadSymbols, compileBitcode and uploadBitcode as appropriate for your project.

Deploying the Release to GitHub

Finally, we need to tell Travis to upload the artifacts

I recommend using the Travis command line client to set up deployment to GitHub by running the following in your project root: travis setup releases

This will prompt you for GitHub credentials, request an api_key so that artifacts can be uploaded from Travis to the GitHub Release, and confirm the filename to be uploaded. The Travis CLI can also encrypt the api_key, which you'll probably want to do. The resulting configuration will look something like this:

deploy:
- provider: releases
  api_key:
    secure: <your-GitHub-api-key>
  file:
  - MyApp.ipa
  - MyApp.xcarchive.zip
  on:
    repo: YPlan/MyApp
    tags: true

We added an additional file entry above, as we'd like to also attach the .xcarchive to our GitHub Release.

Now we can simply tag a release on GitHub and Travis will build, test and deploy an archive to GitHub release. This one-step process is simple and greatly reduces the risk of error.

Our final Travis configuration

Here is the the final .travis.yml in full.

language: objective-c
osx_image: xcode7.3
script: script/cibuild MyApp
before_install:
- curl -L -O https://github.com/Carthage/Carthage/releases/download/0.15.2/Carthage.pkg
- sudo installer -pkg Carthage.pkg -target /
before_deploy:
- if [[ -n $TRAVIS_TAG ]]; then ./script/distribution-deploy; fi
deploy:
- provider: releases
  api_key:
    secure: <your-GitHub-api-key>
  file:
  - MyApp.ipa
  - MyApp.xcarchive.zip
  skip_cleanup: true
  on:
    repo: YPlan/MyApp
    tags: true

Wrapping up

At YPlan, we're using CI for the above and more. It's easy to add Slack Notifications, or set up a beta branch which automatically deploys to Fabric or TestFlight. Hopefully this whets your appetite for automation and inspires you to flesh out your CI pipeline and automate where possible.

Further Reading