GitHub Actions for iOS SDK

David Schuppa
Emarsys Craftlab
Published in
8 min readMay 27, 2021

--

This article was co-authored by László Őri.

Github actions and Apple logo

The team

We are a four member team, using extreme programming methodology with its practices like collective ownership, pair programming, TDD development, continuous integration etc.
We use pipelines to run our test in various ways. Every pushed commit is going through a process where we run all of our tests and if everything looks good, it will result in a freshly built Sample application published.

The product

We’re developing the Android and iOS Emarsys SDK, which is the unified version of the former Mobile Engage and Predict SDK of Emarsys. This SDK provides an API for our customers mobile applications to be able to communicate with our backend in favour of enhancing user engagement and customer retention, by providing real-time personalised experience and product recommendations. More information about the product is available here.

The task

Our pipelines used to be on Bitrise, which worked well, however, our Android pipeline was very slow, since the tests of our several modules ran one by one, and although we could make them run in parallel with a tricky script, it was not the best solution. This is why we decided to move to Github Actions, where we could set up simultaneous jobs for the different tasks. Well, it worked like a charm.

After a few months of experimenting, we decided to migrate our iOS pipeline as well, so that way we can use the same tooling for both platforms.

At the end, we wanted to have 2 different pipelines:

  • one that is triggered by every pushed commit, runs all the tests on a simulator, builds a sample app when everything is good and finally notifies the team on Slack of the result.
  • and another, that is scheduled for every night to run all our tests on Firebase Test Lab on multiple real devices (one from each supported iOS version), builds a sample app when everything is good and notifies the team on Slack of the result.

The challenge

GitHub Actions is still rudimentary for iOS, there is a lack of actions on the marketplace, which could set up a provisioning profile or sets a secret file in a place, so we created our own action to create a file on an exact place from a base64 encoded GitHub secret. With this we solved both of the problems. We saw lots of problems during the creation of our pipeline, like installing multiple certificates, bumping build numbers during build, triggering another workflow, removing the old files from previous builds, even the checkout were quite challenging.

To make the pipeline easily readable and understandable, we decided to split it into three workflows.
One for testing triggered by commits, one for testing scheduled by the beginning of the day and one for build and deploy our sample app after successful testing.

Steps of the pipelines

Checkout

This was a simple step, we could use the checkout action to get the job done.

We used this action with the fetch-depth: 0 option, which will fetch all history from all branches. This will be important in the build step of the sample application.

- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0

Clean up previous workflows

It is not deterministic that the machine we are working on has leftover files from a previous run or not. So at the beginning step we clear the directory of the provisioning profiles and the directory that we are using for derived data. For this we use a simple bash command:

rm -rf ~/tmp | rm -rf ~/Library/MobileDevice

Install dependencies

We manage our project dependencies with CocoaPods and this step is done by a simple command:

pod install -no-repo-update -verbose

Setting up necessary files

For our project we need the following files to be accessible:

  • Provisioning profiles for every target
  • exportOptions.plist files

We store these files base64 encoded as a repository secret.

This simple command can be used to base64 encode and copy your file:

base64 /path/of/the/provisioningProfile.mobileprovision | pbcopy

To import files decoded to the desired location we are using the base64Secret-toFile-action like this:

- name: Setup Provisioning Profile
uses: davidSchuppa/base64Secret-toFile-action@v1
with:
destination-path: ~/Library/MobileDevice/Provisioning\ Profiles/
filename: TheNameOfTheProvisioningProfile.mobileprovision
secret: ${{ secrets.THE_NAME_OF_THE_SECRET_THAT_YOU_JUST_SET }}

Setting up code signing certificates

For installing certificates we used the import-codesign-certs action, which is creating a temporary keychain and installs the base64 encoded p12 cert with the password into it. The only tricky part here is that if you want to set up more than one certificate, you should use a dedicated keychain password for each step, or the cert won’t be installed. The keychain password should be the same for each import.

- name: Setup Dev Account Dist Cert
uses: apple-actions/import-codesign-certs@v1
with:
keychain-password: ${{ secrets.KEYCHAIN_PASS }}
p12-file-base64: ${{ secrets.DEV_ACCOUNT_DIST_CERT_P12_BASE64 }}
p12-password: ${{ secrets.DEV_ACCOUNT_DIST_CERT_PASS }}

Testing

We use separate workflows:

  • on every pushed commit we only run our tests on a simulator.
  • every evening we have a scheduled pipeline, which runs all our tests on Firebase Test Lab on real devices, one from every iOS version supported by the Emarsys SDK.

For the simple simulator testing we use a command which requires the xcworkspace files path, scheme, destination and derivedDataPath. Here is how we use it:

- name: Build And Test
run: xcodebuild -workspace ~/work/ios-emarsys-sdk/ios-emarsys-sdk/EmarsysSDK.xcworkspace -scheme Tests -configuration Debug -destination ‘platform=iOS Simulator,name=iPhone 12 Pro Max’ -derivedDataPath ~/tmp test
shell: bash

For the Firebase Test Lab testing we need more steps:

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@master
with:
project_id: ${{ secrets.GCP_PROJECT }}
service_account_key: ${{ secrets.FIREBASE_SERVICE_KEY_BASE64 }}
  • create a test build. The destination is different here, because we will run the tests on real devices and we also allowed provisioning updates:
xcodebuild -workspace ~/work/ios-emarsys-sdk/ios-emarsys-sdk/EmarsysSDK.xcworkspace -scheme Tests -configuration Debug -destination generic/platform=iOS build-for-testing -allowProvisioningUpdates -derivedDataPath ~/tmp
  • We need to zip it:
- name: Zip Test Files For Firebase
run: cd ~/tmp/Build/Products && zip -r ~/Tests.zip ./
shell: bash
  • After this we can start the tests. To run it, we have to pass the zip that we created, and the device matrix where we want to run the tests:
- name: Run tests on Firebase
run: gcloud firebase test ios run —quiet —test ~/Tests.zip — device model=iphonexr,version=13.2,locale=en_GB —device model=iphonexsmax,version=12.3,locale=en_GB —device model=iphone8,version=14.1,locale=en_GB
shell: bash

Triggering the build workflow

Since building the Sample application would be exactly the same in both testing workflows, to avoid duplication, we extracted the Sample application build to a different workflow, so when every test succeeds, we trigger a workflow with the repository-dispatch action.

This creates a repository dispatch event, in our case it is named build-sample, which we can listen to in another workflow:

- name: Trigger Sample App Build
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}
event-type: build-sample

Build workflow

The sample app build workflow is triggered by a build-sample dispatch event after tests have finished successfully:

name: Sample App Build
‘on’:
repository_dispatch:
types: [ build-sample ]
branches:
—master
workflow_dispatch:

Bumping build number for TestFlight deploys

To achieve the continuously increasing build numbers, we used the commit count for it. For this we have to modify our project’s info plist file with the following step.

However, for this to work it is very important to include a fetch-depth: 0 option in the checkout step, or the build number will always be 1.

- name: Set build number for EmarsysSample
run: /usr/libexec/PlistBuddy -c “Set :CFBundleVersion $(cd ~/work/ios-emarsys-sdk/ios-emarsys-sdk && git rev-list master — count)” ~/work/ios-emarsys-sdk/ios-emarsys-sdk/Emarsys\ Sample/Emarsys-Sample/Info.plist
shell: bash

Archive project

To be able to deploy the application, we have to create an archive for it. Very important to include clean in this command.

- name: Archive project
run: cd Emarsys\ Sample && xcodebuild -workspace Emarsys-Sample.xcworkspace -scheme Emarsys-Sample -destination generic/platform=iOS -configuration Release archive -archivePath ./Emarsys-Sample.xcarchive clean
shell: bash

Export ipa

Next we export the ipa from the xcarchive, that we just created in the previous step, for this the export options plist file is necessary.

- name: Export ipa
run: cd Emarsys\ Sample && xcodebuild -exportArchive -archivePath ./Emarsys-Sample.xcarchive -exportOptionsPlist ./exportOptions.plist -exportPath ./ -allowProvisioningUpdates
shell: bash

An example plist file:

<?xml version=”1.0" encoding=”UTF-8"?>
<!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” “http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version=”1.0">
<dict>
<key>distributionBundleIdentifier</key>
<string>BUNDLE ID</string>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>PROVISIONING PROF KEY</key>
<string>PROVISIONING PROF NAME</string>
</dict>
<key>signingCertificate</key>
<string>SIGNING CERT</string>
<key>teamID</key>
<string>TEAM ID</string>
<key>teamName</key>
<string>TEAM NAME</string>
</dict>
</plist>

Upload app to TestFlight

For this step we could use upload-testflight-build action, it makes the uploading pretty easy, but gathering the necessary id-s are not so straightforward.

You have to login to App Store Connect and go under Users and Access => Keys

Under this you can create the api key, and get the issuer-id, too.

- name: Upload app to TestFlight
uses: apple-actions/upload-testflight-build@v1
with:
app-path: ‘~/work/ios-emarsys-sdk/ios-emarsys-sdk/Emarsys Sample/Emarsys-Sample.ipa’
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }}

Notify on Slack

We included a failure step in every workflow to notify us on Slack, if the pipeline fails at any point.

For this we use slack_action, which we can tailor in many ways, we included custom icons, messages and even buttons to show in the slack message. This action needs a webhook environment variable, which needs to be set in the workflow file.

- if: ${{ failure() }}
name: Slack Notification On Error
uses: megamegax/slack_action@0.2.3
with:
actions: ‘[{ “type”: “button”, “text”: “View actions”, “url”: “https://github.com/emartech/ios-emarsys-sdk/actions" }]’
channel: ${{ secrets.SLACK_CHANNEL }}
job_status: <failure or success>
message: <message>
user_icon: <link to an awesome icon>
user_name: <username to display>

The build workflow contains this slack action with the success version, which will only be executed if every previous step succeeded.

Conclusion

Using GitHub Actions for iOS pipelines can be tricky in many ways. You need to have a better understanding on how things work in the background, especially, if you are working on an SDK and not on an average application. We strongly recommend you to be careful with the paths of the files, which are easy to mess up and could be really annoying and time consuming till you find the problem.

But with these steps, we think you will be able to create your own pipeline or fix it, if you are stuck anywhere.

--

--