Automate iOS deployment process by CI/CD using Gitlab CI and Fastlane

Naman Mittal
12 min readMar 9, 2022

--

When we develop iOS apps, we usually manage the app publication process using the Xcode Organizer. Then we sign, test, build, archive, submit, change versions, submit new builds again and again to the TestFlight or AppStore.

If we generate our builds daily, this process is tedious and tiring. So, in this article I am going to show you how to implement Gitlab CI in your iOS project and then we will see how to automatically upload builds on TestFlight.

What is CI/CD?

Continuous Integration and Continuous Delivery (CI/CD) for iOS enable us to improve our build deploys. We’re able to release updates at any time in a sustainable way without the hurdle of doing it manually every time. We don’t need to run all our tests when we add new code or take a trial and error approach before pushing a commit to our repository.

  • GitLab comes with CI built-in for all projects, for free.
  • Fastlane helps you to automate the most time-consuming beta distribution steps including incrementing the build version, code signing, building and uploading the app, and setting a change-log.

Continuous integration with Gitlab

You will need an iOS app to configure our Continuous Integration and Delivery pipeline. Then, you will need to configure a new repository.

Before jumping into the implementation, let us first understand the workflow of Gitlab CI.

  1. You make changes to your copy of the codebase and push a commit to GitLab.
  2. GitLab recognizes that the codebase has changed.
  3. GitLab triggers a build with the GitLab Runner you set up on your Mac for the project.
  4. The GitLab Runner runs through the build and test process you specified in .gitlab-ci.yml.
  5. The GitLab Runner reports its results back to GitLab.
  6. GitLab shows you the results of the build.

For implementation, we have these basic requirements:

  1. Gitlab: as our CI(Continuous Integration) platform
  2. Xcode 11 or above with Swift 5.1 (or above) as our development tools.
  3. XCTestCases to create and run unit tests and UI tests.

Assumptions & Prerequisites

We are assuming that you have the Gitlab repository of your iOS app already setup. Also, these things are mandatory in your Xcode project:

  1. Make sure to include Unit Tests and Include UI Tests in the project.
  2. Have a gitlab repo setup for your project.
  3. Make sure that your project scheme is Shared.

Note: To share a scheme in Xcode, choose Product > Scheme > Manage Schemes.

To make the product scheme shared

By sharing project scheme, GitLab CI gets context it needs to build and test your project.

Now, open Terminal and navigate to the folder you created for your iOS project.
It’s convenient to add a standard .gitignore file. For a Swift project, enter:

$ curl -o .gitignore https://www.gitignore.io/api/swift

The curl command conveniently downloads the contents of the page at the given gitignore.io URL into a file named .gitignore.

Install XCPretty

Install xcpretty for test case reports. When Xcode builds and tests your project, xcpretty will transform the output into something more readable format.

Installation:
Open Terminal and enter the following command:
$ sudo gem install xcpretty
OR
$ gem install xcpretty --user-install

Try using any one of the above, in case the other one throws some permission error (which I personally faced).

Installing & Registering Gitlab Runner

The GitLab Runner is a service that’s installed on your Mac, which runs the build and test process that you set up in a configuration file(.gitlab-ci.yml).

Installation
Download the binary for your system:

sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64

Give it permissions to execute:

sudo chmod +x /usr/local/bin/gitlab-runner

Register

To register a Runner under macOS run the following command:

$ gitlab-runner register

Enter your GitLab instance URL:

Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )

https://serveraddress.com/gitlab/

Enter the token you obtained to register the Runner: (You will obtain this token once you logged in to the gitlab into your account setting pipeline section. If this token is not correct then it won’t register runner to GitLab)

Please enter the gitlab-ci token for this runner

xxxxxxxxxxxxxxxxxx (Use your GitLab token)

Here you can get the instance URL & registration token in the Settings -> CICD in gitlab

Enter a description for the Runner, you can change this later in GitLab’s

Please enter the gitlab-ci description for this runner

ios-runner

Enter the tags associated with the Runner, you can change this later in GitLab’s UI:

Please enter the gitlab-ci tags for this runner (comma separated):

ios-tag, version-1.0

Enter the Runner executor: (There are given number of executors, We have used shell since its won’t have any dependancy)

Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:

shell

How to verify if Runner properly registered

  • Goto below Path <Home>/Users/<user>/gitlab-runner/config.toml
  • .gitlab-runner directory will be created. This is hidden directory
  • In that directory there is “config.toml”
  • This config file will list down all the runners installed on the Mac machine
  • You can increase the runner limit from this file. output_limit = 20000
gitlab-runner config.toml file

Commands-
To run gitlab runner use:
gitlab-runner run
To verify that your gitlab runner is running or not, enter the below command:
gitlab-runner verify
Check the runner status. If it’s not running, Please use below commands to run the runner.
gitlab-runner stop
gitlab-runner restart

You can check the runners in the Settings -> CICD -> Runners (Expand)

PIPELINE IMPLEMENTATION

