Community: Building a CI/CD pipeline for mobile app deployment

Building a CI/CD pipeline for mobile app deployment

By JJ Niemand on December 10, 2018

Continuous Integration (CI) / Continuous deployment (CD) pipelines help developers ship software faster by automating the build, test and deploy stages of the software lifecycle. It used to be difficult to do this for mobile app development. This was because the concept of multiple environments — for example: “development”, “testing” and “production” — did not really exist. Testers would normally only have one physical device, without the ability to run multiple versions of an app alongside each other. With new tools, though, it's now possible.

In this article, I’ll do a walkthrough of how I set it up. The workflow I explain is especially helpful in environments where in-house APIs are used for back-end services and the app needs to support multiple endpoints for each API environment.



The context of my project

In a recent mobile app development project, my app needed to support multiple environments — namely "development", "test" and "production" — for our backend API. My initial approach to the problem was to follow the old "hackable" way: using constants for my endpoint URLs, and then commenting out what was not needed for the build. Although this worked well when I remembered to comment out the correct URLs, the testers got frustrated with having to constantly delete and install different app versions for different environments.

I then started looking for a better way to support this workflow with iOS. I discovered that iOS did in fact support this. I started exploring ways to leverage that support for a CI/CD tool-set, so that I could automate my distribution. This would remove the need for me to comment out the URLs, thereby eliminating all the trouble. The combination of iOS targets and a solid CI/CD pipeline would also enable the testers to have the same app installed for each environment, on the same device. This meant they didn't have to constantly remove and install different versions.

Background: What is continuous integration and continuous deployment?

Continuous Integration (CI) is the process of automating the building and testing of code every time code is committed to the code repository. CI encourages developers to commit their code as often as possible so it can be merged with changes from others to validate the code base through the build and test process. This process allows errors to be picked up and resolved much faster.

Continuous deployment (CD) builds on CI by automating the deployment of the solution after the code was successfully built and tested. In large corporate environments, this normally translates to automated deployments to several environments when the solution passes certain stage gates.

The solution I set out to build


I wanted to set up a CI/CD pipeline to automatically build, package and distribute my app for different environment configurations. This needed to happen every time I committed my code to my code repository. The tools that I used to achieve this were:

  • XCode 10.1 as my development IDE.
  • Swift 4.2 as my development language.
  • Microsoft App Center as my CI/CD technology. I chose this instead of its alternatives like fastlane and because App Center can be used as SaaS and doesn't require setup or installation. It also provides a very generous free tier: it allows up to three hours of free build-time per month.
  • GitHub to host my GIT source repository. I feel most at home with GitHub.

In the sections that follow, I'll show how I set up my app to support a development, test and production environment, by allowing different versions to be installed on the same device. Each version would be configured to use resources specific to that environment. In addition, I'll show how I set up the CI/CD pipeline to allow our app to be automatically built, tested, and distributed to our testing audience. I'll use the example of a simple "Hello world" app to show the same principles that I used in solving my real-world problem.

NOTE: Here is the GitHub repository for this project, if you want to see the end result.

Setting up the XCode project

1. Create a simple "Hello World" project

Step 1: I opened XCode and create a new project. From the template wizard, I selected "Single View App" and clicked "Next".


I gave my application a name, chose my organisation name, and also specified my organisation identifier. This is normally a fully qualified domain name, in reverse. In my case, that was "".


I clicked "Next" and selected the location where I wanted to save my app: on my desktop in a folder called "ci_cd_app".

Step 2: I clicked on my "main.storyboard" file and added two labels to the View Controller. I embedded these in a horizontal Stack View with centre alignment, filled equally and spaced by 10px.


Later on, I would use the bottom label to display information from my environment-specific setup.

Step 3: I created an IBOutlet link for the bottom label. I called it "envLabel" and changed the text in the viewDidLoad method.


Step 4: I saved my changes and clicked the “Run” button. My CI/CD application was now working and the text on the bottom label could be changed from my code.

Next, I would enable multiple environments and link the configuration to my code-base.


2. Enable multiple environments

To be able to reach my overall goal of setting up a CI/CD pipeline to streamline delivery, I need to ensure that the app was properly configured for multiple environments. Here’s how I did that.

Step 1: I clicked on my project on the left side and then clicked on “General”. I had one target with the same name as the application. I renamed this target to “development”.

Step 2: I right-clicked on the “development” target and selected “Duplicate”. I changed the name to “test”, and did the same for production. Once I completed this, my setup looked like this:


XCode automatically creates a new PLIST file for each target. I renamed mine to match the target name and moved them to the project folder.

NOTE: If you decide to do the same, remember to select your PLIST file in the general settings, so it can be associated with your target again.

Step 3: I clicked on each of the targets and changed the app display name to be unique for each target. I also updated my bundle identifiers. For "development" and "test", I added a ".dev" and ".test", respectively.

TIP: To allow the same app to be installed for each environment, you will need to change the bundle identifier, else iOS will see it as the same app.

Step 4: For each of my PLIST files, I added a property called "EnvVar" and gave it a unique value. I used a string "this is XXXX" to identify the environment.


Step 5: In my "ViewController.swift" file, I added the following method:

func getEnvValue() -> String? {
    return Bundle.main.object(forInfoDictionaryKey: "EnvVar") as? String

The method allowed me to pull the variable out of the PLIST file active for the specific target.

Now I could modify my viewDidLoad method like below:

    override func viewDidLoad() {
        // Do any additional setup after loading the view, typically from a nib.
        if let env = getEnvValue() {
            envLabel.text = env
        } else {
            envLabel.text = "SOMETHING WENT WRONG"

Step 6: I selected my development target at the top and ran the application again. I could now see my variable value in the below label.

TIP: If you don't see your renamed target names at the top, just restart XCode as the names seem to get cached.

Step 7: I did the same for each target and made sure that I saw my unique value.


3. Set up GitHub check-In

NOTE: I used GitHub for GIT version control, but you can use other hosted GIT solutions like Bitbucket if you prefer. As a reminder, here's the link to my GitHub repository.

These are the high-level steps that I followed:

  1. I created a "ci_cd_app" repository.
  2. I included a Swift ".gitignore" file.
  3. I created a "test" branch from "master".
  4. I created a "development" branch from "test".
  5. I pushed code to the "development" branch.

NOTE: I prefer a branching strategy where "master" always reflects what is deployed in production. I then branch for every other environment, for example: "master" → "test" → "development". This allows me to support multiple versions of the code, and allows bugs to be fixed, without moving any unwanted changes into the next environment.

4. Set up Apple certificates and provisioning profiles

In order to complete the CI/CD pipeline, as explained in the section below, I needed signing certificates and provisioning profiles. The detailed steps for doing that are outside the scope of this article, but you can learn more on Apple's website.

I obtained these from my Apple Developer account. The provisioning profiles needed to include the UDIDs of the devices that I intended to distribute and test the app with.

TIP: App Center has the ability to pull the UDID information automatically when your test users register via the invitation link they receive. This is a very neat feature and makes life much simpler when dealing with non-technical testers as an audience.

Setting up my CI/CD Pipeline

Microsoft App Center brings together multiple services, commonly used by mobile developers, into a single, integrated product. You can build, test, distribute, and monitor your mobile apps, and also implement push notifications.

I headed over to the website to create a free account, so that I could complete the steps below.

1. Creating a project

Step 1: I logged into App Center and added a new app. I called it "CI CD App".**

NOTE: Be sure to select "iOS" as the OS and "Objective-C / Swift" as the platform.


After adding the app, you will notice an overview screen that provides instructions for adding Microsoft Analytics and Crash reporting frameworks to your application. I prefer to use Google Firebase, but Microsoft's solution provides pretty decent information as well.

2. Creating my distribution groups

Step 1: In the navigation panel on the left, I clicked on "Distribute" and then selected "Groups".

Step 2: I added three new groups ("Development", "Test" and "Production") and added myself as a tester to each of the groups.


TIP: App Center also allows you to retrieve the UDID for your testers' devices if you need it for your provisioning profiles. After the tester completes the registration (on their mobile device), App Center will prompt them to add their device and automatically install the relevant profiles on the device to retrieve the UDID information.

Here is what it looks like in App Center when you want to retrieve the UDID information:


3. Configuring my builds (CI/CD)

Step 1: From the navigation menu on the left, I selected "Build". App Center will ask you to connect your source repository. In my case, I connected my GitHub account and completed the required authorization steps. Once completed, I selected my project and specified the following config in the Build app section:

  • Shared Scheme: "Development".
  • Build Frequency: "On every push".
  • I selected to automatically increment the build number using the build ID.


Step 2: In order to test my builds on a real device, I needed to add my signing certificate and provisioning profile. I added my device to the provisioning profile as previously discussed. I also made sure to select that I wanted the build distributed to my “Development” group.


TIP: I created a wildcard app ID because I changed the bundle identifier to include “dev” and “test” at the end, for those two targets.

Step 3: I clicked on “Save” and “Build”. This told App Center to start a new build process. Once the build had completed, I received an email with an installation link.


From my mobile device, I clicked on "Install" in the email. I then followed the steps to download and install the app.

NOTE: If everything worked as intended up to here, you should have the DEV version of the app on your device. You will know that it's the development version based on the name and the description in the bottom label of the app.

Step 4: I replicated the above configuration processes for both the "test" and "master" branches. However, I only saved them — I didn't run the build yet. That's because App Center would automatically build and distribute the app once I merged into those two branches.

Step 5: I create a pull request to merge the "development" branch into the "test" branch.

Step 6: I created a pull request to merge the "test" branch into the "master" branch.

NOTE: If everything worked, you should have three apps installed on your device, corresponding to the three different environments. Each app should display the message relevant to its respective environment at the bottom of the screen.


Further use cases

The above steps illustrated the concepts and tools that I used to create an automated mobile CI/CD pipeline. I simply changed the application name and displayed some text on the screen to showcase how XCode environments and CI/CD can be used to change application configuration at build time. This concept can be applied to API endpoints, where multiple environments have to be supported. It is also useful in those cases where you need to produce an app per environment for the testing team and allow them to easily distinguish between the versions.

The App Center distribution can also be extended to include the Google Play Store and Apple App Store distribution groups. This allows you to automate the release of new app versions directly to the relevant store in a fully automated fashion — even with release notes!


  1. Further reading on CI and CD
  2. Microsoft App Center
  3. My code repository for the steps in this article.

JJ Niemand loves making stuff work. He has 12+ years experience in tech and is currently focused on mobile, microservice and cloud-native development.


Cat eyes@2x

Subscribe to our blog

Don’t miss out on cool content. Every week we add new content to our blog, subscribe now.