Automating iOS app builds

Setting up a continuous delivery system for iOS applications.

I believe an important part of the development process is getting a working version into the hands of others. As part of this, being able to install the latest and greatest on the fly is paramount. It took me a lot of time to find a reliable way to build, sign, and distribute apps as part of this internal continuous delivery.

I do not recommend running your own build machines. Wasted days with Jenkins failing and Xcode crashing have taught me this is the most fragile part—and worth outsourcing. Fortunately, there are now companies dedicated to it. CircleCI has served me well: their support is fantastic and their service, despite the beta label, is very reliable. You might need to send them a quick message to request access.

Below is the outline of creating a script to generate builds. My goal isn’t to create a file you can copy-and-paste, rather to describe the methodology. If you want a full solution, I hear good things about fastlane.

Configure deployment

To instruct CircleCI to run a distribution script, add a deployment step like the following to your circle.yml file. You can find more information in the CircleCI iOS docs.

machine:
  xcode:
    version: 6.3.1 # We want nullability, etc.
deployment:
  s3:
    branch: /.*/ # All branches
    commands:
      - ./your-directory/your-script.sh | tee $CIRCLE_ARTIFACTS/distribute.log:
          timeout: 600

I like to log the build verbosely, so I save the entire transcript as an artifact. This makes it a lot easier to find compile issues. Note the timeout key is indented another level below the script command, which is a key itself. YAML syntax is strange.

Create a keychain

The most complicated part is handling your signing credentials. I’ve found that the simplest and most secure solution is a keychain checked into the repository. To store your private key and Apple-signed certificate, let’s create one:

  1. Launch Keychain Access (inside your Utilities folder).
  2. In the “File” menu, choose “New Keychain” and note the secure password you use.
  3. From your “login” keychain, locate and select both:
    • Your distribution certificate—the one from Apple.
    • Your private key, which you generated, likely a sub-item.
  4. Copy the two items to your new keychain by holding Option (⌥) and dragging them into the keychain.

Using a keychain directly allows Xcode’s tools access to your credentials. It’s also easy to update: just repeat the above steps with your existing keychain after renewing your certificate. You can include as many signing identities as you wish in the same keychain: enterprise, ad-hoc, App Store, etc.

Use the keychain to codesign builds

Create an environmental variable (here named IOS_CI_KEYCHAIN_PASSWORD) with the password you created above. Don’t store this within your repository.

Command-line operations require absolute paths to the keychain file, so we need to save that into a variable. This example assumes you’re executing a script in the same directory as the keychain:

# Make any command erroring fail the whole script
set -e

# Get the absolute keychain path - `security` requires absolute paths
pushd $(dirname $0)
TOOLS_PATH=$(pwd)
popd

KEYCHAIN_NAME="$TOOLS_PATH/distribute.keychain"

With our keychain’s absolute location, we can now:

  1. Tell the system to use our keychain, and
  2. Use the password environmental variable to unlock our keychain.

Note: If you run the following locally, you will lose access to your keychain. To fix it, instead use the absolute path to $HOME/Library/Keychains/login.keychain.

# Set our keychain as the search path. Don't run this locally.
security list-keychains -s "$KEYCHAIN_NAME"
# Unlock it using the environmental variable.
security unlock-keychain -p "$IOS_CI_KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
# Make sure we have a long timeout window.
security set-keychain-settings -t 3600 -u "$KEYCHAIN_NAME"

function cleanup {
  # Clean up on any kind of exit, just in case something bad happens.
  echo "Cleaning up keychain due to exit"
  security lock-keychain -a
}
trap cleanup EXIT

When run, your commands will have access to your certificates and keys until the script exits cleanly or otherwise.

Copy provisioning profiles

You will also need the provisioning profiles you’ve configured in Xcode. For simplicity’s sake, don’t bother figuring out which; just copy everything into your repository.

PROVISIONING_SYSTEM="$HOME/Library/MobileDevice/Provisioning Profiles/"
PROVISIONING_LOCAL="$TOOLS_PATH/provisioning-profiles/"

mkdir -p "$PROVISIONING_SYSTEM"
find "$PROVISIONING_LOCAL" -type f -exec cp '{}' "$PROVISIONING_SYSTEM" \;

You can add an rm -rf "$PROVISIONING_SYSTEM" to the cleanup function as well.

Create and upload the build

Building .ipa files is unnecessarily hard. I don’t know why. I use Nomad, which cleanly wraps the somewhat-private Xcode commands.

Nomad supports many upload destinations and services; for file storage, I like to use S3. Its library uses AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environmental variables, which you should create as a new limited-access IAM user.

# Where to store the IPA. CircleCI has an artifacts folder you can stuff things into.
# Some other providers seem to lack this, so perhaps use /tmp/.
IPA_NAME="$CIRCLE_ARTIFACTS/$CIRCLE_BRANCH.ipa"

# Disable archive because otherwise it will build twice, wasting time.
# If you aren't using Bundler (you should, though), drop the `bundle exec` part.
bundle exec ipa build --no-archive --verbose --ipa "$IPA_NAME"
bundle exec ipa distribute:s3 -b "your-bucket-name" -f "$IPA_NAME"

You may also use Nomad to upload additional files like the dSYM to your crash reporting service, metadata like the latest build number from agvtool, or certain branches directly to iTunes Connect.

Install on device

Name files by branch. This makes testing somebody’s idea or feature extremely easy. The iOS Deployment Reference gives you all you need to get started. Basically, you need an itms:// link pointing to a .plist file pointing to the .ipa file. It’s quite easy.