Continuous delivery for web applications, with multiple environments, is fast becoming a best practice in professional environments. At Kunstmaan we build and deploy our web applications using Jenkins. The entire process is scripted, configured and stored in GIT. I wanted to do the same for our iOS applications but the tools and tutorials for setting it up are not as widespread. Yet there is one project, Fastlane, now part of Twitter Fabrick that is paving the way.

For all our applications we use four different environments.

  • Debug: for running the app from Xcode
  • Alpha: these are our internal builds, running one or more times a day
  • Beta: these are builds going out to external testers, we average once a week
  • Release: the production release for the App Store

All these versions need their own app identifier, icon (with build number on it), and app name. This way we can install them all on one device, and identify which is which. We deploy to both Hockeyapp and to iTunes Connect (Testflight). We use a mix of enterprise and AppStore certificates for different environments.

Let's first start with an iOS project

For this tutorial we are going to create a simple empty Xcode project. Open Xcode and create a new project.

Give your project a clear product name. I suggest not using any spaces here, this will make your life a lot easier going further. Also put in your organisation identifier. Don't worry about the bundle identifier, we will change this later. Save this project somewhere on your computer and let Xcode create a GIT repository on your mac.

To be able to customise the icons for each build, we have to add the icons to the project. For this app I just used makeappicon.com, fast and easy. 

This is also a good time to change the bundle identifier. I like them to be lowercase, but you can do whatever you want here. You also need to add .debug to the end for the default Xcode build environment. In our case, that would be "be.kunstmaan.labs.fastlanedemo.debug".

You also need to add the Alpha and Beta configurations to your app by duplicating the existing ones.

Up next, installing Fastlane on your computer.

Install the Fastlane toolset by running the following and waiting quite a bit. Initialise Fastlane in the folder of your application.

sudo gem install fastlane
fastlane init

Enter your app identifier and Apple ID email address. Don't setup anything else, we will do this by hand later on. 

Adding some custom actions and the Fastfile

Fastlane has a lot of built in actions. You can see a list of these icons by running `fastlane actions`. To see more details of an action you can run `fastlane action [name]`. Fastlane also allows you to create your own custom actions. We will create three custom actions in the `fastlane/actions` folder.

  • app_name.rb: this action allows us to change the name of the application. We will call it later on with "Fastlane Demo", "Fastlane Demo α" and "Fastlane Demo β".
  • build_number_icon.rb: this action allows us to add a build number to each PNG file that starts with "Icon-". We use ImageMagick and Ghostscript for this. You can install them by running `brew install imagemagick ghostscript`.
  • color_icon.rb: this action allows us to change the colors of the default icon by modulating all colors with a certain degree. This action also uses ImageMagick and Ghostscript.

Next we open up the Fastfile. The Fastfile is the file that will contain the script to build and deliver our application. Remove everything until you just have the following left.

# Define the minimal Fastlane version
fastlane_version "1.41.1"

# Use the iOS platform as default
default_platform :ios

# Define what to do for the iOS platform
platform :ios do

end

In the platform :ios section we add some general maintenance tasks. Before we do anything, we are making sure that the GIT status is clean. At the end we clean up after ourselves, both on success and on failure.

# Run this before doing anything else
before_all do

  # If the GIT status is not clean, abort. We won't want to include junk in the build
  ensure_git_status_clean

end

# After all the steps have completed succesfully, run this.
after_all do |lane|

  # Remove all build artifacts, but keep mobileprovisioning profiles since they are stored in GIT
  clean_build_artifacts(
    exclude_pattern: ".*\.mobileprovision"
  )

  # Reset all changes to the git checkout
  reset_git_repo(
    force: true
  )

end

# If there was an error, run this
error do |lane, exception|

  # Remove all build artifacts, but keep mobileprovisioning profiles since they are stored in GIT
  clean_build_artifacts(
    exclude_pattern: ".*\.mobileprovision"
  )

  # Reset all changes to the git checkout
  reset_git_repo(
    force: true
  )

end

In the Fastfile we will define "lanes". Lanes are similar to functions and there are private and public lanes. Add the following private lane to the platform :ios section. This lane contains all the commands to build the app. It will color the icons and add the build number if it's an alpha or beta release. It will set the name and the app identifier to the ones for the environment we are building. It will make sure the certificate and provisioning profile are loaded. It will increase the build number and finally build the application. This lane is totally generic, all values that matter are being passed in the options array.