Create a .gitlab-ci.yml in your project root directory with given build stages Build, Test, Archive, Release.

Build: This stage will build the your project.
Test: This stage will run the Unit and UI test cases of your project and display on your GitLab console/terminal.
Archive: This stage will generate and IPA archive file, and you can also upload it on Gitlab.
Release: There are 2 methods to release the builds:

  • This stage will upload the archive file on Gitlab(or in the local project directory) from there you can distribute the build to testers and stakeholders.
  • Other method is to deploy the build on the TestFlight using Fastlane.

The last thing to do is to configure .yml file. To do so, open your .gitlab-ci.yml file with the text editor and enter the following:

stages:
- build
- archive

build_project:
stage: build
script:
- xcodebuild clean -workspace ProjectName.xcworkspace -scheme SchemeName | xcpretty
- xcodebuild test -workspace ProjectName.xcworkspace -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 12 Pro,OS=15.2' | xcpretty -s
tags:
- ios-tag
- version-1.0
archive_build:
dependencies: []
stage: archive
artifacts:
paths:
- build/ProjectName.ipa
script:
- fastlane beta
tags:
- ios-tag
- version-1.0
only:
- master

Save this file in your Xcode project folder as .gitlab-ci.yml, and don't forget the period at the beginning of the file name!

Let’s go through the file with some detail:

  • The file first describes the stages available to each job. For simplicity, we have one stage (build) and one job (build_project).
  • The file then provides the settings for each job. The build_project job runs two scripts: one to clean the Xcode project, and then another to build and test it. You can probably skip the cleaning script to save time, unless you want to be sure that you're building from a clean state.
  • Under tags, add the tags you created when you registered the GitLab Runner.
  • The fastlane betabasically triggers the fastlane script present in the fastlane/fastfile and uploads the build on Testflight. If you want to only archive the build, then use fastlane gym instead of fastlane beta . We will look into this later in this article only.

There are also some things to look out for:

  • Make sure to replace all references to ProjectName with the name of your Xcode project; if you're using a different scheme than the default, then make sure you pass in the proper SchemeName too (the default is the same as the ProjectName).
  • In the xcodebuild test command, notice the -destination option is set to launch an iPhone 12 Pro image running iOS 15.2 in the Simulator; if you want to run a different device (iPad, for example), you'll need to change this.

There’s a simple tool for “linting” (i.e., validating) your .gitlab-ci.yml in GitLab. From your GitLab project page, click on CI/CD > Pipelines in the sidebar, then in the upper-right corner, click on CI lint:

Paste the contents of your .gitlab-ci.yml file into the text box and click on Validate.

Validate CI Lint Screen

You should see something like:

Status: syntax is correct

This won’t tell you if your project name or the Simulator chosen is correct, so be sure to double-check these settings.

The .gitlab-ci.yml file is extremely customizable. You can limit jobs to run on success or failure, or depending on branches or tags, etc.—read through the documentation to get a feeling for just how flexible and powerful it is.

How to trigger builds

To trigger a build, all you have to do is push a commit on GitLab. And Wohoo!! you have triggered a pipeline. You can see piplelines on the Sidebar menu in CICD -> Pipelines section on Gitlab dashboard.

Successful pipelines in green status

If you click on the pipeline, it will show some logs like this in green if it is passed and successfully done.

Successful pipeline logs

We have finally configured CI/CD workflow with GitLab CI.

Other salient points

  • This workflow should work for any kind of Xcode project, including tvOS, watchOS, and macOS. Just be sure to specify the appropriate Simulator device in your .gitlab-ci.yml file.
  • If you want to push a commit but don’t want to trigger a CI build, simply add [ci skip] to your commit message.
  • If the user that installed the GitLab runner isn’t logged in, the runner won’t run. So, if builds seem to be pending for a long time, you may want to check on this!
  • If you’re working on a team, or if your project is public, you may want to install the GitLab Runner on a dedicated build machine. It can otherwise be very distracting to be using your machine and have Simulator launch unexpectedly to run a test suite.

CONTINUOUS DEPLOYMENT USING FASTLANE

In this article, we will archive the build using fastlane and then later deploy it on Testflight.

Installing Fastlane in Mac

Fastlane setup is the crucial step in the whole process since it is the backbone of the deployment process. To install Fastlane in your system, please go through the below steps: Install the latest Xcode command line tools:

sudo gem install fastlane -NV

Install fastlane using rubygems or homebrew:

# Using RubyGems
sudo gem install fastlane -NV
# Alternatively using Homebrew
brew cask install fastlane

To start using fastlane in your project, you’ll need to run fastlane init from your project directory in command line.

$ fastlane init

Fastlane will ask you for some basic configuration and then create a folder called fastlane in your project which will contain mainly two files:

1. fastlane/Appfile

This file is straightforward, so you just want to check to make sure that the Apple ID and app ID that you set up earlier are correct.

app_identifier("APP IDENTIFIER") # The bundle identifier of your app
apple_id("APPLE ID") # Your Apple email address

