Integrating CI/CD for MULTIPLE environments with Jenkins & Fastlane (Part 1/2)

Eleni Papanikolopoulou
11 min readSep 19, 2020

--

Incorporating Continuous Integration (CI) and Continuous Delivery (CD) in the development process is undeniably the only way to validate the correctness of code changes and to identify integration errors from early on. It is also a way to have bug-free versions of an application, quickly available for testing, and ready to be shipped to production after significant code alterations.

By integrating CI/CD in our day-to-day work, the developers can achieve two significant goals: First, the ability to run test suites on a server which is different than the development machine, so as the engineer can continue working on developing new features in a distractions-free manner, and second, the ability to send remote builds to their Product Owner or QA Engineer, in order to demo/test/review a new feature even if they cannot build and run the project on their own machines.

But what happens when there are multiple staging environments, and tests must run in different servers or features must be tested in different environments? Here’s where the magic happens with Jenkins and Fastlane, tools that allow to automate the process for different configurations and this is exactly where this article focuses on.

There are many excellent articles and tutorials that describe in detail how to use Jenkins and Fastlane in order to set up CI for a mobile project. However, where I want to concentrate is how we can configure the relevant scripts to work for multiple environments.

Jenkins & Fastlane 🛠

In order to understand how we can integrate CI in a project, we have to understand how Jenkins and Fastlane work. Very briefly, Jenkins is an open-source continuous integration server written in Java and it is one of the most widely used tools for managing continuous integration builds. Jenkins offers the ability to create delivery Pipelines which basically are lifecycle processes which are called Jobs, including build, document, test, package, stage, deploy, static analysis and much more. Jobs are interlinked with one another in a sequence forming the pipeline and this is where Fastlane gets involved.

Fastlane is an open-source tool that is used to automate the deployment and distribution process of mobile projects (iOS & Android) offering a great number of automation features within the lifecycle of a mobile app such as packaging, code signing, distribution of builds and many more. Fastlane allows to create Lanes which are basically scripts, meaning a series of commands called Actions that describe the workflow we want to automate.

Our Goal 🥅

So what we basically want to achieve is automate the process of distributing our iOS application by automating the uploading of our application to Testflight, from where the stakeholders (Product Owner, QA Engineer, Sales person that wants to demo the app, you name it) can log in and download it effortlessly. What is more, we want to take it a step further and be able to upload to Testflight, automatically with the click of one button:

a) for multiple feature branches

b) for multiple configurations corresponding to a different environment

This will be our end goal 🙏.

Prerequisites 🤓

In this article, we consider a prerequisite that Jenkins and Fastlane have been set up, there is a great number of tutorials out there that describe in detail how to successfully perform the set up. Additionally, Jenkins will need some plugins to be installed in order in order to allow Jenkins to be triggered from Github’s webhooks and run a job. Those are the Github plugin, the Xcode plugin, the SCM plugin (Source Control Management) which will be used in order to checkout our project from Github and the Credentials Plugin in order to bound credentials to environment variables.

Let’s start 🙌🏻

To begin with, we need to create a new Pipeline in Jenkins. This pipeline will be described by a script we are going to create that performs exactly this: uploading our app to Testflight.

From the Jenkins Main Dashboard menu below, we select the first option => New Item (Pipeline) and we create the Jenkins Job named “Upload to Testflight”

To proceed we select the Configure option from the left menu and we go on by adding the configuration for the pipeline we just created. We start by adding a small description.

From the Definition field below, in the Pipeline section, we choose the the “Pipeline Script from SCM” option. This option instructs Jenkins to obtain the Pipeline from Source Control Management (SCM), which will be our cloned Git repository.

We add a parameter which will be the branch we want to build and upload to Testflight each time and also we add the link of the github repository by providing also a parameter for the github login credentials.

Finally we define the script which will describe the whole pipeline process.

We hit the Save button and voila! We have managed to create our Job in Jenkins. Now we are ready to start writing our script!

We will basically use the Scripted Pipeline approach. Our script is going to have several phases called stages which describe what our pipeline wants to do. We ultimately want to upload to Testflight but before uploading to Testflight, we want to make sure that our unit tests pass successfully. So the different phases of the automation script should include the following stages:

  1. Checkout repo
  2. Install dependencies
  3. Reset simulators
  4. Run tests
  5. Build project
  6. Upload to Testflight
  7. Cleanup

Once we have written our script we want to hit the Build with Parameters option from the menu of the Pipeline we just created and specify the branch we want to build:

After we hit the Build button, and the Pipeline runs successfully we will see the following, in Jenkins Stage view:

which will mean that we have successfully uploaded our app to Testflight!

In case one of those stages fails, it automatically means that the whole Job will fail and we will see the corresponding stage with a red color. In this case we can go to the Console Output option of the Job’s menu, review the logs and locate the cause of failure, and of course re-run the job after we have fixed the issue.

The script 📜

So in our script, that we named MyScript.groovy, we define a function called deploy() and inside we are going to implement the afore-mentioned stages as follows:

  1. Checkout repo

We checkout our repository by using the checkout command of the SCM plugin which will run the project checkout using the configuration options that we specified in the Jenkins Pipeline.

stage('Checkout') {
checkout scm
}

2. Install dependencies

stage('Install dependencies') {
sh 'gem install bundler'
sh 'bundle update'
sh 'bundle exec pod repo update'
sh 'bundle exec pod install'
}