private_lane :build_app do |options|

  # This part is done only when the app is not for the "production" environment
  if not options[:release]
    # Modulate the color of the icons
    color_icon(
      modulation: "#{options[:modulation]}"
    )
    # Add the build number to the icon
    build_number_icon
  end

  # Update the app name
  app_name(
    plist_path: "#{options[:project_name]}/Info.plist",
    app_name: options[:app_name]
  )

  # Update the app identifier
  update_app_identifier(
    xcodeproj: "#{options[:project_name]}.xcodeproj",
    plist_path: "#{options[:project_name]}/Info.plist",
    app_identifier: options[:app_identifier]
  )

  # Install the certificate
  import_certificate(
    certificate_path: options[:certificate_path],
    certificate_password: options[:certificate_password],
    keychain_name: "login.keychain"
  )

  # Install the provisioning profile
  update_project_provisioning(
    xcodeproj: "#{options[:project_name]}.xcodeproj",
    profile: options[:profile]
  )

  # Version bump
  increment_build_number(
    build_number: options[:build_number]
  )

  # Build the app
  gym(
    scheme: "#{options[:scheme]}",
    configuration: options[:configuration],
    provisioning_profile_path: options[:profile],
    codesigning_identity: options[:codesigning_identity],
    export_method: options[:export_method]
  )

end

We also add three helper lanes to publish to different services. Publishing to Testflight, to Hockeyapp and to the AppStore. 

# Publish to Testflight
private_lane :publish_testflight do |options|

  # Generate a changelog with GIT since the last successful build in Jenkins
  changelog = sh("git log --graph --pretty=format:'%h -%d %s <%an>' --abbrev-commit #{ENV['GIT_PREVIOUS_SUCCESSFUL_COMMIT'] || 'HEAD^^^^^'}..HEAD")

  # Send the app to Testflight
  pilot(
    changelog: "#{changelog.to_s}"
  )
end

# Publish to Hockeyapp
private_lane :publish_hockey do |options|

  # Generate a changelog with GIT since the last successful build in Jenkins
  changelog = sh("git log --graph --pretty=format:'%h -%d %s <%an>' --abbrev-commit #{ENV['GIT_PREVIOUS_SUCCESSFUL_COMMIT'] || 'HEAD^^^^^'}..HEAD")

  # Send the app to Hockeyapp (fill in your API token!)
  hockey(
    api_token: "<your api token here>",
    notes: "#{changelog.to_s}",
    release_type: options[:release_type]
  )
end

# Publish to the AppStore
private_lane :publish_appstore do |options|
  deliver(force: true)
end

About certificates, app ids and provisioning profiles

When starting with iOS development, everybody has run into provisioning and certificate issues. While Fastlane can automate a lot here, I prefer to handle this myself in the developer center and store the certificates and provisioning profiles in the GIT repo of the project. I use `fastlane/certs`.

We start by creating a distribution certificate in the membercenter. Just follow the steps and import the certificate into your keychain. You will then need to export the certificate as a .p12 file. Remember the password and store this file in the `fastlane/certs` folder.

Next we create the four app ids for our environments. In this example I use:

  • Debug: be.kunstmaan.labs.fastlanedemo.debug
  • Alpha: be.kunstmaan.labs.fastlanedemo.alpha
  • Beta: be.kunstmaan.labs.fastlanedemo.beta
  • Release: be.kunstmaan.labs.fastlanedemo

And we finish by creating a distribution provisioning profile for each of these app ids. (I use distribution profiles since we test with enterprise certificates, but any certificate type will work here) Store these .mobileprovision files in the `fastlane/certs` folder as well.

Let's get to building!

At this point we have a Fastfile with the private lane for building, and all certificates and provisioning profiles collected. They are all stored into GIT. This will become useful when we move to Jenkins for building. 

For now we need to expand the Fastfile a bit more. We are adding four public lanes to build each of our environments except Debug. For the release version we have a lane to put it on Hockeyapp and the AppStore. All variables are documented and should speak for themselves. You can easily change publish_hockey to publish_testflight to use Testflight.

