Continuous Integration

Best Practices

Two-step or Two-factor auth

If you want to upload builds to TestFlight/iTunes Connect from your CI machine, you need to generate an application specific password:

  1. Visit appleid.apple.com/account/manage
  2. Generate a new application specific password
  3. Provide the application specific password using the environment variable FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD

Because your CI machine will not be able to prompt you for your two-step or two-factor auth information, you also need to generate a login session for your CI machine in advance. You can get this by running:

fastlane spaceauth -u [email protected]

This will generate a token you can set using the FASTLANE_SESSION environment variable on your CI system.

Deploy Strategy

You should not deploy a new App Store update after every commit, since you still have to wait 1-2 days for the review. Instead it is recommended that you use Git Tags, or custom triggers to deploy a new update.

You can set up your own Release job, which is only triggered manually.

Jenkins Integration

Deploying from your own computer isn't cool. You know what's cool? Letting a remote server publish app updates for you.

fastlane automatically generates a JUnit report for you. This allows Continuous Integration systems, like Jenkins, access the results of your deployment.

Installation

The recommended way to install Jenkins is through homebrew:

brew update && brew install jenkins

From now on start Jenkins by running:

jenkins

To store the password in the Keychain of your remote machine, it is recommended that you run match or deliver using ssh or remote desktop at least once.

Plugins

You'll find the following Jenkins plugins to be useful:

Build Step

Use the following as your build step:

fastlane appstore

Replace appstore with the lane you want to use.

setup_jenkins

You can use setup_jenkins action which integrates well with the Keychains and Provisioning Profiles Plugin. Selected keychain will automatically unlocked and the selected code signing identity will be used. Also all build results, like IPA files, archives, dSYMs and result bundles will be stored in the ./output folder in the job workspace. In additions setup_jenkins will create separate derived data folder for each job (in the ./derivedData).

Under the hood setup_jenkins configures other actions like: gym, scan, xcodebuild, backup_xcarchive and clear_derived_data.

commit_version_bump || git_commit

You can use commit_version_bump or git_commit action to commit changes to your repository in your fastlane setup. When you are using webhooks to trigger your build on a push this will cause an infinite loop of triggering builds.

Gitlab

When you are using Gitlab you will need the GitLab Plugin. Inside the job you want to configure you go to Build Triggers > Build when a change is pushed to GitLab > Enable [ci-skip]. When you include [ci-skip] in your build this commit won't trigger the build in jenkins at all.

Example

build_number = increment_build_number
commit_version_bump(message:"[ci-skip] Version Bump to #{build_number}")
git_commit(path:"./CHANGELOG.md", message:"[ci-skip] Updated CHANGELOG for Build #{build_number}")
push_to_git_remote

Test Results and Screenshots

To show the deployment result right in Jenkins

  • Add post-build action
  • Publish JUnit test result report
  • Test report XMLs: fastlane/report.xml

To show the generated screenshots right in Jenkins

  • Add post-build action
  • Publish HTML reports
  • HTML directory to archive: fastlane/screenshots
  • Index page: screenshots.html

Save and run. The result should look like this:

/img/best-practices/JenkinsIntegration.png

Circle Integration

To run fastlane on Circle as your CI, first create a Gemfile in the root of your project with the following content

source "https://rubygems.org"

gem "fastlane"

and run

gem install bundler && bundle update

This will create a Gemfile.lock, that defines all Ruby dependencies. Make sure to commit both files to version control.

Next, use the following circle.yml file

machine:
  xcode:
    version: "7.3"
dependencies:
  override:
    - bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3 --without development
  cache_directories:
    - vendor/bundle
test:
  override:
    - bundle exec fastlane test

This will automatically cache the installed gems on Circle, making your CI builds much faster!

Bamboo Integration

Repository setup

In bamboo under Linked Repositories (where you configure your git repo) under Advanced Settings is an option called Exclude changesets

This dialog will allow you to enter a regular expression that if a commit matches, a build will not be triggered.

