Mastering Github Actions for iOS
By: Przemek Jablonski
Tags: Mac, Github, Github Actions, iOS, Swift

Mastering Github Actions for iOS

Clean up your mess! Unsplash

This article is part of a series where we’ve created and deployed out first Swift Package and then we’ve married it with Github Actions CI.

I have spent last few weeks exploring Github’s new CI service, Github Actions. I managed to set up my workflows for both my iOS apps, macOS apps and multiplatform Swift Packages. I have to admit, I really like it! It’s quick, versatile, cheap and just really fun to work with 🐙.

However, I feel like Github Actions are not really designed for the iOS audience — you start to see your workflows become more bloated and messy quite early on as you expand their capabilities.

But does it really have to be this way? No! After extensive docs study and experimentation I can teach you some cool tips and tricks to master your CI workflows. Let’s make your jobs supercharged with features and more elegant at the same time! 💪

Use matrix for job parametrisation

In previous articles, we have established that we would like to perform CI workflows against variety of platforms, OS versions, macOS images and Xcode versions. It would be great if we could have one parametrised job template to cover all of those, instead of copy-pasting the same one over and over again, applying minor changes to it each time, right?

Enter the matrix. Matrix is a nice and elegant way to define a group of parameters, which can be then used inside of your job steps. For example, if matrix contains a list of schemes to build as a parameter, then those values are usable in your xcodebuild steps later down the pipeline.

And not only that — if you group mutliple parameters in a matrix, then each time CI is triggered, it creates multiple instances of the same job, instead of just single one. The idea is to create as many job instances with parameters switched all around so that all of the parameter combinations are fulfilled. Let the examples do the explanation:

Straightforward matrix example

Below you can find code snippet of adding matrix to the build-and-test job, containing just one xcodebuild step. This matrix consists of three schemes and two xcodebuild commands to perform.

Matrix containing schemes and xcodebuild commands

So now, each time CI is triggered, Github Actions will create 6 jobs instead of one— each serving one combination of parameters. There will be job for building Core scheme, another for testing Core scheme, then another for building UI , another for testing UI and so on.

Each parameter group as a separate axis, totalling to 6 unique jobs

Full multiplatform matrix example

Now that you know matrix works, I would like to share with you my opus magnum of Github Actions as of now. This gist below is a curated snippet of fully parametrised, no-maintenance, matrix-based job I came up with, which uses xcodebuild to build and test my multiplatform Swift Packages. No flimsy destination strings, iPhone names, mac IDs, or whatever — just Actions stressing my Package out against ridiculous amounts of variables. 🐙

This example uses matrix consisting of multiple variables worth testing on, such as different xcode versions, macos environment versions, scheme s to build, platform s to test against etc.

This is so cool that you can probably copy-paste this one to your repo with little changes

You can find full CI config in the Earendil repo we’ve been talking about before here. I have similar config for my another Package, Casimir, which you can check out here.

Make Open-Source Actions work for you

As you may have noticed from the examples above, those contain some prebuilt Actions from Github Marketplace (mainly setup-xcode and xcodebuild ). Those are free, open-source scripts created by the ever-growing Github Actions community. At first, browsing the Marketplace and eyeing out iOS stuff seemed to me a bit redundant — it felt like I would give up some of the control of my build process, so I tried to configure every nook-and-cranny on my own.

However, now I would highly recommend taking advantage of them though. I’ve understood that the best CIs are those which are always reliable and 100% maintenance-free — if third-party solutions get you closer to this ideal, then it’s just unwise to not use them!

I mean, those are built by people smarter than you anyway 🤭

Xcode switcher

One of the Actions I tend to use often is maxim-lobanov/setup-xcode . This seems to be a bit silly, since we all know how easy switching to given Xcode actually is ( sudo xcode-select -s PATH_TO_XCODE.app ). But remember when we’ve talked about perfect CI being maintenance-free? You, as a dev, should only care about which Xcode version to run your job on — let the open-source community take care of the mundane and fragile stuff, like figuring out the path to the Xcode.app for your builds. One less thing to worry about!

Turbocharged xcodebuild command

Another one, which is frankly quite insane, is custom mxcl/xcodebuild . This Action redefines the inputs for your standard xcodebuild , making them readable faster and also expands the command’s base capabilities overall.

Moreover, as you can read in the docs, this Action not only is made to be continuously resilient (never failing), but also houses tons of fail-safe mechanisms, so that below usage will always trigger code testing properly:

Even though, if you think about it, it doesn’t have all of the necessary data to do so — destination, scheme, platform and others are all inferred for your convenience. It’s even smart enough to replace test command with build for watchOS builds under Xcode <12.5. And that’s just one example. Haven’t thought of that yourself, did you? 🧐

Conditionally excluding matrix job combinations

You should be wary of certain edge cases when designing your job parametrisation matrix. If your matrix is anything like mine, then you are probably parametrising xcode and macOS versions to use in your jobs, like so:

xcode: ['13.0', '12.5.1', '12.4', '12.0.1']

macos: ['macos-11', 'macos-10.15']

This looks good at a first glance, but if you think about it, at least one of the jobs are destined to fail — there’s no xcode '13.0' available on Catalina and no xcode '12.0.1' available on Big Sur environment and so on.

The solution is the keyword exclude available in Github Actions syntax. Using it you can escape non-favourable job scenarios created by the matrix parameters. You can also go the other way around with the keyword include .

If any job matches the parameter pair in the ‘exclude’ section, then they won’t be created at all

Trigger builds manually from the web

This is a pet-peeve of mine actually — how many times you had to create dummy commits in your PRs so that the CI jobs would trigger (especially when you are setting up the workflow for the first time)? You can actually create a button in your repos web interface, which will trigger the CI whenever you need to do so. No more clumsy commit history!

Cool thing is that it’s done right in the config file as below, with the keyword workflow_dispatch . No web code needed. You can even add additional input parameters to be available, but a plain button with branch selector will do too.

Enabling triggerring button in the web interface

Diagnostic ideas before building and testing

I feel like I have spent too much of my professional career figuring out “why this CI build failed?” and creating makeshift diagnostic tools to help me debug the issues on the fly. This is why I’d recommend coming out with as many diagnostic steps as possible, to be fired before you actually build, test or release anything, each time. Better to have a think about it now, then to frantically try to think of something when your jobs actually start to fail miserably. Right?

Have a go at my little cheatsheet below and feel free to incorporate some of those ideas to your own Github Actions jobs.

Possible diagnostic lines for future debugging

Hope that was helpful! Share your favourite tricks for Github Actions below!