# Build and publish the Alpha version to Hockeyapp
lane :alpha_hockeyapp do
  # Build
  build_app(
    # Not a production release, so add build number and do the color modulation of the icons
    release:false,
    # Modulate the colors of the icons by these degrees
    modulation:66.6,
    # Change the app name
    app_name:"FastlaneDemo α",
    # Set the app id
    app_identifier:"be.kunstmaan.labs.fastlanedemo.alpha",
    # Set the path to the certificate to use in building
    certificate_path:"./fastlane/certs/<your key>.p12",
    # Set the password of the p12 certificate file
    certificate_password:"<your p12 password>",
    # Set the path to the provisioning profile to use (change this!)
    profile:"./fastlane/certs/KunstmaanLabsFastlaneDemoApp_Alpha.mobileprovision",
    # What configuration to use, usefull for keeping different API keys etc between environments
    configuration:"Alpha",
    # Use this codesigning identity (this is the name of the certificate in your keychain)
    codesigning_identity:"iPhone Distribution: Kunstmaan NV",
    # Export an enterprise app
    export_method:"enterprise",
    # the projectname, this is the name of the .xcodeproj file and the folder containing your code in the project
    project_name:"KunstmaanLabsFastlaneDemoApp",
    # the scheme to build
    scheme:"KunstmaanLabsFastlaneDemoApp",
    # the build number to use, we use the build number from Jenkins
    build_number: ENV["BUILD_NUMBER"]
  )
  # Push to Hockeyapp as Alpha release
  publish_hockey(release_type: "2")
end

# Build and publish the Beta version to Hockeyapp
lane :beta_hockeyapp do
  # Build
  build_app(
    # Not a production release, so add build number and do the color modulation of the icons
    release:false,
    # Modulate the colors of the icons by these degrees
    modulation:166.6,
    # Change the app name
    app_name:"FastlaneDemo β",
    # Set the app id
    app_identifier:"be.kunstmaan.labs.fastlanedemo.beta",
    # Set the path to the certificate to use in building
    certificate_path:"./fastlane/certs/<your key>.p12",
    # Set the password of the p12 certificate file
    certificate_password:"<your p12 password>",
    # Set the path to the provisioning profile to use (change this!)
    profile:"./fastlane/certs/KunstmaanLabsFastlaneDemoApp_Beta.mobileprovision",
    # What configuration to use, usefull for keeping different API keys etc between environments
    configuration:"Beta",
    # Use this codesigning identity (this is the name of the certificate in your keychain)
    codesigning_identity:"iPhone Distribution: Kunstmaan NV",
    # Export an enterprise app
    export_method:"enterprise",
    # the projectname, this is the name of the .xcodeproj file and the folder containing your code in the project
    project_name:"KunstmaanLabsFastlaneDemoApp",
    # the scheme to build
    scheme:"KunstmaanLabsFastlaneDemoApp",
    # the build number to use, we use the build number from Jenkins
    build_number: ENV["BUILD_NUMBER"]
  )
  # Push to Hockeyapp as Beta release
  publish_hockey(release_type: "0")
end

# Build and publish the Release version to Hockeyapp
lane :release_hockeyapp do
  # Build
  build_app(
    # Not a production release, so add build number and do the color modulation of the icons
    release:true,
    # Change the app name
    app_name:"FastlaneDemo",
    # Set the app id
    app_identifier:"be.kunstmaan.labs.fastlanedemo",
    # Set the path to the certificate to use in building
    certificate_path:"./fastlane/certs/<your key>.p12",
    # Set the password of the p12 certificate file
    certificate_password:"<your p12 password>",
    # Set the path to the provisioning profile to use (change this!)
    profile:"./fastlane/certs/KunstmaanLabsFastlaneDemoApp_Release.mobileprovision",
    # What configuration to use, usefull for keeping different API keys etc between environments
    configuration:"Release",
    # Use this codesigning identity (this is the name of the certificate in your keychain)
    codesigning_identity:"iPhone Distribution: Kunstmaan NV",
    # Export an enterprise app
    export_method:"enterprise",
    # the projectname, this is the name of the .xcodeproj file and the folder containing your code in the project
    project_name:"KunstmaanLabsFastlaneDemoApp",
    # the scheme to build
    scheme:"KunstmaanLabsFastlaneDemoApp",
    # the build number to use, we use the build number from Jenkins
    build_number: ENV["BUILD_NUMBER"]
  )
  # Push to Hockeyapp as Enterprise release
  publish_hockey(release_type: "3")