For example, if your Fastfile is configured to make a commit message in the style of

Build Version bump by fastlane to Version [0.3] Build [8]

Then you could use the following regex to ignore these commits

^.*Build Version bump by fastlane.*$

Setting repository remote

By default bamboo will do an anonymous shallow clone of the repo. This will not preserve the git remote information nor the list of tags. If you are using bamboo to create commits you may want to use a code block similar to the following:

# In prep for eventually committing a version/build bump - set the git params
sh('git config user.name "<COMMITTER USERNAME>"')
sh('git config user.email <COMITTER EMAIL>')

# Bamboo does an anonymous checkout so in order to update the build versions must set the git repo URL
git_remote_cmd = 'git remote set-url origin ' + ENV['bamboo_repository_git_repositoryUrl']
sh(git_remote_cmd)

Speeding up build times with carthage

Carthage is a wonderful dependency manager but once you are start using a large number of frameworks, things can start to slow down, especially if your CI server has to run carthage EVERY time you check in a small line of code.

One way to make build times faster is to break your work up into two separate build plans (this can get even more funky if you start having multiple branches)

The general idea is to make a build plan: Project - Artifacts that builds the Carthage directory and stores it as a shared artifact. Then you create a second build plan Project - Fastlane that pulls down the Carthage directory and runs fastlane.

Artifact Plan

Use a simple setup to create this build plan. First off you want to make sure this plan is manually triggered only - because you only need to run it whenever the Cartfile changes as opposed to after ever single commit. It could also be on a nightly build perhaps if you desire.

Stages / Jobs / Tasks

This plan consists of 1 Job, 1 Stage and 2 Tasks

  • Task 1: Source Code Checkout
  • Task 2: Script (carthage update)

Artifact definitions

Create a shared artifact with the following info:

  • Name: CarthageFolder
  • Location: (leave blank)
  • Copy Pattern: Carthage/Build/**

Optional: You may want to automatically make the fastlane plan trigger whenever this plan is built

fastlane plan

When configuring fastlane to run in this setup you need to make sure that you are not calling either:

reset_git_repo(force: true)

or

ensure_git_status_clean

as these calls will either fail the build or delete the Carthage directory. Additionally you want to remove any carthage tasks from inside your Fastfile as carthage is now happening externally to the build.

Build plan setup

What this build plan does is it checks out the source code, then it downloads the entire Carthage/Build/ directory into your local project - which is exactly what would be created from carthage bootstrap and then it runs fastlane

  • Task 1: Source Code Checkout
  • Task 2: Artifact Download
  • Task 3: fastlane

GitLab CI Integration

Use GitLab CI Runner running on a macOS machine to build using fastlane.

Repository setup

First create a Gemfile in the root of your project with the following content:

source "https://rubygems.org"

gem "fastlane"

Add a .gitlab-ci.yml file to trigger fastlane.

stages:
  - unit_tests
  - test_flight

variables:
  LC_ALL: "en_US.UTF-8"
  LANG: "en_US.UTF-8"

before_script:
  - gem install bundler
  - bundle install

unit_tests:
  dependencies: []
  stage: unit_tests
  artifacts:
    paths:
      - fastlane/screenshots
      - fastlane/logs
  script:
    - fastlane tests
  tags:
    - ios

test_flight_build:
  dependencies: []
  stage: test_flight
  artifacts:
    paths:
      - fastlane/screenshots
      - fastlane/logs
  script:
    - fastlane beta
  tags:
    - ios
  only:
     - /^release-.*$/
     - master

See the `.gitlab-ci.yml documentation for more information on how this file works.

Setting up the lanes

You should have a lane - in this example called beta - which should do the usual, match, gym, pilot, to distribute an updated Test Flight version, and one lane tests which calls scan to run UI Tests.

Auto-incremented build number.

To get an auto-incremented build number you can use something like the following lane:

lane :increment_build_number do
  increment_build_number(build_number: ENV['CI_BUILD_ID'])
end

Then the GitLab CI build ID (which iterates on each build) will be used.