iOS Automation: Fastlane and Jenkins

iOS developers’ ultimate guide to manage time, signing process and builds.

Tarek Ezzat Abdallah
12 min readFeb 21, 2021

Developing a mobile application has always been more than writing code. As an iOS developer, you always have to suffer with certificates, provisioning profiles, archiving, releasing a test build and submitting to the store. Personally, I have always faced issues with the code signing process when it came to working with a team. Should I use automatic signing? What should I do when the certificates expire? The app is archiving on my colleagues’ computers but not mine, let me download the certificates all over again. I forgot to update the build number, I should archive again and wait to make sure all is well. I forgot to upload a TestFlight version to the QA, the release will be late :/

As a senior iOS developer or a team leader, you should guarantee that every team member, especially the new recruits, have a seamless experience in order to be as efficient as possible. A major part of that is an easy access to certificates, eliminating time consuming tasks that are not very relevant to their jobs such archiving and releasing, and eliminating the human error factor. So you are in a front of two choices, handle the code signing/release process yourself or delegate it to a specific developer. Well I’m here to offer you a third option, automation.

This article will guide you through automating this necessary yet irrelevant stage of development, even if you don’t have any experience with CI/CD.

If you’re interested in automating your Android process you can check Android Automation Guide.

First things first

You need to:

  • Use a mac to complete this task.
  • Have HomeBrew installed. Check the installation guide.
  • Have Xcode installed.
  • Have rbenv installed due to issues on the stock ruby environment while used with bundler. Check the installation guide.
  • Have an apple Developer account, and enrolled in developer program.
  • Have a project to work with configured with git. (This article is based on a Github project)

Notes about the article:

  • The usage of ${any_text} means that you need to add the variable or value without ${}.

Author’s Advice:

  • It’s better to use a text editor that highlight your code. Sublime Text would be a good option.
  • The article covers the main functionality of fastlane. You can always check more capabilities of this powerful platform. Give the website a look!
  • Take a look on Jenkins website to see it’s full potential!

Fastlane

Let’s start with what is fastlane

fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. 🚀 It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.

Fastlane it a set of lanes defined by the user where you call a lane and it does the action defined.

For example having a lane called release should trigger the following actions:

  • Check if the app builds.
  • Archive.
  • Upload the app to Apple connect.
  • Upload dSYM to Crashlytics.

Once these steps are defined, you will have to call fastlane release rather than do all these steps on your own and risk missing any of them.

Fastlane will make the process better and atomic. However, it will not make it automatic. Jenkins will be our automation tool.

Setup fastlane

Install fastlane on your machine

brew install fastlane

Navigate to the project and add fastlane

fastlane init

Then Select.

4. 🛠 Manual setup — manually setup your project to automate your tasks

This step will add a fastlane folder and the needed files including Gemfile.

Add Cocoapods to the Gemfile:

It’s always a good idea to specify the version of the dependencies. By default you will have your fastlane dependency.
gem 'cocoapods' ~> '1.10.1'

How does Fastlane work?

Think of fastlane as an automation script you write, once you call the function it will execute the steps on the shell.

These automation instructions are called lanes and they’re written in Ruby. These lanes are to be written inside the automatically generated Fastfile. Once a lane is required, you simply call fastlane ${lane_name}.

There are two types of lanes:

  • Private lane:
  • Public lane:

The access modifiers work just like any development language. The public lane can be called from anywhere. While, calling a private lane from the shell using fastlane ${private_lane_name} will result in an error.

You can’t call the private lane ‘${private_lane_name}’ directly

The preferred way to use lanes is to create a wrapper for your actions in private lanes, then call the private lanes from a lane. This way you can guarantee that the actions will always be executed in sequence and you will never miss a step. Even more, you will be able to update a functionality in all the lanes by updating its private lane. Below is a quick example:

As you notice, you can pass arguments to your lane, arguments are passed in this case by using fastlane release app_version:1.0.0 . When |options| is used to give the lane access to arguments, the order of these arguments doesn’t matter.

How to manage Certificates?

There are multiple ways to manage your certificates on iOS. The easiest way would be automatic signing, which with experience you know it’s not the preferred way. This is why we will be using match, a fastlane tool that handles fetching and creating certificates and provisioning profiles on your apple developer console.

How does match work?

In order to use match, you will have to create a private repository for your certificate. Once you start using match it will add the certificates to this repository and every time someone new joins the team 🎉 will be able to call our fastlane fastlane fetch_certificates and ready to go.

Setup Match:

Create Certificates repo:

You can create a private git repo for your certificates. It is important to have your repo private for security purposes.

Install Match:

fastlane match init

Then Select

1. git

You will be requested to enter the created repo URL (the one used here is a private repo created as an example):

