Smarter ideas worth writing about.

Continuous Delivery for iOS Apps

What is it?

One of the more recent buzzwords in software engineering is Continuous Delivery. The goal is to quickly deploy working software at any time. In the mobile app world, the idea is to compile, unit test, package, sign, and install an app with a minimum effort.

I want it now

At Cardinal we follow a variant of the Agile process, known as Scrum. Because of this process, we have a need to deliver apps at the end of each sprint. In addition, we often need to deliver apps during a sprint to obtain customer input. This ability to deliver an app at any time also allows the customer to view progress during the sprint, which satisfies our goal of development transparency. 

For this post, I’m going to focus on iOS apps and describe the steps to accomplish this feat. The goals are to automatically build, unit test, sign, notify, and upload the app to Apple’s TestFlight. This entire process will be invoked with a simple press of a ‘button’.  We’ll start with a few key components. 

 

Ingredients

The primary components needed for this process are Xcode, Jenkins, Apple’s TestFlight, Ruby, and Fastlane. Information about these tools can be found at: 

  • Xcode: https://developer.apple.com/xcode/
  • Ruby: https://www.ruby-lang.org/en/ 
  • Jenkins: https://jenkins-ci.org
  • Apple’s TestFlight: https://developer.apple.com/testflight/
  • Fastlane: https://fastlane.tools

Installation

The endgame to is install everything on a build server. I recommend that you test this process on your development machine to work out all the kinks. Once that’s completed, you can install fastlane on a development server. I’m not going to discuss installing Jenkins, but we do have a previous post that describes Continuos Integration with Jenkins. This post describes installing Jenkins, if you haven’t done so already. 

I assume you have Xcode already installed. In addition to the IDE, you will need to install the command line tools. To install the tools, type “xcode-select –install” in the terminal. You then will need to enter “sudo xcodebuild -license accept” to accept the terms of service. 

A prerequisite to running fastlane is installing Ruby. I like to use homebrew to manage tools on my machine. See http://brew.sh and following the install instructions. Once complete, simply enter “brew install Ruby” in the terminal. You can verify the install by typing “ruby –v”. Now run the fastlane installation: “sudo gem install fastlane –verbose”. This will install the most recent version of the fastlane components. You are ready to create the project. 

Create the project

The next step is to create your project with Xcode as normal. Once you have the project created, invoke the following in the root of your project:“fastlane init”. Fastlane will ask for an Apple ID and password. At Cardinal, we have a ‘system’ ID that we use for the automated builds. This system id allows us to not tie a real user’s credentials to the automated builds. 

“Fastline init” will magically perform a number of tasks. It will create a new app id in the app portal, add your app to to iTunes connect, and create a number of fastfile configuration files. One of these configuration files (./fastlane/Appfile), will contain your team ID and Apple ID. Also note the password you entered for your Apple account is saved to your local keychain. When invoking commands in the future, there is no need to enter a user id and password again. This persistence of the credentials will be helpful for automated builds.   

At this time, you can add testers via the iTunes Connect portal. You could configure fastlane’s pilot task (described later) to add test users, but in this example I choose to use the portal.  

One last thing, you will need to add a CURRENT_PROJECT_VERSION key to your Xcode project. If you don’t, you will experience an error when the ‘add_git_tag’ task runs. This task is explained below. 


Configure The Builds’

The next step is to customize your fastlane config file (./fastlane/fastfile). This file contains the information needed to run your builds. The init switch will create a starter fastfile. If you view this file, you will notice a number sections called lanes. You can run each lane with the following command fastlane [lane_name]. In addition to the lanes, notice the before_all and after_all sections. These sections allow global pre and post processing logic when a lane is invoked. 

The following is an example of my fastfile. Note my project’s name is Wombat2000. Let’s take a walk through the file. 


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
# Customise this file, documentation can be found here:
# https://github.com/fastlane/fastlane/tree/master/docs
# All available actions: https://github.com/fastlane/fastlane/blob/master/docs/Actions.md
# can also be listed using the `fastlane actions` command

# Change the syntax highlighting to Ruby
# All lines starting with a # are ignored when running `fastlane`

