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:
- Continuous integration: build, test and archive the app.
- Continuous deployment: submit the build to Testflight.
We will start by:
- Setting up our repository in GitHub.
- Signing up for Semaphore CI.
- Linking our GitHub repository to Semaphore CI Project.
- Configuring our first CI/CD pipeline.
- 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:
- Git: for version control.
- A GitHub account to manage our repository.
- Xcode 11 with Swift 5.1 as our development tools.
- SwiftUI to build our user interface.
- 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.
Download: Semaphore 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
- Generate automated App Store screenshots.
- Upload the archived
At the end of this tutorial you will achieve a result like in the below image:
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.
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 email@example.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.
After you connected the repository to Semaphore, select your project’s main language. Choose “Swift” to continue with an iOS setup.
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
# 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 version, name and agent. An agent is a virtual machine that powers the pipeline. Let’s understand the above file, which defines our pipeline workflow.
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.
Next, we have an
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 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:
bundle exec xcversion select 11.2.1selects an Xcode version.
bundle exec fastlane testrun 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
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
- 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! ?
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:
- Configured Fastlane to automate deployments and releases (Continuous Deployment).
- Added a block to build and run our unit and UI tests.
- 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:
- Install Fastlane.
- Follow the instructions of configuring Fastlane files.
- 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.
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:
- Your new app configured on the Apple Dev Center.
- Your new app available in App Store Connect.
- 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.
To store the deploy key as a secret file in the Semaphore environment, click Create a new Secret button.
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.
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.
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/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.
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á!
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
- Generate automated App Store screenshots.
- Upload the archived
Open the finished project and explore the code on your own.
Download: Semaphore Demo iOS Swift Xcode
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.