14.
Continuous Integration
Written by Pietro Rea & Keegan Rush
Automation makes a developer’s life so much easier. One benefit of automation is that it removes some of the capacity for human errors. When your build script records all the steps for distribution, you’re much less likely to archive the app on the wrong Git branch or with the wrong build configuration.
So far, the automation you’ve explored is great for your own personal use. But working as a team, while filled with benefits, comes with its own set of problems.
In a team, coordinating work between developers can be as much work as the programming itself. Combining, or integrating, the work of each developer is a challenge.
Continuous integration (CI) is the development practice of integrating the team’s work early and often.
Rather than waiting weeks or months for the team to complete large chunks of work, CI makes quick work of team coordination by integrating all code in one central location. By frequently running unit tests to ensure that integration is successful, CI keeps the team clued in to the state of the app’s codebase.
Note: This chapter assumes some basic knowledge of Git and the command line, as well as your own GitHub account.
More CI benefits
Effortless integration is the first and foremost benefit of CI, but the advantages don’t end there. Here are some other important benefits.
Detecting problems early
Often, when someone refers to continuous integration, they’re referring to CI services. Developers implement continuous integration through the use of a CI service that provides a build server. While you can roll your continuous integration solution, it’s much easier with a service.
Rather than running automated tests on your computer when you feel like it, continuous integration means running the tests on a build server every time the code changes. If a developer pushes code to GitHub that causes the tests to fail, also known as breaking the build, your CI lets you know immediately.
Using continuous integration, you can automatically run your build script every time you finish a feature or commit code to GitHub.
If you trigger your CI service to run tests on every new commit, you never need to backtrack to find the cause when something goes wrong. If all the tests pass on one commit but fail on the next, you know exactly where to look to fix the broken build.
It’s great that your tests are running early and often, but another benefit of CI is where your tests are running.
Providing a single source of truth
CI services leave your personal computer out of the equation. All you need to do is push your latest code to GitHub, and CI will build your app and run your tests in the cloud.
Not only can you step out for a coffee while your app is building, but with CI, you can pack up your laptop and take it with you. This is a big deal because a developer’s computer isn’t always the most stable.
Locally, you may be running a beta version of macOS and Xcode, or the latest and greatest fastlane build. This can be a problem, however, when it comes time to run your release build. An app built on one version of Xcode might show different behavior when a different Xcode version builds it.
That’s why your CI server acts as your single source of truth. Continuous integration provides you with a server that’s perfectly configured to build your app. The versions of macOS, Xcode and any other dependencies stay consistent.
If your computer has problems building the app, take a look at the state of the build on the CI server. If the build is broken, then you can track down which commit broke the build to fix things. But, if the build passes — meaning the app compiles successfully and all tests pass — then the problem probably lies with your computer.
A CI server makes it possible to merge your latest work into the main branch often. You have one central place that tells you if the integration succeeded or failed.
So, you know why you might want to set up CI for your app, but how does it work? To understand the process, you’ll learn about the components that embody this development practice.
CI components
Continuous integration works through the combination of a build server, jobs and triggers.
The build server is the central location where CI does its work. It prepares release builds or runs tests to report on the state of the app.
A job is any operation that you want to perform on the build server. Running your build script on the CI server is an example of a job. You could have one job to run unit tests, another to upload a build to App Store Connect and another to email you with the result.
The build server won’t do anything until a trigger tells it to kick into gear. When a trigger fires, the CI server will execute one or more jobs.
When you’re working on your computer, you run an automation script by calling it in the terminal. With CI, you don’t run anything yourself. Instead, you set up triggers that run your jobs when certain events happen. For example, you can create a trigger that runs on the creation of a pull request, when pushing code to GitHub or even at a certain time of day.
In the example above, pushing code to GitHub is an event that executes a trigger on your continuous integration server. That trigger initiates a job, which could be running your tests, creating a new release build, sending a Slack message or anything in between.
Luckily, you don’t have to write the code to implement jobs and triggers. There are plenty of CI providers that handle that for you.
The mechanics of CI stays consistent between jobs and triggers. Depending on your needs, however, there are different types of build servers to choose from when picking a CI provider for your project.
Different types of CI
Roughly speaking, CI providers come in three different flavors: full service, managed and manual.
Full-service CI
This is the most convenient option of the three. With a full-service CI provider, you get a simple point-and-click interface. The interface guides you through setting up your build to deploying it to TestFlight or elsewhere.
Full-service CIs are easy to use, but they aren’t very customizable. If you have complicated requirements, they might not fit your needs.
Two examples of full-service providers are Bitrise: https://www.bitrise.io/ and Microsoft’s App Center: https://appcenter.ms/.
Managed CI
Managed CI straddles the line between guided point-and-click options and full customization. A managed CI service handles the hardware for you in the cloud. All you need to do is provide a build script, usually using fastlane or xcodebuild.
Managed CI is a good option if you have a solid understanding of deploying apps, because you’ll have to do the work of building and signing the app yourself.
Two popular examples are CircleCI: https://circleci.com/ and GitHub Actions: https://Github.com/features/actions.
Manual CI
These services provide the most opportunity for customization. In addition to handling app building and deployment, you’ll also need to manage your own build server.
Manual CI services are the cheapest of the options, but managing a build server can be a full-time job in itself. For this reason, manual CI tends to be the domain of larger companies that demand full control over the build and distribution of their apps.
The most popular option for manual CI is Travis CI: https://travis-ci.org/. For iOS, you also have the option of Xcode Server.
Implementing your first CI
Now that you know what continuous integration does and how it works, it’s time to jump in by implementing CI in the Emitron project.
As you’ve seen, there are numerous CI providers to choose from. In this chapter, you’ll work with GitHub Actions, a managed CI system. You’ll create a repository for Emitron on GitHub and connect it to GitHub Actions to implement your first CI pipeline.
Creating a GitHub repository
Any project that uses CI needs a version control system such as Git. Without it, there’s no easy way for your CI server to integrate everyone’s changes.
To get started with CI, you’ll first create a Git repository and upload the Emitron app to GitHub.
In your browser, open https://github.com/. If you’re not signed in to your GitHub account, sign in now.
In the left sidebar, click the New button to create a new repository.
Set the repository name to emitron. Mark the repository as private, since it’s just for you.
Next, click Create repository.
You’ll need the Emitron repository page later, so keep it open for now.
At the moment, you have an empty repository. You’ll add Emitron’s project code to it next.
Uploading Emitron
To upload Emitron’s project code to your empty repository, you need to initialize Git within Emitron’s project folder on your machine.
Open a terminal window. Then, cd to the Emitron folder within the starter project.
Once there, run the following command in the terminal:
git init
This initializes Git within the Emitron project.
Next, run this command:
git add . && git commit -m "Initial commit"
See below:
You’ve now added the entire Emitron folder to your new Git repository and committed it. With that, you’ve set up your repository locally.
To upload it to GitHub, you need to point your local repository to the one you created on GitHub. Back in your browser, you should still have the page open for your empty repository. There, you’ll find the repository’s URL:
Copy your repository’s URL. You’ll need it to upload your local repository to GitHub.
Open your terminal window again. Run this command, replacing my repository’s URL with your own:
git remote add origin \
https://github.com/TheCodedSelf/emitron.git
Note: To make things easier to read, the backslash at the end of the first line splits the command you entered into two lines.
Before uploading the project to GitHub, you have one final step to complete. Git repositories have a history of setting the name of the main branch to master. However, GitHub uses the more inclusive main as its primary branch name.
Run this command to update your branch:
git branch -M main
This updates your branch name from master to main to match GitHub’s conventions.
Finally, you’re ready to upload Emitron. Run this last command:
git push -u origin main
With that, you’ve uploaded Emitron to GitHub.
Back in your browser, refresh the repository page:
There it is, all your project code is on GitHub and ready for continuous integration!
Setting up GitHub Actions
Since your project is already on GitHub, GitHub Actions is a convenient solution for CI. Since you already have a GitHub account, you won’t need to sign up for any other third-party accounts.
To use GitHub Actions for CI, all you need is a workflow folder in your repository with one or more workflows.
In GitHub Actions, a workflow is a list of jobs that should run, as well as a trigger that executes them. You define workflows as .yml files in a .github/workflows directory in your repository.
In your terminal, create the workflows folder for GitHub Actions.
mkdir .github && mkdir .github/workflows
Note: Your terminal’s active folder should be the same as the root folder for your GitHub repository.
GitHub uses the .github folder for any project configuration, such as your workflows. This command creates the .github folder and then a workflows folder within it.
Creating a workflow
Next, run this to create your first workflow file:
touch .github/workflows/run-tests.yml
This creates an empty workflow named run-tests.yml.
To see hidden files in Finder, press Command-Shift-. (i.e., Command-Shift-Period).
Open run-tests.yml in your favorite text editor. Replace its contents with the following:
# 1
name: Run tests
# 2
on:
push
# 3
jobs:
build:
runs-on: macos-latest
# 4
steps:
# 5
- uses: actions/checkout@v2
# 6
- name: Install fastlane
run: |
bundle install
# 7
- name: Execute fastlane
run: |
bundle exec fastlane test
Your first workflow will instruct GitHub Actions to build Emitron and run its set of automated tests using fastlane. Here’s what’s happening, step by step:
- Set the name of your workflow to Run tests. You’ll see this name in GitHub when your workflow runs.
- Use the
onkeyword to specify your trigger. This workflow runs when a new commit is pushed to your repository. - Under
jobs, you declare all the jobs that should run when triggering this workflow. Your new workflow has only one job namedbuild. - Each
jobhas a list ofsteps. - The first step is an action that GitHub provides. Actions are reusable pieces of code that you can use in your jobs. Here, you use the
checkoutaction to check out your repository on the build server. Your repository isn’t even on the build server until you usecheckout! - When you want to do something that isn’t available in
action, you define a custom step with the name and run keywords.nameis the name of your step andrunis the command that the build server will run when the step executes. This step uses Bundler to install dependencies and make fastlane available on the build server. - Lastly, you have the meat of your workflow. The final step uses fastlane to run Emitron’s
testlane. This will run Emitron’s test suite. If all tests succeed, your workflow will end successfully, and you’ll have a green build. If not, you’ll up with a red (failed) build.
You’ll notice that the workflow starts with the basics: checking out the repository and installing dependencies. This happens every time your workflow executes.
Transient builds
Usually, CI servers are transient. Whenever GitHub Actions runs one of your workflows, the build server starts in a clean state. Each time a workflow runs, you need to fetch the repository with checkout and install any dependencies you need, such as fastlane.
This might seem like an extra bit of work, but it’s a good thing. The reason that CI helps you to ensure a consistent build is because you must be explicit about what your workflow needs to run successfully.
With your new workflow file, you’ve done everything you need to set up GitHub Actions. However, everything you’ve done so far only exists locally on your computer. To see CI in action, you need to push your changes to GitHub.
Running the workflow
In your terminal window, run this command:
git add .github/workflows/run-tests.yml
This prepares your workflow so you can commit it. Next, commit the file:
git commit -m "Add first workflow"
Finally, you’re ready to upload your changes. Run this:
git push -u origin main
See below:
With that, you’ve uploaded your new workflow to the main branch in your GitHub repository. The workflow is set to trigger when you push a new commit, so you should have a build running in GitHub Actions right now.
Viewing the results
Back in your browser, navigate to your GitHub repository.
Click the Actions tab.
Here, you see an entry for the Run tests workflow executing the Add first workflow commit. On this screen, you’ll see all the different workflows that you or other developers run for any commits in the repository.
Click Add first workflow to open the running build. Give it some time to finish successfully:
Great job!
On this screen, you’ll see the results of all the jobs in your workflow. Run tests has only one job, build, so that’s all you see here.
In the left sidebar, under Jobs, click build.
The job summary tells you how long each step took to execute. You can also view the logs for each step — something that comes in handy if you’re trying to figure out why a build failed.
Click Execute fastlane to view the logs. Scroll almost all the way to the bottom, somewhere around line 860. Here, you’ll see the results that fastlane prints to the console after running Emitron’s tests:
You’ve successfully set up your first build on CI. With this, any developers can see if the tests are successful whenever they push code.
If the workflow fails, either because Emitron doesn’t compile or because a test fails, treat it as a warning sign. CI builds should be green before integrating different branches. That way, it’s much more likely that your main branch will build and work as you expect.
Key points
- CI helps you easily integrate the work of many developers.
- By paying attention to public build status, you can catch failing tests early, before they become a problem.
- Your CI server becomes a single source of truth that you can rely on if things aren’t working locally.
- A workflow is the combination of a trigger with one or more jobs.
- CI services can be full service, managed or manual.
Where to go from here?
In this chapter, you explored the benefits and practices behind continuous integration. You also created your first CI workflow, which runs Emitron’s tests and reports back on the state of the build.
Continuous integration is all about making the development process easier when working as a team. A workflow that simply runs your tests and publishes a build status for the team is enough to achieve smoother integration.
The lessons you’ve learned in this chapter, by implementing Emitron’s Run tests workflow, are the building blocks towards something even bigger: continuous delivery.
Your builds on your CI server don’t need to stop after running tests. Continuous delivery (CD) takes a successful build, archives it and publishes it to TestFlight or elsewhere.
Thanks to code signing, iOS apps come with unique challenges when trying to publish an app via a CI workflow. With all the lessons you’ve learned so far, you’re prepared to take on the challenge of continuous delivery!
In the next chapter, you’ll look at some case studies of how successful companies use App Store Connect, code signing, automation and CI/CD to publish apps in the real world.