end

# Build and publish the Release version to the AppStore
lane :release_appstore do
  # Build
  build_app(
    # Not a production release, so add build number and do the color modulation of the icons
    release:true,
    # Change the app name
    app_name:"FastlaneDemo",
    # Set the app id
    app_identifier:"be.kunstmaan.labs.fastlanedemo",
    # Set the path to the certificate to use in building
    certificate_path:"./fastlane/certs/<your key>.p12",
    # Set the password of the p12 certificate file
    certificate_password:"<your p12 password>",
    # Set the path to the provisioning profile to use (change this!)
    profile:"./fastlane/certs/KunstmaanLabsFastlaneDemoApp_Release.mobileprovision",
    # What configuration to use, usefull for keeping different API keys etc between environments
    configuration:"Release",
    # Use this codesigning identity (this is the name of the certificate in your keychain)
    codesigning_identity:"iPhone Distribution: Kunstmaan NV",
    # Export an enterprise app
    export_method:"app-store",
    # the projectname, this is the name of the .xcodeproj file and the folder containing your code in the project
    project_name:"KunstmaanLabsFastlaneDemoApp",
    # the scheme to build
    scheme:"KunstmaanLabsFastlaneDemoApp",
    # the build number to use, we use the build number from Jenkins
    build_number: ENV["BUILD_NUMBER"]
  )
  # Push to the appstore
  publish_appstore
end

At this point you should be able to run `fastlane alpha_hockeyapp` on your computer. If not, carefully read the error messages and adjust!

Onwards to Jenkins

To do continuous delivery you need a dedicated server to do the building for you. Since we are building iOS apps you need a OSX machine. We use an old MacPro, but a Mac Mini or even a MacBook can work (only slower). Install a clean 10.11 El Capitan, and create an admin user and download Xcode. Make sure you start Xcode and accept the license.

We are using homebrew to install Jenkins. 

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install jenkins
mkdir -p ~/Library/LaunchAgents
ln -sfv /usr/local/opt/jenkins/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist

If you want to access Jenkins from another computer edit the plist file and change httpListenAddress to "0.0.0.0". Another lesson learned is that you need to make sure the server doesn't go into sleep mode. I use the open-source KeepingYouAwake utility. 

With Jenkins running we add some plugins:

  • Ansi color: to show the colored output of Fastlane
  • GIT: to be able to checkout from GIT
  • Green balls: success is green, not blue...

Since we are going to checkout code from GIT over ssh, you need to add an SSH key to the user running Jenkins. Also add a CI user to your GIT server (Gitlab/Github,...) with the same key. 

We also need Fastlane, ImageMagick and Ghostscript installed on the CI server.

sudo gem install fastlane
brew install imagemagick ghostscript

Now you need to create a new job in Jenkins. Start with a Freestyle project. Give it a descriptive name including the environment it will build, e.g. "kunstmaan-labs-fastlane-demo-alpha" In the config section choose GIT as source code management and add your repo url. I prefer to use specific branches for each environment. I build the alpha release from the alpha branch. So i add "alpha" as the branch specifier.

In the build triggers section we use "Poll SCM" with the following cron style timing "* * * * * ", which means check every minute. Under build environment select Color ANSI Console Output and choose xterm as terminal. Finally add a build step of type "Execute Shell" with the following content.

#!/bin/bash -lx

# Tweak some terminal settings to prevent errors
export TERM=xterm-256color
export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8

# Run the alpha_hockeyapp lane using fastlane
fastlane alpha_hockeyapp

# Cleanup a file fastlane creates that prevents the repo to be clean at the end of the run
rm -Rf fastlane/report.xml

And now it's time to see if it works, just hit "Build now" and watch it go!

Written by

Roderik van der Veer

Technology Director at Kunstmaan, passionate about innovation and the technology to do so.

Follow @r0derik