# By default, fastlane will send which actions are used
# No personal data is shared, more information on https://github.com/fastlane/enhancer
# Uncomment the following line to opt out
# opt_out_usage

# If you want to automatically update fastlane if a new version is available:
# update_fastlane

# This is the minimum version number required.
# Update this, if you use features of a newer version
fastlane_version "1.51.0"

default_platform :ios


platform :ios do
  before_all do
    # ENV["SLACK_URL"] = "https://hooks.slack.com/services/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"
    ENV["SLACK_URL"] = "https://hooks.slack.com/services/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"
    cocoapods

  end


 desc "Runs all the tests"
  lane :test do
    scan(scheme: "Wombat2000")
  end


  desc "Submit a new Beta Build to Apple TestFlight"
  desc "This will also make sure the profile is up to date"
  lane :beta do

    begin
      build_number = latest_testflight_build_number + 1
    rescue => exception
       build_number = 1
       UI.error("no build number on the server - setting to 1")
    end

    # download and install provisioning profile
    sigh

    increment_build_number({
       build_number: build_number
    })

    gym(scheme: "Wombat2000") # Build your app - more options available
    scan(scheme: "Wombat2000") # Run unit tests

    pilot(
      skip_submission: true,
    )

    # Make sure our directory is clean, except for changes Fastlane has made
    clean_build_artifacts

    commit_version_bump

    add_git_tag

    push_to_git_remote

    # "your_script.sh"
  end

  desc "Deploy a new version to the App Store"
  lane :deploy do
    snapshot
    sigh
    gym(scheme: "Wombat2000") # Build your app - more options available
    # deliver(force: true)
    # frameit
  end

  # You can define as many lanes as you want

  after_all do |lane|
    # This block is called, only if the executed lane was successful

    slack(
      message: "Successfully deployed new App Update."
    )
  end

  error do |lane, exception|
     slack(
       message: exception.message,
       success: false
     )
  end
end



# More information about multiple platforms in fastlane: https://github.com/fastlane/fastlane/blob/master/docs/Platforms.md
# All available actions: https://github.com/fastlane/fastlane/blob/master/docs/Actions.md

before_all

On line 25 we begin the before_all section. The code adds a Slack url to an environment variable. Fastlane has a number of operations that will ‘talk’ to Slack. To configure a Slack webhook see the following URL: https://api.slack.com/incoming-webhooks. Once you have the webhook configured, you only need the url added to the Fastfile as shown in the code.

Next, I run cocoapods, to update or install all our needed third party libraries. This step could be added to a lane, but in my case I choose to run this before all lanes. 

Lane :beta

Now we get to the meat of the Delivery process. The beta lane performs all the tasks needed to  install an app on a testers device. In addition to fastlane, we also use TestFlight to complete the process. Let’s take a look at each step in our beta lane.  

Line 41 marks the beginning of the lane. The first snippet of code is on line 43. In this step we are saving the build number for later use. Note the “latest_testflight_build_number” value is automatically retrieved from Apple’s Testfligh via fastlane. We are simply incrementing it by one, thus we have a new build number for each build. 

In this step we are surrounding the setting the build_number variable line with a begin rescue end block. This block is will capture errors and execute the rescue block. This is done to handle the instance where a build number is not present in testflight. This will occur the first time the script is ran due to the fact an IPA has not been uploaded yet. 

On line 51 we see that we are creating (if needed), downloading, and installing a provisioning file with the sigh tool. This is yet another task that fastlane simplifies. Dealing with provisioning files can be painful and make one ‘sigh’ in frustration. 

The next step on line 53 is incrementing the build number. The “increment_build_number” step will update the correct entries in the app’s plists. Note we are using the build_number variable that was set earlier in this lane.

The next task is Gym. On line 57 we see that we are passing in a scheme name. This tool will build and package the app. The end result is a signed IPA file. That was easy.

On line 58 we run the Scan tool. This tool performs a build for the simulator and runs both the unit tests and the UITests. It prints out a report on the command line as well as a set of reports in html. These reports can be integrated into Jenkins. This step also sends a summary of the results to the Slack channel we configured earlier. 

