Published April 12, 2023

As a Senior Software Engineer on Arcadia’s Frontend Infrastructure team, I built a lot of custom GitHub Actions (and “Workflows”) that were used internally to replace all of the continuous integration processes across all of the frontend products at the company.

GitHub Actions make it incredibly simple to run custom code in response to an event (like a git push), and generally if you’re storing your code on GitHub, using GitHub Actions are gonna be cheaper, faster, and more convenient than basically any other CI solution if you’re storing your code in GitHub.

Hopefully after reading this post, will get some ideas on how to structure your own internal library of actions.

BTW: This post is not an introduction to GitHub Actions, or a full open source of the work. If you’re unfamiliar with the basics, start with GitHub’s documentation.

The Foundations

The majority of the frontend applications at Arcadia depended on using the correct NodeJS runtime, along with setting up credentials to install private npm packages. Additionally, most teams wanted to benefit from caching npm installation between CI job runs to reduce the time it takes to rerun your tests.

To ensure we did all of this uniformly across products, we created an action called “node-setup”, which did the following,

  1. Clone the repository with actions/checkout.
  2. Parse the .nvmrc file in the repository (see: Node Version Manager).
  3. Setup the Node environment with actions/setup-node, along with the custom registry auth.
  4. Check if we have a cache hit for the npm modules required, if not, run npm ci and upload the npm cache.

We did this with an action so that it could easily be reused as a step in both other actions or workflows. This is one of those weird nuances you’ll have to grapple with as you build out your own library of reusable actions.

For caching, we used the npm cache folder. This is slower than caching the node_modules folder directly, but as I learned, doing that can break stuff, especially when dealing with packages that have binaries (eg: Cypress), or monorepo’s (eg: NX).

We also had an input on the action to disable caching, and you’ll thank yourself later for including this when debugging a really thorny Javascript problem and you’re scratching your head wondering, “Is it a caching issue?”.

The other foundational actions that we developed which “unlocked” a lot of opportunity was our ability to run custom Javascript in actions, and re-use that output between actions or workflows. This enabled us to ditch cumbersome, hard to decipher shell scripts, and write really straight forward Javascript that anyone using these actions could understand.

For example, imagine you wanted to compute a url safe subdomain from a Git repository and branch name. Writing this in bash is pretty gross,

prefix=$(printf "${app}-${git_branch}" | tr -c '[:alnum:]\\n' '-' | tr '[:upper:]' '[:lower:]')
release_name=$(printf ${prefix:0:30} | sed 's/-$//')

but is much more “human readable” in a scripting language like Javascript,