When we develop iOS apps, we usually manage the app publication process manually using the Xcode Organizer. Then we sign, test, build, archive, submit, then change versions, and 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 many times per day. 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 run, test, and deploy our custom build.

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

For this project, 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
  2. A GitHub account.

To begin, we can simply create a new iOS Single View Application project from scratch. Xcode will set up Git automatically for you since it is checked by default. Otherwise, you can set up manually running git init.

You can also check this by going to the root project directory and writing git status in the terminal. You will see the following result:

$ git status

On branch master
nothing to commit, working tree clean

Build and run the app on the simulator. 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 iOS Swift app from Github.

Now, let’s push it using Semaphore.

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, 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.

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 project language in semaphore

You will see that Semaphore created a new file to be committed to our repository:

version: v1.0
name: Hello Semaphore
    type: a1-standard-4
    os_image: macos-mojave
  - name: Swift example
      - name: Run some code
          - echo 'print("Hello World")' > swifthello.swift
          - xcrun swift swifthello.swift
          # Uncomment the following line to pull your code,
          # then proceed by adding your custom commands:
          #- checkout

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


The first line version: v1.0 uses the latest stable version of Semaphore 2.0 YML syntax. By default, they are named Hello Semaphore. This is our pipeline name.


Next, we have an agent. 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.

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


Blocks define actions for your 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 example, our block is named Swift example and defines a task with some jobs. These jobs define some commands to run print "Hello World" and show it in a terminal. Below is a more descriptive explanation:

  1. echo 'print("Hello World")' > swifthello.swift prints Hello World in a terminal.
  2. xcrun swift swifthello.swift compiles and run the .swift file.
  3. checkout if uncommented, clones the Github repository.

To commit your file, tap the button Commit and Start Workflow. Also, pay attention to the fact that a new branch semaphore-setup will be created for you.

screenshot of iOS CI/CD tutorial

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

screenshot of iOS CI/CD tutorial

We have a working setup, and we’re ready to improve this for a real workflow that makes sense for iOS developers entering the Continuous Delivery stage. After running the code, everything shows a “Passed” message in green. You can go further and see the output of every step by tapping in your commit message or job name Run some code to understand everything that happened in the backend of the console while running the pipeline steps.

We can even have more than one block if we want. For example, a block to run all the steps to test our app, or to build, or to deploy it, this all depends on you and your pipeline strategy. So, now that we understand the file and how the integration process works, let’s improve our deployment process.

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 some-branch will trigger the automated process defined in the .semaphore.yml configuration file. This way we can just stay focused 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.

To do this, we will:

  1. Setup Fastlane to automate deployments and releases (Continuous Deployment).
  2. Configure a block to build and run our tests.
  3. Configure 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. Also, we will improve our previous Continuous Integration process a little bit.

So, we 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 usually 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.

I will highlight that to continue to the next steps, 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 have been 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” is 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.

We will use sem, a command-line tool to help us achieve this. If you have not installed yet, this is a good time to install it, see the sem reference and follow the instructions.

To store the deploy key as a secret file in the Semaphore environment:

$ sem create secret your-fastlane-ios-certificates-repo -f id_rsa_semaphoreci:/Users/semaphore/.keys/your-fastlane-ios-certificates-repo

You will see the following message on the terminal, Secret ‘fastlane-ios-certificates-repo’ created. This means that everything worked as expected.

The sem create command is used for creating new resources. This will create the file ~/.keys/your-fastlane-ios-certificates-repo in your Semaphore CI jobs.

Next, add the URL for the certificates repository and the encryption password as environment variables that will be accessible in Semaphore. I recommend also adding your App Store developer account’s credentials to the same secret.

$ sem create secret fastlane-env \
   -e MATCH_GIT_URL=“<your ssh git url>” \
   -e MATCH_PASSWORD=“<password for decryption>” \
   -e FASTLANE_USER=“<App Store developer’s Apple ID>” \
   -e FASTLANE_PASSWORD=“<App Store developer’s password>”

After running this command, you will find your secrets inside the Semaphore dashboard Secrets. You can confirm that we used a CLI tool for convenience and productivity, but you can also configure secrets and keys directly from your Semaphore dashboard.

screenshot of creating a secret in semaphore for iOS CI/CD tutorial

Updates your .semaphore/semaphore.yml file with the generated keys and secrets.

  - name: Fastlane build
      - chmod 0600 ~/.keys/*
      - ssh-add ~/.keys/*
      - bundle exec fastlane release
  - name: fastlane-env
  - name: fastlane-ios-certificates-repo

Now that we have set up Fastlane and our secrets, let’s make some changes in our previous Semaphore configuration.

Improving our pipeline

In your project, open .semaphore/semaphore.yml file. This is the configuration file to run your pipeline every time you push your code. The file is well commented, and you’ll need to change it accordingly to our own needs.

Change the key name: value to “Semaphore iOS Swift example with Fastlane”.

name: Semaphore iOS Swift example with Fastlane

Change the block to:

  - name: Run tests
      # Set environment variables that your project requires.
        - name: LANG
          value: en_US.UTF-8
          # Download source code from GitHub:
          - checkout
          # Restore dependencies from cache.
          - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH-,gems-master-
          - bundle install --path vendor/bundle
          - cache store gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock) vendor/bundle
        - name: Fastlane test
            # Select an Xcode version, for available versions
            - bundle exec xcversion select 10.3
            # Run tests of iOS and Mac app on a simulator or connected device
            - bundle exec fastlane test

Inside your configuration file, you can define another block. In fact, you can add however many blocks you need in your workflow. Just as you did before, define a set of steps inside your block to build and deploy apps with Fastlane.

We will name this new block Build app, and it will cache some libs and release your build with Fastlane. Since your code is running on a hosted machine, you’ll save local resources on your current machine so you can work without any concerns of hardware capacity.

Check out the complete file configuration here:

- name: Build app
        - name: LANG
          value: en_US.UTF-8
          - checkout
          - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH-,gems-master-
          - bundle install --path vendor/bundle
          - cache store gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock) vendor/bundle
        - name: Fastlane build
            - chmod 0600 ~/.keys/*
            - ssh-add ~/.keys/*
            - bundle exec xcversion select 10.3
            # Gym builds and packages iOS apps.
            - bundle exec fastlane release
        - name: fastlane-env
        - name: fastlane-ios-certificates-repo

Now, let’s learn about the new lines that we added inside the .semaphore.yml file:

  • cache restore gems: restores an archive which partially matches any given key. In case of a cache hit, the archive is retrieved and available at its original path in the job environment.
  • bundle install: installs all Fastlane dependencies.
  • cache store gems: archives a file or directory specified by path and associates it with a given key.
  • chmod 0600 ~/.keys/*: 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 ~/.keys/*: gives the path of the key file as an argument to ssh-add to add the generated keys.
  • bundle exec xcversion select 10.3: selects an Xcode version for available versions.
  • bundle exec fastlane test: runs the tests of your app on a Simulator or connected device.
  • bundle exec fastlane release: runs every step that is inside your Fastfile. So, in this case, it will build, archive, and deploy a new beta version to the TestFlight.
  • secrets: allows Semaphore to download certificates from a private certificates repository configured in Fastlane. We needed to create a deploy key and add it to Semaphore secrets, and we did this with sem cli tool.


Only one more step to go! Commit the changes to get the CI/CD workflow started:

$ git add -A
$ git commit -m "Ready to deploy"
$ git push

Commit and push your code again and check your Semaphore dashboard.

screenshot of pushing code in semaphore dashboard for iOS CI/CD tutorial

Voilá! 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.

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 manually delivering your app every time.

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