iOS Continuous Deployment with Bitbucket, Jenkins and Fastlane at UpGrad

Prateek Grover
Technology at upGrad
7 min readJun 14, 2018

--

Credits: Lynda.com

Continuous integration has become an indispensable part of all development processes — irrespective of whether the project is as small as a personal task or as broad in scope as an enterprise level product.

As part of the mobile team at UpGrad, we constantly have to create builds and upload them to the Apple App store or Google Play store. Naturally, the process of creating a build and submitting to the app store is very lengthy, time consuming and requires a lot of manual effort. But before submitting to the app store, we need to be sure that all the tests have been passed, and for that we need continuous integration in place.

Now one might think that this is easy to accomplish using existing tools like Circle CI or Travis CI. We decided not to go ahead with them because of several factors and cost being one of the major factors in the long run. Besides, in our case here at UpGrad, we are already using Jenkins as the CI/CD tool for our backend services so it made sense to integrate iOS CI/CD with Jenkins, for easier manageability.

Here, I will be explaining the steps to setup a Bitbucket repository with Jenkins and Fastlane using a Mac OSX slave (for iOS). Let’s get started:

Pre-requisites:

  1. Macbook Pro or any other machine running macOS with Xcode installed.
  2. A server running Jenkins on it. If you do not have one, there are plenty of tutorials on how to setup a Jenkins server.

Disclaimer: Some of the steps like installing GIT, creating SSH keys or installing JAVA have been omitted.

The idea is to use Jenkins as a routing mechanism. This is done by setting up a master slave configuration in Jenkins where the master is the Jenkins server and slave is Macbook Pro. So, the flow would be somewhat like below:

Flow diagram for the integration

The first step is to create a node on Jenkins.

  1. Log in to Jenkins and go to Manage JenkinsManage Nodes and click ‘New Node
  2. Mention the node name, select Permanent Node and proceed
  3. You will be presented with a screen with various configuration options for the node. Let’s discuss some important ones:

Remote root directory: This is the directory that Jenkins master will access in your local Macbook Pro. This can be anywhere in your system. Mention the path in “Remote root directory” option.

Usage: We will be using the Macbook Pro mostly for iOS builds and releases so mention “Only build jobs with label expressions matching this node” in the “Usage” option. This is used when we create Jenkins jobs meant for this particular node.

Launch Method: We will be using Java Web start as Launch method.

4. Click “Save

Go back to Manage JenkinsConfigure Global Security

Scroll down and look for an option “TCP port for JNLP agents”. Mention a port 59072 or any other port that can be opened on the node and keep it fixed.

5. Head over to your Macbook Pro and download slave.jar from

https://<CI Server URL>/jnlpJars/slave.jar

6. Create a directory on the same path as mentioned in the remote root directory.

7. Move the two files in that directory.

8. slave.jar will help us connect to the master Jenkins server. We need to run it using appropriate parameters.

9. To test that the connection is being established, run the following command (after appropriate substitutions inside angle brackets):

/usr/bin/java -jar /Users/<username>/jenkins/slave.jar -jnlpUrl https://<CI Server URL>/computer/OSX2/slave-agent.jnlp -workDir /Users/<username>/jenkins -jnlpCredentials <username for Jenkins>:<password for Jenkins>

You will see your slave status as “IDLE” on your Jenkins home page. That means your slave was able to connect to the server successfully.

But it is a problem to ssh into the CI server and manually typing the command every time the system reboots. We would want the command to run automatically on Login. So let’s set that up now.

First of all, set up automatic login on your machine. You may have a look at this tutorial to do that.

Next, create a plist file at/Library/LaunchAgents/org.jenkins-ci.slave.jnlp.plist

Put in the below details in it with appropriate substitutions inside angle brackets:

This plist will run the previous command that we ran to connect to the master, every time the machine reboots.

Now, when a CI server is run, it needs to fetch the latest version of the repository to build and run tests. For this, it needs to have access to the repository.

For this, you may generate an RSA pair to authenticate the machine to Bitbucket.

Problem faced: It always hangs if there is a password for the SSH key.

So follow this guide, but do not enter the password for SSH key

Now the machine can connect to the master Jenkins and clone the repository and build and run tests. All we need to do is write Jenkins job for it.

For that, we would need a plugin called Bitbucket Plugin from here

Now this plugin though fine, has some caveats. We have to build on the base setup by this plugin. The plugin only provides support for repository push events but not for pull requests. So I forked the plugin’s repository on Github and made some changes to enable that support.

After that, and a couple of hours of figuring out the way to build and package a Jenkins plugin, I got the plugin that will work for me.

Install the plugin and reboot the Jenkins server.

Now we are ready to write Jenkins jobs.

We would need two jobs here: one to parse the Bitbucket payload that will trigger the other job with the appropriate parameters.

Lets start configuring the first job:

  1. Create a new job and give it a name.

2. Mention the GIT URL of your repository and enter the credentials so that Jenkins server has a read access to the repository.

3. Under “General”, for “Restrict where this project can be run” mention your node’s label expression that we specified while creating the node.

4. In the “Branch to build”, mention “**” for any branch.

5. Under “Build triggers”, tick the box for “Build when a change is pushed to BitBucket”. This is from Bitbucket plugin we installed.

6. Under “Build”, click on Execute Shell. Here, we need to write our parsing script. Below is the one we are using currently, you may change it as per your needs with appropriate substitutions inside angle brackets:

The job token in the above script is for the next job that we are going to create.

  1. Create another job and give it a name, land on the configure page and mention the usual stuff.
  2. Now here, we want this job to be parameterized because the previous job will send it some parameters based on which this will run its tasks.
  3. Click on “This project is parameterized

Enter the details for the 5 parameters as mentioned below:

  1. release”: Boolean and Default value “false”. Whether this should be a production release.
  2. branch_name”: String and Default value “develop” (Could be different in your case). The target branch to build and run tests on.
  3. pullrequest”: Boolean and Default value “false”. Whether this was a pull request notification.
  4. beta”: Boolean and Default value “false”. Whether this should be a staging release.
  5. destination_branch”: String and Default value “develop” (Could be different in your case). In case of a pull request, what is the destination branch.

In the “Branch Specifier” mention “${branch_name}

For “Additional Behaviours”, mention “Wipe out repository and force clone” to prevent any side effects.

Under “Build Triggers”, tick the box for “Trigger builds remotely (e.g., from scripts)” and generate a long alphanumeric token this online service and paste that in there. This will be your job token

Under “Build”, click on execute shell and write a script you want to run on your machine to build and run tests for your iOS application. Typically, this will include selecting an appropriate branch, installing pods(if you are using Cocoapods) and running fastlane commands.

Below is the script we are using:

Some important points in this script:

  1. We are installing a different ruby version so that the gems installed do not interfere with the system rubies.
  2. When we connect to any host using SSH, the machine asks us to put the domain and IP in the known_hosts list. To prevent that user input, we are running ssh -o StrictHostKeyChecking=no “bitbucket.org”
  3. Everything else is pretty straight forward.

Write your Fastfile to create builds and you are good to go.

To test the setup, push commits to your repository and see if everything is working.

That’s it folks. You have a running CI/CD system ready to build, test and deploy your iOS applications.

NOTE: This guide does not claim to be the ultimate guide. The scripts could be sloppy and methods wrong. I would be happy to be corrected and improve upon the process setup here.

If you are interested in joining UpGrad, please visit our careers page to know more about the open positions

--

--