DEV Community

Cover image for Tutorial: Build, Test, & Deploy an iOS App with CI/CD
Amarildo Lucas for Semaphore

Posted on • Updated on • Originally published at semaphoreci.com

Tutorial: Build, Test, & Deploy an iOS App with CI/CD

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. Sooner or later, you will ask yourself:

How can we automate this entire process?

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.

In this tutorial, we’ll learn how to automatically deploy our apps to the TestFlight using Semaphore as our CI/CD platform. When we push our code to our remote repository, Semaphore will test, build, deploy and generate screenshots of our app.

To make things simple, we’ve divided this tutorial into two parts:

We will start by:

  1. Setting up our repository in GitHub.
  2. Signing up for Semaphore CI.
  3. Linking our GitHub repository to Semaphore CI Project.
  4. Configuring our first CI/CD pipeline.
  5. Improving our CI/CD pipeline with Fastlane.

Continuous integration with Semaphore

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

We will use:

  1. Git: for version control.
  2. GitHub account to manage our repository.
  3. Xcode 11 with Swift 5.1 as our development tools.
  4. SwiftUI to build our user interface.
  5. XCTest to create and run unit tests and UI tests.

Note: SwiftUI previews and inspectors are only available when running on macOS Catalina 10.15.

You can download the project and follow along with this tutorial, or create a new iOS Single View Application project from scratch.

DownloadSemaphore Demo iOS Swift Xcode 

Xcode will set up Git automatically for you since it is checked by default. Otherwise, you can set up manually by running git init in the root project directory in the terminal.

You can also check your new git repository by writing git status in the terminal.

$ git status

On branch master
nothing to commit, working tree clean

Build and run the app on the simulator. If you have unit tests and UI tests in the project, run your test cases first in the Test navigator. If everything looks right, push the code to the GitHub repository. Nearly every new iOS project runs without problems the first time. If you run into a problem, you can always grab the Semaphore demo app from Github that we are using to following through this tutorial.

The Semaphore pipeline is configured to:

  • Run all unit and UI tests.
  • Build the app and generate an ipa archive.
  • Generate automated App Store screenshots.
  • Upload the archived ipa and screenshots.

At the end of this tutorial you will achieve a result like in the below image:

semaphore CI pipeline for iOS

Setting up Semaphore for an iOS project

Go to https://semaphoreci.com/login and log in with your GitHub account.

Click on + Projects to add the project.

screenshot of creating a new project in semaphore for iOS

Semaphore will list GitHub repositories. If you haven't pushed the code yet, you will need to push it so you can see the repository listed.

Follow the GitHub instructions after you created a new repository, which is the most important part:

$ git remote add origin git@github.com:amarildolucas/ios-semaphore-intro.git
$ git add -A
$ git commit -m "First commit"
$ git push -u origin master

This will push your code to GitHub. Since this is likely part of your day-to-day workflow, I won't go into too many details here. Remember, if you don't want to start a new project from scratch, you can just fork the Semaphore iOS demo app as recommended above.

Now, in Semaphore, connect the new repository. Refresh the list to see the repository, if needed.

screenshot of adding a repository from GitHub in Semaphore

After you connected the repository to Semaphore, select your project's main language. Choose "Swift" to continue with an iOS setup.

screenshot of choosing a starter workflow for Swift in Semaphore

You will see that Semaphore created a new file to be committed to our repository. If you chose the Semaphore demo app to follow through this tutorial, find the configured pipeline inside the root directory .semaphore/semaphore.yml..

# Use the latest stable version of Semaphore 2.0 YML syntax:
version: v1.0

# Name your pipeline. If you choose to connect multiple pipelines with
# promotions, the pipeline name will help you differentiate between
# them. For example, you might have a build phase and a delivery phase.
# For more information on promotions, see:
# https://docs.semaphoreci.com/article/67-deploying-with-promotions
name: Tallest Towers

# The agent defines the environment in which your CI runs. It is a combination
# of a machine type and an operating system image. For a project built with
# Xcode you must use one of the Apple machine types, coupled with a macOS image
# running either Xcode 10 or Xcode 11.
# See https://docs.semaphoreci.com/article/20-machine-types
# https://docs.semaphoreci.com/article/161-macos-mojave-xcode-10-image and
# https://docs.semaphoreci.com/article/162-macos-mojave-xcode-11-image
agent:
  machine:
    type: a1-standard-4
    os_image: macos-mojave-xcode11

