Continuous Integration Using GitHub Actions for iOS Projects

Github Actions are finally publicly released! It's an opportunity to easily enable continuous integration in your projects on GitHub, so here's how you set it up for your iOS projects to perform automated code validation and testing on pull requests.

Written by 

This post will cover a simple approach to perform CI for iOS projects using the newly released Github Actions. This new CI tool is well-integrated into GitHub, making it the fastest option for effective CI in your GitHub projects. So without further ado, let me guide you through the setup.

Preparing Mint packages 🌱

As part of doing code validation, we'll need SwiftLint. My preferred way of setting up CLI tools such as SwiftLint is by using Mint. Mint is great for most CLI packages that you would normally download with Homebrew, as you can easily download and run a specific version of a package. This way, we can be sure that we're running the correct version in GitHub Actions. Add a Mintfile in the root of your project. It should contain one line:

realm/SwiftLint@0.38.0

That's it. We'll create a script later to install Mint itself from Github Actions.

Preparing RubyGems 💎

We'll be using Bundler so we have a Gemfile specifying the RubyGem dependencies that we need to do CI. If you don't have Bundler installed already on your Mac, do it by running gem install bundler. If you don't have permissions, you need to have rbenv installed, so you don't need to sudo it.

Navigate to the directory of your project and run bundle init. That will create a Gemfile for you. Here are the dependencies we need to install, so copy/paste this code into your newly created Gemfile:

source "https://rubygems.org"

gem "danger"
gem "danger-swiftlint"
gem "fastlane"
gem "xcode-install"

Now that we've specified the dependencies we need, install them by running bundle install. This also adds a Gemfile.lock to your project, specifying the versions of the dependencies that were installed.

Script to automate the installation of dependencies 📄

Now that we've specified our dependencies, we'll create a script for automating the installation. Create a file called bootstrap.sh in the root of your project containing the following script:

#!/bin/sh

echo "checking for homebrew updates";
brew update

function install_current {
  echo "trying to update $1"

  brew upgrade $1 || brew install $1 || true
  brew link $1
}

if [ -e "Mintfile" ]; then
  install_current mint
  mint bootstrap
fi

# Install gems if a Gemfile exists
if [ -e "Gemfile" ]; then
  echo "installing ruby gems";

  # install bundler gem for ruby dependency management
  gem install bundler --no-document || echo "failed to install bundle";
  bundle config set deployment 'true';
  bundle config path vendor/bundle;
  bundle install || echo "failed to install bundle";
fi

By running sh bootstrap.sh from the terminal, we'll have all dependencies ready - greatly simplifying our GitHub Actions setup.

Creating the workflow 

Now, we need to create the workflow to be triggered when creating a pull request towards the master branch. Follow these steps - and we'll go through the setup after you've had a chance to see it.

  • Go to your repo in GitHub and press Actions -> Set up a workflow yourself
  • Change the name to "ci.yml"
  • Paste the following code into the editor:
name: CI
on:
  pull_request:
    branches:
      - master

jobs:
  validate-and-test:
    runs-on: macOS-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v1

      - name: Cache RubyGems
        uses: actions/cache@v1
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: ${{ runner.os }}-gem-

      - name: Cache Mint packages
        uses: actions/cache@v1
        with:
          path: ${{ env.MINT_PATH }}
          key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
          restore-keys: ${{ runner.os }}-mint-

      - name: Install dependencies
        run: sh ./bootstrap.sh

      - name: Run code validation
        run: bundle exec danger
        env:
          DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}

      - name: Run tests
        run: bundle exec fastlane test
    env:
      MINT_PATH: ${{ github.workspace }}/mint

What you need to take away from this is that we have one job called "validate-and-test". It consists of several steps which are simple tasks run one after another. If one step passes, we proceed to the next step. In this setup, we have the following steps:

  1. Fetch cached dependencies, if any. This uses an official action called "cache". You can read about the concepts of actions here.
  2. Install all dependencies using the bootstrap.sh if no cache was found.
  3. Run code validation using Danger. This reads the access token provided as an environment variable.
  4. Run tests using fastlane. The command bundle exec fastlane test triggers a lane called "test" in our Fastfile that executes the tests.