2. fastlane/gymfile

gym builds and packages iOS apps for you. It takes care of all the heavy lifting and makes it super easy to generate a signed ipa or app file .

To create a gym file in your project, you’ll need to run this command from your project directory in command line.

$ fastlane gym init

Since you might want to automatically trigger a new build but don’t want to specify all the parameters every time, you can store your defaults in a so called Gymfile.

Run fastlane gym init to create a new configuration file. Example:

scheme("SchemeName")sdk("iphoneos15.2")clean(true)output_directory("./build")    # store the ipa in this folder
output_name("AppName") # the name of the ipa file

3. fastlane/fastfile

The fastfile defines the build steps. Since we’re using a lot of the built-in capability of fastlane this is really straightforward. We create a single lane which increments build number, gets certificates, builds, and uploads the new build to TestFlight. Of course, you may want to split these out into different jobs depending on your use case. Each of these steps, get_certificates, get_provisioning_profile, match, gym, and upload_to_testflight are pre-bundled actions already included with fastlane.

Code Signing approach:

  1. Using fastlane match

The concept of match is described in the codesigning guide.

With match you store your private keys and certificates in a git repo to sync them across machines. This makes it easy to onboard new team-members and set up new Mac machines. This approach is secure and uses technology you already use.

Getting started with match requires you to revoke your existing certificates.

2. Using cert and sigh

If you don’t want to revoke your existing certificates, but still want an automated setup, cert and sigh are for you.

  • cert will make sure you have a valid certificate and its private key installed on the local machine
  • sigh will make sure you have a valid provisioning profile installed locally, that matches the installed certificate

get_certificates and get_provisioning_profile are actions associated with the cert and sigh approach to code signing; if you’re using match, you may need to update this by commenting get_certificates and get_provisioning_profile.

There are some special environment variables we need to set to avoid the jobs failing due to 2FA from Apple that were made mandatory recently.
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD and FASTLANE_SESSION

In order to authenticate against the App Store for the TestFlight upload, fastlane must be able to authenticate. In order to do this, you need to create an app-specific password to be used by CI. You can read more about this process in this documentation.

FASTLANE_USER and FASTLANE_PASSWORD

Finally your fastfile should look like this:

default_platform(:ios)before_all do
ENV['FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD']= 'xxxx-xxxx-xxxx-xxxx'
end
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(build_number: latest_testflight_build_number + 1,
xcodeproj: "ProjectName.xcodeproj")
build_app(workspace: "ProjectName.xcworkspace", scheme: "SchemeName", include_bitcode: true)
upload_to_testflight
end
end

Note: I have added the App_Specific_Password as we are having 2fa enabled in our our developer account which was creating problem for fastlane to authenticate with AppStore. So, adding this resolves our problem.

If you want to add an App Specific password in your app, you can read the doc here. There’s so much more there in Fastlane so, if you want to explore more about it, you can visit here.

Now, add the below code to your existing .gitlab-ci.yml file for triggering the job to upload build on the Tesflight when you commit the code to master branch.

  • You have to add a new stage in our existing .yml file
  • Add Job description in the .yml file.
  • Only defines the branch name on which you want the job to be triggered.

Your final .gitlab-ci.yml file would look like this:

The last thing to do is to configure .yml file. To do so, open your .gitlab-ci.yml file with the text editor and enter the following:

stages:
- build
- test_flight
build_project:
stage: build
script:
- xcodebuild clean -workspace ProjectName.xcworkspace -scheme SchemeName | xcpretty
- xcodebuild test -workspace ProjectName.xcworkspace -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 12 Pro,OS=15.2' | xcpretty -s
tags:
- ios-tag
- version-1.0
test_flight_build:
dependencies: []
stage: test_flight
artifacts:
paths:
- fastlane/logs
script:
- fastlane beta
tags:
- ios-tag
- version-1.0
only:
- master

Save this file in your Xcode project folder and push the code on gitlab and your job will be triggered, and hence your project will be built successfully.

If you have added the fastlane gym instead of fastlane betacommand in the .gitlab-ci.yml file, and you have also created the gym file in the folder, then you just have to commit the code and your pipeline will start running and you can see the artifact on gitlab. And if you are using the fastlane betacommand, then it will upload the build on the TestFlight.

If you have only archived the build and not uploaded it on testflight by using the fastlane gym command in yml file, then you can see your archived file uploaded on the Gitlab from here:

You can download the archived file by tapping on the download button in 3 dots menu

You can also view the archived file in the system(in which gitlab runner is running). For that you just have to open the pipeline and view the logs. It will show you the path where the archived is being stored.

Path of the archived file in the system

More documents related to Fastlane can be found here.

Conclusion

Ah!! Kudos to you!!! We have finally configured our own CI/CD workflow with GitLab CI/CD and Fastlane as our primary tools.

Thanks for reading this article. Hope it proves to be helpful for you for configuring a robust CI/CD workflow for automating your iOS app deployment process.

--

--