iOS CI/CD with GitHub Actions
By: Jeremie Siffre
Tags: Github Actions, Github, Continuous Delivery, iOS, Continuous Integration

For many years, we had our iOS Continuous Integration system running on Jenkins with a unique MacOS slave as a physical machine.

It worked pretty well overall but we started to encounter scalability issues as the team grew. iOS developers complained about the CI being slow and we knew having a single Xcode instance available at a time could be an issue.

On August 2020, we were preparing our tooling migration to get ready for iOS 14 when we realized our physical MacOS machine was not matching minimum technical requirements to install the latest Xcode version.

We needed to find an alternative solution…

Here comes Github Actions into play.

In this article we will present 2 main use cases to show you how an iOS Continuous Integration could look like on GitHub Actions.

Prerequisites

At TheFork we mainly rely on Fastlane tools to implement the different steps of our CI/CD pipeline.

We have 3 main lanes declared in our Fastfile:

test : using fastlane scan

: using fastlane scan deploy_to_firebase : relying on gym, firebase_app_distribution

: relying on gym, firebase_app_distribution sync_certs: relying on match, to download the provisioning profiles and certificates in order to deploy the application

We also use Bundler to keep track of the gems versions we use ( including the Fastlane one ).

In the next sections we will consider we already have those lanes ready to be used.

Use case #1: Run tests on Pull Request

We started with the migration of our Pull Request job. Basically, whenever an iOS developer opens or modifies a pull request, we want to make sure no code regressions have been introduced, by running unit tests.

Let’s see how the workflow definition looks like:

name: Pull request on:

pull_request:

branches:

- develop jobs:

test:

runs-on: macos-10.15

timeout-minutes: 15

steps:

- name: Cancel previous jobs

uses: styfle/cancel-workflow-action@0.6.0

with:

access_token: ${{ github.token }}

- name: Git - Checkout

uses: actions/checkout@v2.3.4

with:

ref: ${{ github.ref }}

- name: Setup - Xcode

run: sudo xcode-select -s /Applications/Xcode_12.3.app

- name: Setup - Ruby and bundler dependencies

uses: ruby/setup-ruby@v1.57.0

with:

ruby-version: 2.4.0

bundler-cache: true

- name: Test - Fastlane tests

run: bundle exec fastlane test

We start with the trigger definition and launch this job on pull request events but only from the develop branch.

We continue with jobs listing and declare a single job named “test”. We specify the OS version we want (macOS 10.15) along with a timeout of 15 minutes.

This job is composed of 5 steps:

“Cancel previous jobs”: We start by cancelling previous jobs on the same branch using this action https://github.com/styfle/cancel-workflow-action

“Git — Checkout”: We checkout the branch content

“Setup — Xcode”: We select the Xcode version we want the tests to run on. See here for the full list of Xcode versions available on MacOS 10.15.

“Setup — Ruby and bundler dependencies”: We install ruby, download gems with bundle install and cache the result

“Test — Fastlane tests”: We lastly run our fastlane “test” lane and check for non regressions

Use case #2: Test builds on demand for Product Owners

At TheFork, we create a new Git branch for each feature increment we work on. In order to get our branch merged into the main project (i.e. develop branch), we need it to be reviewed by a minimum of 2 iOS peers but we also need to get approval from the Product Owner.

In order to get this “Product Owner” approval, we need to be able to provide him test builds matching the exact content of the Git branch we are currently working on.

What if we could have a user interface embedded in GitHub allowing us to select our branch and click on a button to generate testing builds on the fly.

Say hello to workflow_dispatch :-)

Workflow_dispatch is a manual trigger allowing us to specify “inputs” we can collect from users in a lightweight form automatically generated and available in the GitHub interface.

This trigger definition..

name: Launch Firebase builds ( Product Owner testing )

on:

workflow_dispatch:

inputs:

message:

description: 'Message to the PO to help him test the feature'

required: false

default: 'No message left by the developer to help you test :('

preprod:

description: 'Do you want to generate a preprod build? (true/false)'

required: false

default: 'false'

prod:

description: 'Do you want to generate a prod build? (true/false)'

required: false

default: 'false'

…will be rendered as:

..and will be available directly on the GitHub “actions” section, under the right workflow and after clicking on the right button “Run workflow”:

In our workflow we ask for 3 inputs:

“Message”: A message to add to the build descriptions on Firebase

2 parameters to determine whether or not we should generate builds connected to production and/or pre-production environments

We can then have access to those inputs parameters and check their values to know if we should run jobs or not:

jobs:

deploy_preprod_firebase:

if: ${{ github.event.inputs.preprod == 'true' }}

runs-on: macos-10.15

timeout-minutes: 45

steps:

- name: Git - Checkout

uses: actions/checkout@v2.3.4

with:

ref: ${{ github.event.ref }}

- name: Setup - Xcode

run: sudo xcode-select -s /Applications/Xcode_12.3.app

- name: Setup - Ruby and bundler dependencies

uses: ruby/setup-ruby@v1.57.0

with:

ruby-version: 2.4.0

bundler-cache: true

- name: Certificates - Sync ad-hoc

run: bundle exec fastlane sync_certs type:adhoc

- name: Firebase App Distribution - Archive and upload preprod app

run: bundle exec fastlane deploy_to_firebase env:preprod changelog:"${{ github.event.inputs.message }}" deploy_prod_firebase:

if: ${{ github.event.inputs.prod == 'true' }}

runs-on: macos-10.15

timeout-minutes: 45

steps:

- name: Git - Checkout

uses: actions/checkout@v2.3.4

with:

ref: ${{ github.event.ref }}

- name: Setup - Xcode

run: sudo xcode-select -s /Applications/Xcode_12.3.app

- name: Setup - Ruby and bundler dependencies

uses: ruby/setup-ruby@v1.57.0

with:

ruby-version: 2.4.0

bundler-cache: true

- name: Certificates - Sync ad-hoc

run: bundle exec fastlane sync_certs type:adhoc

- name: Firebase App Distribution - Archive and upload prod app

run: bundle exec fastlane deploy_to_firebase env:prod changelog:"${{ github.event.inputs.message }}"

Cherry on the cake, Github Actions jobs run by default in parallel. If we answer “yes” for prod and preprod builds generation we get them both available in ~30 min 🎉

Thank you for reading! We hope we gave you a great overview of how an iOS CI could be implemented on Github Actions :-)