if you're not familiar with the concepts of workflows, jobs, and steps, I recommend that you read about the workflow syntax from the GitHub Actions docs.

Code validation using Danger + SwiftLint ⚠️

Danger is one of the dependencies we specified in our Gemfile earlier. It's a great tool to improve our code reviews and simplify pull requests to our project. The Danger docs describe itself like this:

Danger runs during your CI process, and gives teams the chance to automate common code review chores.

These chores could be anything from formatting issues in the code to formalities such as not leaving a description in the pull request. You decide on the rules using the Dangerfile. What happens is that Danger will leave messages in your pull requests when you violate any of the rules.

We need to provide Danger with an access token. Go to GitHub's Personal Access Token in Settings and generate a new access token. You can name it "danger" if you like. Then you need to go to the secrets section under Settings in your GitHub project. Copy/paste the access token to a new secret called "DANGER_GITHUB_API_TOKEN". That will give Danger the access it needs.

We also specified "danger-swiftlint" in the Gemfile. It's a plugin for Danger that allows us to use SwiftLint in combination with Danger to perform linting and report back if there are any violations. Create a ".swiftlint.yml" file in the root of your project. You can copy/paste the linting rules that I normally use here. Lastly, make sure that you have these two lines in your Dangerfile:

swiftlint.config_file = '.swiftlint.yml'
swiftlint.lint_files inline_mode: true

That's the code validation! Now, onto the automated testing.

Testing using Fastlane scan 🛠

We are using Fastlane scan to run our tests. So we need to set up Fastlane by running fastlane init at the root of our project and select: "4. Manual setup - manually set up your project to automate your tasks". That will create a Fastfile inside the newly created fastlane folder.

Now, go ahead and run fastlane scan init in your project to create a Scanfile inside the fastlane folder. This is a configuration file with all the default parameters we want to set when we run the tests. It could look like this, but you want to check the docs to see what makes sense in your project:

devices(["iPhone 11 Pro"])
reset_simulator(true)
clean(true)

In your Fastfile, create one or more lanes where you'll run your tests. In my case, it looks like this:

default_platform(:ios)

before_all do
  xcversion(version: "11.3")
end

platform :ios do
  desc 'Runs the tests in Introspect'
  lane :test do
    scan(scheme: "Introspect")
  end
end

You should change the code to match the scheme in your project.

Results ✅

Now you should be all set with the testing. If you've come this far, you should be able to create a pull request with changes to your code and see the CI in action. If everything is successful, you should see a screen like this in the details about the run:

Code validation, build and testing in about 5 minutes is quite acceptable. In the project settings on GitHub, make sure to enable branch protection for the master branch to only accept pull requests - allowing only changes that pass code validation and testing.

Conclusion

That's it! A good CI setup can speed up your team's workflow significantly. It may even determine if your app makes or breaks it during development. You can find the source code for this setup on GitHub.

Also, check out this free course, How to Accelerate Your iOS Career and Double Your Income, that I just published this week. Thanks!

Share this post

Facebook
Twitter
LinkedIn
Reddit

You may also like

Process

Refactoring iOS Apps – A Pragmatic Guide

While your app and development teams grow larger, the codebase is inevitably going to scream for refactoring – that is, improvements in the design of the existing code. There are no silver bullets to refactoring, but iOS apps tend to get into the same pitfalls. Luckily, the common cases of refactoring can be mitigated by being in line with the following set of practices on improving and maintaining code while speeding up development.

DevOps

Architecting a Logging Service for iOS Apps

Understanding how your apps behave in production is a fundamental part of our jobs as iOS engineers. We need to gather log events in order to investigate and reproduce issues that customers run into. Here’s how you create a log service with a clean architecture that’s modular and easily extensible.

Comparison

Native App vs React Native App – What Should You Choose?

Gone are the days where the only options in mobile app development were native iOS and Android. The choices are broader nowadays, and frameworks have popped up with React Native being the most popular alternative. So what approach should you choose for your next app?