Our project uses CocoaPods as Dependency Manager, therefore we need to run pod install in order to download the project’s dependencies. In order to execute this command, and all other shell commands, we use Bundler, which is a tool to manage a Ruby application’s gem. By executing the first 2 commands, we manage to install the bundler and then the bundler downloads all the gems that are specified in the project’s Gemfile. This is the stage where we can specify the set of tools that our app might need, for example Fastlane that we will use right after, or if we want a linter or Danger, this is the place to determine those ruby gems. Then we go on and we run pod repo update in order to always have the latest pod versions and finally we run pod update in order to download the project’s dependencies.

3. Reset simulators

stage('Reset Simulators') {
sh 'bundle exec fastlane snapshot reset_simulators --force'
}

This step is optional but essential if we want to run the unit tests in the next stage effectively, and without having to worry about a potential stale condition of a simulator which may result in the unit tests failing.

So this is the first time that Fastlane gets in the picture. As I mentioned earlier, Fastlane uses Lanes and Fastlane has already a great number of predefined lanes, or we can create our own custom lanes inside a file called Fastfile. Here we are using a predefined lane that does exactly what we want.

So this command will delete and re-create all iOS simulators, which one might say it’s kind of extreme but sometimes “Drastic times call for drastic measures” 😜.

4. Run tests

stage('Run Tests') {
sh 'bundle exec fastlane test'
}

In this stage we run the unit tests of our project. Now the test is a custom lane that we have implemented inside our Fastfile and has the following implementation:

Here we use two basic Fastlane actions, the scan and the slather.

Scan is used in order to actually run the unit tests and can be configured with several parameters such as the workspace, scheme, code_coverage and most importantly the devices where we can specify the simulator in which we want to run our unit tests.

Slather is used in order to gather code coverage while running unit tests, and also takes several arguments such us the output directory of the coverage report and also an array of documents to ignore while gathering the code coverage.

This is where the Fastlane magic ✨ happens. By only defining this lane with just two actions we manage to effortlessly run our unit tests inside Jenkins. Note that they should obviously all be successful, otherwise this stage and the entire job will fail.

5. Build

In this stage we use the withEnv Jenkins Pipeline function in order to set the necessary environment variables as specified here according to the Fastlane documentation. So we specify the FASTLANE_USER environment variable. Afterwards we set two more environment variables, the MATCH_PASSWORD and the FASTLANE_PASSWORD which cannot be obtained without credentials. Basically they are stored, encrypted for obvious reasons, inside Jenkins under the “Credential” menu option of Jenkins Dashboard, in the secret_text format, and they are obtained by providing the credentialsId.

Again, for the build phase, we have implemented a custom lane inside the Fastfile which will be the most complex lane that we will need to create as follows:

Now let’s take this step by step.

We start by using the match Fastlane action. Match basically creates all required certificates & provisioning profiles which are stored in a separate git repository, so it basically automates the code signing experience. This means that prior to running match, we should have created a different Github repository where we have stored our provisioning profiles. Alternatively, if we don’t want to use match for the code signing, we can use the sigh and cert actions.

Now this is where the interesting part comes up. What we want to automate, is to increase the build number for the same release in order not to do this manually every time from Xcode build settings. We are all aware of the fact that, in order to upload multiple times a build to Testflight for the same release, it automatically fails if we don’t increase the build number, and each time we have to go to the project settings or .plist file, do this manually and then try to re-upload it. With the above code, we manage to automate this procedure by following the 3 steps below:

  1. get_version_number : Get the version of the project that is currently uploaded
  2. latest_testflight_build_number: Get the current build number for the version we obtained in the previous step
  3. increment_build_number: Increment the build number by specified times (here by one).

This is also Fastlane magic ✨ to me!

Finally we continue by calling the gym action which makes the actual build and packaging of the application. It gets configured with several arguments such as the configuration, scheme, and xcargs where we can specify the bundle_identifier, export_options etc.

6. Upload to Testflight

Again here, we specify the environment variables necessary, same way we did in the previous step, and we implement another custom lane inside the Fastfile as follows:

We use the pilot Fastlane command which uploads the generated .ipa file from the previous step to Testflight. With this action we can also specify a changelog. We can also skip the submission of the binary, which means, the .ipa file will only be uploaded and not distributed to testers.

7. Cleanup

Last but not least, this is the step where we perform the Workspace cleanup by using the cleanup Jenkins plugin.

stage('Cleanup') {
cleanWs notFailBuild: true
}

This means that we delete the Workspace when the build is done since it’s no longer needed.

So to sum up, this is is how the deploy() function looks inside a Deploy.script we have created.

And this is how our Fastfile looks like now:

The deploy() function is called from the script that we have defined in the job, the MyScript.groovy, and is the following:

We load the Deploy.groovy script and we call the deploy() function that does all the work. Here we can notice that we also load a utils.groovy script which basically helps us set some environment variables before starting the Jenkins.job. The ansiColor is another Jenkins Plugin that is used in order to colorize the output of the steps in a pipeline build. Finally, we can notice that we start the script inside a

node(label: 'ios')

In the Scripted Pipeline, the above node is a crucial first step as it allocates an executor and workspace for the Pipeline.

And this is how we have managed to create a Jenkins Job that distributes different feature branches of our application by defining the feature branch as a parameter in Jenkins.

Part 2

In the next part, we will examine how we can configure Jenkins in order to distribute our application for different environments and thus for different Xcode configurations.

--

--