The next tool on line 60 is pilot. This tool uploads a build to testflight on iTunes connect. Note we have the “skip_submision” set to true. If skip_submission was false, the step would wait for iTunes connect to finish processing the app. This could take up to 30 minutes.  We don’t wish to tie up a Jenkins executor for that long. Also there really isn’t a need to wait for the iTunes processing to complete to finish our lane. 

Line 65 clean_build_artifacts, cleans our build artifacts. The goal is to delete the files we don’t need to comment to GIT in the next few steps. 

The commit_to_version_bump on line 67 will commit the files that have modified from the version bump. This task will verify that only the following files are modified:

Wombat2000.xcodeproj/project.pbxproj
Wombat2000/Info.plist
Wombat2000Tests/Info.plist
Wombat2000UITests/Info.plist.


If other files are modified, this task will fail and dump an error listing all the modified files. This prevents modifying and committing incorrect files. 

Our next task is add_git_tag on line 69. This will add a GIT tag, with the build number, to the current branch. The tag can be customized if needed, but in my case the default value was sufficient. The default tag is “builds/iosbeta/6”, where 6 is the build number. 

The last task on line 71 is push_to_git_remote. This task pushes our tag and plists to the remote repo (origin). Now we have the information needed to duplicate this build exactly at any time. 

after_all

Note the after_all task on line 87. This block is executed if all the tasks are completed successfully. The following Slack message is result of this fastlane ‘job’. 

error

This section of code, on line 95, will execute a block of code if an exception occurs during the lane. In our case, we are sending the error to Slack and setting ‘success’ to false to indicate to fastlane the lane has failed. The following is an example of a Slack error message: 

Test the lane

Test the lane by entering “fastlane ios beta” on the command line at the root of a project. Once you have everything running we can start to look at moving fastlane to Jenkings. Note you need to have everything committed to GIT before you run fastlane. If not, you will experience the “unexpected uncommitted changes” error. Also note that you may need to wait until TestFlight has finished processing the previous submitted app before trying to upload a new version. You can also comment out various ‘tasks’ in the lane to test individual tasks or tools. 

Wire up to Jenkins

To wire this up to a Jenkins job is fairly strait forward simple. I configure the GIT plug-in for Jenkins to pull the source code to the workspace. Note you have to specify a local branch so we can commit our version number and tags.

Note you can not use a deploy key for this job. We need to push a tag and version number to GIT. In our case we used a service account to perform the push. This isn’t ideal, but we need a mechanism to bump the version number and ‘record’ it in GIT.  

Before we are ready to run fastlane as as part of a Jenkins job, we need to perform a few steps. First, install fastlane on the build server as described at the beginning of this post. Next, run the Jenkins job. Only the GIT section of the Jenkins job should be configured. This job will pull the code from GIT to the Jenkin’s workspace. Run the fastlane command from the terminal to initialize the credentials for Apple. This will give you a chance to test the fastlane script on the server and verify it’s functioning correctly.  

Once we have the fastlane ‘lane’ working correctly, we can continue to configure our Jenkins job. The next step is to configure the Execute shell plug-in to run the fastlane command. In our case we use the same command we run from the command line: “fastlane ios beta”

The last step to configure in the Jenkins job is to publish the JUnit reports that the scan tool created. I use a post build action step to publish the reports. See the image below:

Now run the Jenkins job and verify everything is working correctly. If you have issues, I found the error messages from fastlane to be fairly verbose. 

Conclusion

Now we have a single button to deliver an application with source from our development branch to a testers phone. Fastlane builds, signs, manages the profile and cert, runs the unit tests, updates the build version, tags the code it GIT and posts a result summary to Slack. TestFlight will handle the task of notifying the testers of a new build. It’s pretty slick system. Truly we can deliver an app at any time, ‘Continuously’ as they say at the software engineering parties. 

We have only scratched the surface of testflight’s capabilities. Testflight can assists with other functions such as code coverage, pushing production iOS builds to the app store, and pushing Android apps to Google play. We plan on exploring these functions in future blog posts. 



Share:

About The Author

National Mobile Soution Manager
Dan is the national Mobile Lead for Cardinal Solutions, whose primary job is to develop Cardinal’s strategy and mobile offerings.