https://github.com/tarekabdallah/certificates_repo_medium.git

This step will create a match file inside fastlane directory in your project file. Please make sure to update the file to look like the one below.

I created this template format in order to handle the certificates from fastlane which will provide more flexibility and easier onboarding for the team members and new certificates.

Add match to Fastfile:

In this step you will create two new lanes, one for updating the certificates which you will call whenever you add a new bundleIdentifier to your projects or when you add a new device to the portal.

At first you need to call bundle exec fastlane update_certificates in order to add our certificates to the repo. You will be requested to create a paraphrase. It is crucial to save/remember this paraphrase since every time you will call this lane or bundle exec fastlane fetch_certificates on a new machine you will be requested to supply it in order to decode the certificates.

After running the fetch_certificates lane the user will have the certificates to sign the app.

PS: Note that the user calling update_certificates must have admin access to the developer console.

Building the ipa:

Before building the ipa, you need to make sure that you have the pods installed.

Installing pods:

In order to install cocoapods, you will be using the cocoapods function that is added to fastlane when you added the cocoapods gem to the Gemfile. You can check the documentation for cocoapods here.

This lane is private since it will be called only from within the Fastfile.

Updating build and version number:

You can take multiple approaches to handle build and version naming:

  1. Set the build number and the version code from within the Xcode. All you have to do is set them in the Project setting -> General.
  2. Pass these variables as arguments when calling the lane. This way you will have to call build_ipa(version_number: ${version Number}, build_number: ${Build Number}.
  3. Have the values read from a file.
  4. Fetch the highest build number from TestFlight for this version testflight_build_number. You can check the documentation of this function here.

You can alway mix and match these approaches, or come up with your own approach. In this example, you will be setting the version number from within Xcode and reading the build number from a file.

In order to build the ipa that will be uploaded to TestFlight, you will need to use a function called gym. You can check the documentation for gym here.

gym:

Now you have everything to build the ipa. You will benefit from gym, a tool to build ipa. You can check the documentation here.

Release:

After having the ipa ready to be released, it is time to do the release. You can release to an adhoc distribution service like Firebase distribution, or directly to TestFlight. In this example you will be doing the release for TestFlight only.

Create the upload_testflight lane:

First of all, you should create a lane that will handle uploading to TestFlight by using upload_to_testflight. You can see the documentation here.

Upload dSYMs to crashlytics:

In order to access your crash reports on Firebase Crashlytics, you will need to do this step.

You will be using upload_symbols_to_crashlytics. check the documentation here.

This would be the lane call upload_symbols_to_crashlytics(dsym_path: ${dsym_path}, gsp_path: ${gsp_path}.

The final Fastfile:

In your fast file you will be using environment variables. These variables can be set in two ways.

  1. Create a .env file inside the fastlane directory. Then add all your variables to this file. This approach is a good approach to add non critical variable.
  2. Add the variables to your ~/.zshrc if you use zsh or to your ~/.bash_profile if you use bash. You can achieve this by adding export ${variable_name}=${variable_value} in the respective file.

I prefer using .env for the non critical files, like bundle identifiers, workspace, schemes, google-services-info.plist path (referred to as gsp path) and etc. However I add the critical variables, such as access tokens and password, inside the shell environments. That’s to avoid pushing critical info to the git repo.

The Fastfile will look like this!

And the .env:

Make sure that you fastlane directory is included in git.

It would be best to run the update_certificates once on the CI server shell in order to login to git and apple accounts, which will both be saved on keychain to be used when needed.

Automate with Jenkins

In the first part of the article, we went through handling the build and push to TestFlight process without the using fastlane over the shell. Let’s automate the builds.

What is Jenkins?

The leading open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project.

In this article, I am setting up Jenkins on the local machine for simplicity. However, I will be collaborating with a colleague (Experienced DevOps Engineer) on an article on how to do the setup on a AWS Mac server

Define the automation process

First, you will have to create a file for Jenkins to define the automation process.

Jenkinsfile

Jenkinsfile is a text file that contains the definition of a Jenkins Pipeline and is checked into source control. Consider the following Pipeline which implements a basic three-stage continuous delivery pipeline

Jenkins will be executing shell commands and building our project by calling the lanes you already prepared in the Fastfile.

Now you should follow these steps to have your Jenkins file:

  1. Got to your project root directory.
  2. Create an empty file and name it Jenkinsfile. This file doesn’t have any extensions.
  3. Open the text editor.
  4. On the first line, add #! groovy since Jenkinsfile written in groovy.

You can copy the following template to your file.

You will be doing the automation on multiple stages. These steps will be appearing on your console when building.

Stage 1: Setup

This stage makes sure that the environment is ready to use fastlane and any other gems you are using.

Stage 2: Build

In this stage you will check if the app builds

Stage 3: Publish to TestFlight

In this stage, you will call the release lane, which will create an ipa, publish it to TestFlight and upload the dSYMs to Crashlytics.

The final Jenkins file will look like this:

I added the 3rd party stage as an example of checking the branch of it falls under any of the branches

Make sure that you Jenkinsfile is included in git.

Setup Jenkins

Install Jenkins on your machine

brew install jenkins

Automatically start Jenkins on mac start (Optional)

brew services start jenkins
This command will add Jenkins to the LaunchAgents which will make it run on mac start. After this, you MUST restart your mac for Jenkins to start.

Start Jenkins Manually

jenkins start

After having Jenkins running on your system, you can access it using your browser by going to 127.0.0.1:8080 or localhost:8080.

Unlocking Jenkins

When you open Jenkins for the first time, it will ask you about the initial password to unlock.

You will be able to copy the initial Admin password using cat ~/.jenkins/secrets/initialAdminPassword | pbcopy and then paste it in the displayed location.

Selecting plugins

Click on install suggested plugins and wait for the installation to finish.

Creating the admin user

Afterwards, Jenkins will open a panel to setup your admin account. Fill all the information and click on continue.

Instance Configuration

In this step, you can create your Jenkins URL. In this article we will stick with the localhost.

Finally, click on Start Using Jenkins.

Project setup

Now it is time to setup your project.

After you click on New Item you will see the screen below.

  1. Enter the name of the project you want.
  2. Select Multibranche pipeline.
  3. Click OK.

Afterwards, the project configuration will open.

Jenkins Project Configuration

Add Source

Click on Add source and then select git.
You can connect to the git repo either using ssh or using https. In this example I will use https.

Add Credential

Under credential, click add -> Jenkins. The following popup shows.

Fill the information and press Add

Branch Discovering

By default, Jenkins discovers all the branches and build the ones with JenkinsFile (will go through that in the upcoming steps).
If you want to add additional rules like filter, to filter by branch name, or add Tag discovery:

  1. Under Behavior section in Git, click on Add.
  2. A list shows and you can add the behaviors you want.

For example, you want to build only master, develop, and every branch starting with feature/.

In this example, the branches are being checked as wild cards where “*” can be anything. You can filter by regular expression if needed.

Finally, click Save in order to save the configuration.

Now Jenkins will connect to the repo and check if there are any branches to build.

Discover branches on git push

There are multiple ways to setup discovering branches on push.
Multibranch Scan Webhook Trigger is one of the easiest and best way to achieve our goal.

Install plugin to Jenkins

To install a plugin on Jenkins:

  1. Open Jenkins plugin manager screen:
    - Go to Jenkins Home screen -> Manage Jenkins -> Manage plugins
    OR
    - Open Jenkins_URL/pluginManager
  2. Select Available Tab.
  3. In the Search bar, type Multibranch Scan Webhook Trigger.
  4. Click the box next to it.
  5. In the bottom of the screen, click Install without restart.
  6. The Plugin install screen shows, select the box Restart Jenkins when installation is complete and no jobs are running

Add the webhook configuration

After Jenkins has restarted, Navigate to your project configuration and scroll down to Scan Multibranch Pipeline Triggers.

Now you have a new option, Scan by webhook. Select the box next to it and enter the token.

Configure Github webhook

After configuring Jenkins to check for build on webhook invocation, you need to configure the Github project.

  1. Open Github Project.
  2. Select Setting tab.
  3. Select Webhooks tab from the left menu bar.
  4. Click on Add webhook.
  5. In the Payload add: ${JENKINS_URL}/multibranch-webhook-trigger/invoke?token=${Token_specified_in_jenkins}
  6. Don’t add a secret.
  7. Select the event triggers.
  8. Click on Add webhook.

PS: The Jenkins URL shouldn’t be localhost. If you are working on the local machine, you can use ngrok to expose localhost:8080 and access Jenkins.

This is how to make ngrok point to your Jenkins server. Copy the generated https url and use it as Jenkins_URL

Now you can try pushing any commit and see how Jenkins handling all the hassle!

PS: You might need to generate an App specific password. To do so, go to AppleId, under security section click generate an app password. Now you can add to to your Jenkins environment variables under the key FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD.

Congratulations! Now you have successfully automated your signing and release process.

Final Note:

Now that you started benefiting for the wonders of automation. You can improve your CI/CD to handle running the tests, enforcing code style with SwiftLint or even taking screenshots!

You can always integrate your CI with many powerful plugins. For instance, you can inform your team of the new build using Slack Integration with fastlane, Jira integration with fastlane or Jenkins or tons of other integrations.

--

--