# Blocks are the heart of a pipeline and are executed sequentially. Each block
# has a task that defines one or more parallel jobs. Jobs define commands that
# should be executed by the pipeline.
# See https://docs.semaphoreci.com/article/62-concepts
blocks:
  - name: Run tests
    task:
      # Set environment variables that your project requires.
      # See https://docs.semaphoreci.com/article/66-environment-variables-and-secrets
      env_vars:
        - name: LANG
          value: en_US.UTF-8
      prologue:
        commands:
          # Download source code from GitHub.
          - checkout

          # Restore dependencies from the cache. This command will not fail in
          # case of a cache miss. In case of a cache hit, bundle  install will
          # complete in about a second.
          # See https://docs.semaphoreci.com/article/68-caching-dependencies
          - cache restore
          - bundle install --path vendor/bundle
          - cache store
      jobs:
        - name: Test
          commands:
            # Select an Xcode version.
            # See https://docs.semaphoreci.com/article/161-macos-mojave-xcode-10-image and
            # https://docs.semaphoreci.com/article/162-macos-mojave-xcode-11-image
            - bundle exec xcversion select 11.2.1

            # Run tests of iOS and Mac app on a simulator or connected device.
            # See https://docs.fastlane.tools/actions/scan/
            - bundle exec fastlane test

  - name: Build app
    task:
      env_vars:
        - name: LANG
          value: en_US.UTF-8
      secrets:
        # Make the SSH key for the certificate repository and the MATCH_PASSWORD
        # environment variable available.
        # See https://docs.semaphoreci.com/article/109-using-private-dependencies
        - name: match-secrets
      prologue:
        commands:
          # Add the key for the match certificate repository to ssh
          # See https://docs.semaphoreci.com/article/109-using-private-dependencies
          - chmod 0600 ~/.ssh/*
          # Add the key to the ssh agent:
          - ssh-add ~/.ssh/*
          # Continue with checkout as normal
          - checkout
          - cache restore
          - bundle install --path vendor/bundle
          - cache store
      jobs:
        - name: Build
          commands:
            - bundle exec xcversion select 11.2.1
            - bundle exec fastlane build

            # Upload the IPA file as a job artifact.
            # See https://docs.semaphoreci.com/article/155-artifacts
            - artifact push job build/TallestTowers.ipa
  - name: Take screenshots
    task:
      env_vars:
        - name: LANG
          value: en_US.UTF-8
      prologue:
        commands:
          - checkout
          - cache restore
          - bundle install --path vendor/bundle
          - cache store
      jobs:
        - name: Screenshots
          commands:
            - bundle exec xcversion select 11.2.1
            - bundle exec fastlane screenshots

            # Upload the screenshots directory as a project artifact.
            # See https://docs.semaphoreci.com/article/155-artifacts
            - artifact push job screenshots

Every Semaphore pipeline starts with the versionname and agent. An agent is a virtual machine that powers the pipeline. Let's understand the above file, which defines our pipeline workflow.

Version

The first line version: v1.0 uses the latest stable version of Semaphore 2.0 YML syntax.

version: v1.0
name: Tallest Towers

By default, they are named with the language-specific template that you chose. This is our pipeline name.

Agent

Next, we have an agent.

agent:
  machine:
    type: a1-standard-4
    os_image: macos-mojave-xcode11

An agent defines the environment in which your code will run. There are multiple available machine types and operating system images. This means that on the Semaphore side, your code will be built in a hosted macos-mojave machine. This is a customized image with Xcode 11 preinstalled.

In our case, a1-standard-4 is our machine name with 4 virtual CPUs, 8 GB of memory and 50 GB of disk.

Blocks

Blocks define actions for the pipeline. They are executed sequentially and are probably the most important part of this file. Each block has a task that defines one or more jobs, and these jobs define the commands to execute. Once all jobs in a block are complete, the next block begins. Essentially, blocks define your Continuous Integration and Continuous Delivery pipeline flow.

In this file we have 3 blocks named ** Run tests**, Build app and ** Take screenshots**. All defining a task with some jobs. These jobs define some commands to run, for example bundle exec xcversion select 11.2.1 . Below is a more descriptive explanation for the job named Test inside Run tests block:

  1. bundle exec xcversion select 11.2.1  selects an Xcode version.
  2. bundle exec fastlane test run tests of iOS and Mac app on a simulator or connected device.

The first block run all our unit and UI tests.

- name: Run tests
  task:
    env_vars:
      - name: LANG
        value: en_US.UTF-8
    prologue:
      commands:
        - checkout
        - cache restore
        - bundle install --path vendor/bundle
        - cache store
    jobs:
      - name: Test
        commands:
          - bundle exec xcversion select 11.2.1
          - bundle exec fastlane test

The checkout command download source code from GitHub. The next commands, restore dependencies from the cache and install it. Finally, we select the Xcode version of the project and run the tests using Fastlane.

The next block build the app and generates an ipa archive.

- name: Build app
  task:
    env_vars:
      - name: LANG
        value: en_US.UTF-8
    secrets:
      - name: match-secrets
    prologue:
      commands:
        - chmod 0600 ~/.ssh/*
        - ssh-add ~/.ssh/*
        - checkout
        - cache restore
        - bundle install --path vendor/bundle
        - cache store
    jobs:
      - name: Build
        commands:
          - bundle exec xcversion select 11.2.1
          - bundle exec fastlane build
          - artifact push job build/TallestTowers.ipa

It is very similar to the previous block. The commands inside this block configure our secrets, sign our app and upload it to TestFlight using Fastlane. We will go more in-depth about the other commands and Fastlane later.

The last block capture and upload our app screenshots.

- name: Take screenshots
  task:
    env_vars:
      - name: LANG
        value: en_US.UTF-8
    prologue:
      commands:
        - checkout
        - cache restore
        - bundle install --path vendor/bundle
        - cache store
    jobs:
      - name: Screenshots
        commands:
          - bundle exec xcversion select 11.2.1
          - bundle exec fastlane screenshots
          - artifact push job screenshots

Notice that we repeat some commands like checkout, cache, etc, to get the necessary files from our repositories and dependencies.

To commit your file, tap the button Run this workflow. Also, pay attention to the fact that you can customize the default configuration before running by clicking on the Customize it first link.

Is everything green (Passed)? You did it! 🎉

screenshot of successful commit in semaphore

We have a working setup for a real workflow that makes it easy for iOS developers entering the Continuous Delivery stage. After running the code, everything shows a “Passed” message in green. Go further and see the output of every step by tapping in your commit message or job name in the Semaphore dashboard to understand everything that happened in the backend of the console while running the pipeline steps.

We can even have more blocks if we want. For example, a block to create changelog files of build versions or to send notifications on Slack, etc. This all depends on you and your pipeline strategy. So, now that you understand the file and how the integration process works, let’s understand our deployment process in depth.

Before doing it, let’s make a quick overview of Continuous Integration. In this process, we commit our changes to the main code branch that triggers an automated code to build and run. Every git push origin branch will trigger the automated process defined in the semaphore.yml configuration file. This way we focus on our work rather than manually build and test code every time we do changes.

Continuous Integration helps us archive a build that can be deployed and the automated tests check every tested code to be safe to deploy.

Continuous Deployment with Semaphore

Our code changes continuously while working on our apps. In practice, we deploy several versions of our apps, sometimes even on the same day. Rather, to fix bugs, add new features, refactoring some code, etc. It’s an endless cycle. Continuous Delivery is exactly this process. If we configure our project to trigger our defined pipeline automatically and fully automate the entire process of push code from our repository to production, the process is called Continuous Deployment.

We want to define our pipeline with steps to do Continuous Deployment, so we can focus only on our code while working in small iterations and always keep the code in a deployable state.

If you forked the Semaphore demo app you got all of this out of the box.

Make sure that you:

  1. Configured Fastlane to automate deployments and releases (Continuous Deployment).
  2. Added a block to build and run our unit and UI tests.
  3. Added a block with steps to build and deploy our app to TestFlight.

Fastlane makes our Continuous Deployment life a bit easier. You may know it as is vastly used for deployment of apps to the App Store.

If you don’t have Fastlane installed, you will need to:

  1. Install Fastlane.
  2. Follow the instructions of configuring Fastlane files.
  3. Test if everything is working right.

If you want a quick setup help, you can see the files that we use in the configuration of the Semaphore Demo iOS Swift app here. These files are added after we install and run Fastlane for the first time. To understand more about the process of Fastlane installation and configuration you should read this introductory article.

Run fastlane init in the terminal after installation and follow the instructions.

As we will automate our deployment of build distributions to TestFlight, you will need to have these steps finished:

  1. Your new app configured on the Apple Dev Center.
  2. Your new app available in App Store Connect.
  3. A successfully generated Fastlane configuration.

Also, as an iOS Developer, I think that is your day to day for every new app. So, we will not enter in details here.

This is everything we need to set up and deploy our first build with Continuous Deployment. In case you’re blocked in some steps, check the code of Semaphore Demo for iOS again to guide you, it is very specific and self-explanatory.

Note: Ensure that Xcode “automatically manage signing” it’s unchecked in Xcode when you work with Fastlane. And that your Signing (Release) contains the match AppStore provisioning profile selected if you had a Match file.

Configure your keys and secrets

Now that we have configured Fastlane, we must also provide a way for Semaphore CI to access the Git certificates repository and the Apple Developer portal.

Because we will access a private repository, we need to create a deploy key and add it to Semaphore CI secrets. This typically happens over SSH. If you are not familiar with SSH, this article walks you through the process to create, add and manage SSH keys to use in your pipeline.

Also, I recommend this GitHub article on generating a new SSH key.

$ cd ~/.ssh
$ ssh-keygen -t rsa -f id_rsa_semaphoreci

After generating the key, you will need to connect the SSH key to the project or user. Deploy your key to GitHub to grant access to your private repository.

Also, let’s store the deploy key as a secret file in the Semaphore CI environment.

Open the Semaphore Dashboard for your Organization. On the left sidebar find the Configuration section and click Secrets.

Configuration section of the Semaphore CI dashboard

To store the deploy key as a secret file in the Semaphore environment, click Create a new Secret button.

create new secret in Semaphore Ci 2.0

Name the secret as match-secrets than enter your environment variables name and value to match Semaphore and Fastlane configuration.

  • MATCH_GIT_URL: your SSH Git URL.
  • MATCH_PASSWORD: password for decryption of the match certificates.
  • FASTLANE_USER: App Store developer’s Apple ID.
  • FASTLANE_PASSWORD: App Store developer’s password.

Enter a destination file path /Users/semaphore/.ssh/match-secrets to upload the SSH key that you previously generated id_rsa_semaphoreci on your computer. Click Save Changes.

Entering the destination file in the iOS CI/CD tutorial

This created the file ~/.ssh/match-secrets with necessary environment variables and made it accessible in Semaphore CI jobs.

Now that you created your secret key, go to your Semaphore project page, click Edit workflow button, select block to which you want to connect secret, in your case is the Build app block. On the right sidebar, scroll to Secrets section, click the arrow button and check the secret that you previously created match-secrets. Click Run the workflow button to add the secret.

Running the workflow in Semaphore CI dashboard

After running this command, you will find your secrets inside the Semaphore Dashboard Secrets.

Our Build app block will look like below:

- name: Build app
  task:
    env_vars:
      - name: LANG
        value: en_US.UTF-8
    secrets:
      - name: match-secrets
    prologue:
      commands:
        - chmod 0600 ~/.ssh/*
        - ssh-add ~/.ssh/match-secrets

Let’s explain some commands that we repeated in our pipeline blocks:

  • cache restore: restores an archive that matches any given key. In case of a cache hit, retrieves the archive and make it available at its original path in the job environment.
  • bundle install: installs all Fastlane dependencies.
  • cache store: archives a file or directory specified by the path and associates it with a given key.
  • chmod 0600 ~/.ssh/*: reads and writes in files if it is the owner. It is all about permissions! You should be familiar with it in UNIX-like systems.
  • ssh-add ~/.ssh/match-secrets: gives the path of the key file as an argument to ssh-add to add the generated keys.
  • secrets: allows Semaphore to download certificates from a private repository configured with Fastlane. We needed to create a deploy key and add it to Semaphore secrets.

Deploy

After you do all your changes, commit it to get the CI/CD workflow started:

$ git add -A
$ git commit -m "Updates pipeline"
$ git push

Check your Semaphore dashboard and Voilá!

Screenshot of the final successful test in Semaphore iOS tutorial

Now, you need to continuously push code to your branches, and App Store Connect processes the build. It becomes available in TestFlight, and you will be able to select it for your new iOS app version.

Let’s do a quick recap of what we did in this tutorial. Our pipeline:

  • Run all unit and UI tests.
  • Build the app and generate an ipa archive.
  • Generate automated App Store screenshots.
  • Upload the archived ipa and screenshots.

Open the finished project and explore the code on your own.

DownloadSemaphore Demo iOS Swift Xcode 

Next steps

There is much more that you can do with automated deployments. For example, you can add more steps to your pipeline configuration, such as getting your CHANGELOG file, pushing messages to Slack, automating build number incrementation, dSYM uploads to Crashlytics, and more.

After you take some time to configure CI/CD for your iOS applications, you can focus on writing code instead of manually delivering your app every time.

Have questions about this tutorial? Want to show off your results? Reach out to us on Twitter @semaphoreci.

Top comments (1)

Collapse
 
onlinemsr profile image
Raja MSR

It’s a comprehensive guide that simplifies the process of automating app deployment to TestFlight.

Now, here’s my question:

  1. How to setup MATCH_USERNAME and MATCH_PASSWORD initially? Do we need to use the same password in CI tools?

  2. Why do we need a separate repo for certs and provisionig profile? What is the issue if have used the application code repo itself?