Unified Modules Template: Our CI Journey For PRs

Scott Laing
FanDuel Life
Published in
7 min readNov 3, 2022

With so many options to choose from when selecting a CI tool for a new project, how should we go about making a selection? Here’s how we at FanDuel went about it.

The Problem

We recently began developing a framework to enable teams at FanDuel to take ownership of their front-end user flows, through SDKs, which reduces friction in scalability and enables more freedom of movement between teams. As part of the development of this framework, we’re producing a template which teams can use to bootstrap their SDKs, including PR and release pipelines.

Having used various CI tools over the years we had one question that needed to be answered: which tool should we choose for the template that will be used by many front-end teams across the organisation?

Buildkite, Jenkins, Azure Dev Ops, and GitHub Actions, have all been used at some point by various teams, and we needed to choose just one. Even though the team’s recent experience was split between both Azure and GitHub for CI (Android and iOS respectively) which includes SDKs currently owned by this team, we could see the attraction of trying out GitHub Actions for both platforms in the interest of consolidation, at least. After all, we use GitHub code and artefact repositories already for existing projects, why not consolidate the CI into that? As a GitHub Enterprise customer, we essentially get to try it at no extra cost as it includes 50,000 CI/CD minutes each month.

Simpler is Always Better

That’s one of FanDuel’s company principles and, with the project being greenfield, it seemed like a great time to experiment a little with the goal of keeping things simple by keeping the code, artefacts, and the CI all in GitHub. After all, it’s better to try it and fail fast so we know why not to go down that path.

The Requirements

We ended up creating actions that automate our PR and Release pipelines, but we’ll focus on the PR jobs for this article. We needed the PR action to support the following requirements:

  1. Run separate jobs for each of the 3 platforms — Android, iOS, and web
  2. Run unit tests for a given platform when changes are detected in the platforms’ directory
  3. Sanity check that we’ve made appropriate changes to any version codes
  4. Store secret keys, to allow iOS pipelines to read any privately published dependency pods (and to publish the artefacts in a future release pipeline)

We opted to organise the pipelines into a file per platform and so we ended up with 3 YAML files for the PRs:

.github
/workflows
android-unit-tests.yaml
ios-unit-tests.yaml
web-unit-tests.yaml

Running jobs for each platform

The repository will contain 3 platforms: Android, iOS, and web. We had no concerns about whether we’d be able to create pipelines for Android and web, but iOS requires a macOS image to run on. This was the first thing we needed to confirm as we wouldn’t be able to proceed without it. As with most CI tools, we have the option to do just that with GitHub Actions and it’s very straightforward:

jobs:
tests:
name: Run Unit Tests
runs-on: macos-11

Triggering jobs to run only when the relevant files have changed

It’s expected that a PR may not update all three platforms and we don’t want to run jobs unnecessarily for platforms that are untouched. So our next problem was to find out if we can skip a job based on files changed.

In our case, the project structure has 3 platform directories at the root, /android, /ios, and /web, and in GitHub Actions, we have the option to define triggers for changes to only certain paths. This gave us another easy win by targeting those platform directories, e.g., in ios-unit-tests.yaml:

on:
pull_request:
paths:
- ios/**

Checking that the right version codes have been bumped

The library artefacts for each platform have a version which we agreed as a team would follow Semantic Versioning with an added pre-release suffix, e.g. 2.4.1-alpha.3. As part of the PR pipeline, we wanted to be able to validate that the version numbers were altered correctly each time (let’s face it, it’s easy to forget to bump a pre-release version!). This requirement appears to be less common than the first two and it’s quite specific to the way we work and so we opted to write our own script for this rather than try to find available Action on the GitHub marketplace.

Rather than have complex logic to validate when to bump major vs. minor we decided to stick to our principle of “Simpler is Always Better”. We opted for checking only that the version number is higher than previously. We also have two long-lasting branches which have their own rule, main as the main development branch requires that all versions include the pre-release suffix and release which needs to never include the pre-release suffix.

There are a few steps to what we wanted to achieve here:

Get the current and new version numbers. For this, we needed to get each from some file that’s inside the repository as it is on two separate branches, the current working branch and the PR target branch. We were able to read from these files by targeting the correct branch using the github context¹, which has the properties base_ref and head_ref to get the target and source branches, like so:

git show origin/${{ github.base_ref }}:ProjectName.podspec

Determine the maximum version number. Once we had the two version names, we needed to determine which was higher. To do this, we wrote each version onto its own line in a text file, sorted the lines numerically, and then read the value from the last line (the sorted maximum).

echo $version_old > versions.tmp
echo $version_new >> versions.tmp
version_max=$(sort -n versions.tmp | tail -1)

Run the validation. We were now at the stage of creating the validation logic. We started by confirming if the target branch is main the -alpha.n suffix should be present.

if [ "${{ github.base_ref }}" = "main" ] && [ $(echo $version_new | grep -ce '-alpha.') -le 0 ] ; then
echo "Your VersionCode must contain -alpha."
exit 101

The next check ensures the new version is the maximum version number.

elif [ "$version_max" != "$version_new" ]; then
echo "Your VersionCode is smaller than origin/${{ github.base_ref }}"
exit 102

And finally, we wanted to cover our mistakes when we forget to update the version number at all 😅

elif [ "$version_new" = "$version_old" ]; then
echo "Your VersionCode must be incremented, please update your PR"
exit 103
fi

At this point, we realised that our method for determining which version number is the maximum isn’t perfect, as it doesn’t correctly identify mixes of release and pre-release versions correctly. For us, though, this was not a blocker as we intend to use a dedicated release branch, which means we’ll only ever be comparing release against release and main against main. This meant that we were able to disable the job for that initial release, but we’d still like to solve this though so that initial releases of future projects are not affected.

Storing Secret Keys

In order to allow the iOS PR pipeline to pull in privately published dependency pods (as well as to publish the artefacts in a future release pipeline), we needed to be able to store secret keys somewhere on the CI tool which can be consumed by a pipeline script. Here we encountered an issue having chosen GitHub Actions.

In Azure Pipelines, we used a single project for running the pipelines of multiple SDKs, which meant that we could store a token once and then any new SDK pipeline had access to that token. In GitHub Actions, it seemed as though we had a few options, but none of them were quite as nice. We had the option of repo-level or org-level secrets, and we would have to go with org-level if we want to share between repositories. This would mean any FanDuel repository would have access to the secret key/value. While this isn’t a huge deal for us, the vast majority of FanDuel repositories don’t need access and it would be ideal to limit to only those that do. At the org-level, we did have the option to specify which repositories have access to a secret but that would require GitHub admin privileges, which would mean we’d need to rope in DevOps for something that the developers could manage without additional support on Azure. Due to our principle of ‘Keep it Simple’ we decided to use repo-level secrets so we (without DevOps) can maintain our secrets.

Our Conclusion

For us, choosing to experiment seems to have paid off. It may only have been a minor win for us to have chosen GitHub Actions over Azure as we’ve only encountered one trade-off; storing the secret keys. Given we now have the pipelines in GitHub along with our code and package repositories we feel that’s a good trade. We still have some optimisations to do, for example, the version validation logic is currently duplicated in each project’s YAML file and most of it could be moved into a shared script, but for the most part, we’re happy with our experiment into using GitHub Actions.

References

¹ GitHub Actions Documentation / Learn / Contexts, [online]. Available at: https://docs.github.com/en/actions/learn-github-actions/contexts [accessed 14 September 2022]

--

--

Scott Laing
FanDuel Life

A software engineer and coffee drinker by day.