An Unintentionally Comprehensive Introduction to GitHub Actions CI
Tierney Cyren
Posted on September 30, 2019
We're currently approaching GitHub Actions v2 shipping publicly for everyone to use. I'm personally super excited about this because it means I don't need to configure an external service to run my CI – I can slap in some YAML, and I'm off with a cross-platform (!) CI system with multiple versions of Node.js installed.
For me, that's bliss. No need to go to an external site; everything is very neatly contained. That said, when I've used other CI services in the past (primarily Travis CI and Azure Pipelines) I've generally just copy/pasted someone else's CI configuration from the beginning and then tweaked it with additional context.
This time though, there's minimal prior context. During the beta of Actions v2, GitHub has published a few different CI templates that I could copy/paste certain parts from. However, there are a few standards I hold all of my projects to:
-
npm install
should pass on the latest versions of all operating systems -
npm test
should pass on the latest versions of all operating systems -
npm install
andnpm test
should succeed without fail on all currently supported Node.js versions
This ends up meaning I have a matrix of anywhere from 9 (3 versions multiplied by three operating systems) to 12 (4 versions multiplied by three operating systems) CI runs on every project at any time. I've found that the implementation of how to achieve this varies greatly depending on the CI system.
Given that there's not going to be a massive amount of prior art on release, I figured I'd begin building out some comprehensive templates so at launch people will have something to easily copy/paste and then tweak to suit their exact needs.
GitHub Actions CI Templates
After working on adding GitHub Actions CI to good-first-issue, I figured I should probably abstract the CI file into a repo, so it's a bit more accessible.
As such, last night, I built out GitHub Actions CI Templates. Initially, I shipped it with a single template that covered my needs around Node.js and npm, but as of about an hour ago I've added two additional templates: Node.js and Yarn, and Node.js and pnpm.
If you'd like to check out the templates, they're all relatively straightforward as far as YAML goes:
-
Node.js Cross-Platform:
- Runs builds on:
- Ubuntu (Latest),
- Windows (Latest),
- macOS (Latest)
- Using all versions of Node.js that are currently supported by the Node.js project,
- Using
npm install
andnpm test
.
-
Node.js Cross-Platform (using Yarn)
- Runs builds on:
- Ubuntu (Latest),
- Windows (Latest),
- macOS (Latest)
- Using all versions of Node.js that are currently supported by the Node.js project,
- Using
yarn install
andyarn test
.
-
Node.js Cross-Platform (using pnpm):
- Runs builds on:
- Ubuntu (Latest),
- Windows (Latest),
- macOS (Latest)
- Using all versions of Node.js that are currently supported by the Node.js project.
- Using
pnpm install
andpnpm test
.
Dissecting the GitHub Actions YAML for the Templates
The templates all follow a relatively similar structure. I figured I'd walk you through each line of code of the Node.js Cross-Platform file to help ensure that they're understandable to you. Let's go line by line, with code on the top and the description on the bottom:
name: Node.js Cross-platform CI (using Yarn)
The above line is the title of the entire CI script, as it'll show up in the Actions
tab of the GitHub repo.
Relevant docs:
on: [push]
The above line indicates the trigger for a run. For most CI cases, [push]
will be ideal since you want it to run every time you push code to the repo or to a PR.
Relevant docs:
jobs:
Workflows are composed of one or more jobs. This line is an indicator that we've got multiple jobs to be run.
Relevant docs:
- Workflow syntax docs -
jobs
- Usage limits, for context on limits around jobs
build:
This one is the job_id
of our specific job. Since we're running a build, I named this build
but this specific name has no semantic meaning inside of GitHub Actions CI itself.
Relevant docs:
runs-on: ${{ matrix.os }}
This is a required property, which tells the CI run what kind of machine it should be running on. In our case, we've added some complexity by adding a matrix of operating systems that need to be built against. That said, the context of the matrix gets hoisted, and we can use that context here.
One key thing to note from the docs:
Each job runs with a fresh instance of the virtual environment specified in by runs-on.
Meaning, every job is running a clean instance of whatever OS is selected. This is table stakes for CI, but it's always useful to keep it in mind. ❤️
Relevant docs:
- Workflow syntax docs -
jobs.<job_id>.runs-on
- Virtual environments for GitHub Actions, which lists all the possible supported values for this property
strategy:
Having a strategy
line is the way to begin defining a matrix of environments to run your builds in.
Relevant docs:
matrix:
The tl;dr of a matrix is that it's the set of all the pieces of context you'll want to run against. The most straightforward matrix is one row – for example, multiple Node.js versions on a single platform.
A simple matrix:
ubuntu-latest |
---|
Node.js 8 |
Node.js 10 |
Node.js 12 |
That said, JavaScript and Node.js applications are effectively run on all three of the major operating systems in the world as a part of developer workflows. Often, we'll want to run on the three major operating systems to ensure that there are no unexpected platform-specific bugs that are going to occur – especially in open source when there are very few direct paths to end-users. Luckily, a matrix makes this relatively straightforward.
By adding in multiple operating systems, our matrix gets more complex:
ubuntu-latest | macos-latest | windows-latest |
---|---|---|
Node.js 8 | Node.js 8 | Node.js 8 |
Node.js 10 | Node.js 10 | Node.js 10 |
Node.js 12 | Node.js 12 | Node.js 12 |
But... that's only the latest versions of each platform. What about older versions that we may often need to support? Well, it turns out that we can also use older versions of each platform in GitHub Actions CI, which could even further complicate the matrix:
ubuntu-latest | ubuntu-16.04 | macos-latest | macOS-10.14 | windows-latest | windows-2016 |
---|---|---|---|---|---|
Node.js 8 | Node.js 8 | Node.js 8 | Node.js 8 | Node.js 8 | Node.js 8 |
Node.js 10 | Node.js 10 | Node.js 10 | Node.js 10 | Node.js 10 | Node.js 10 |
Node.js 12 | Node.js 12 | Node.js 12 | Node.js 12 | Node.js 12 | Node.js 12 |
And this is currenlty a downtime for Node.js builds. Half of the year (every year) there are 4 supported release lines, which would look more like this:
ubuntu-latest | ubuntu-16.04 | macos-latest | macOS-10.14 | windows-latest | windows-2016 |
---|---|---|---|---|---|
Node.js 8 | Node.js 8 | Node.js 8 | Node.js 8 | Node.js 8 | Node.js 8 |
Node.js 10 | Node.js 10 | Node.js 10 | Node.js 10 | Node.js 10 | Node.js 10 |
Node.js 12 | Node.js 12 | Node.js 12 | Node.js 12 | Node.js 12 | Node.js 12 |
Node.js 13 | Node.js 13 | Node.js 13 | Node.js 13 | Node.js 13 | Node.js 13 |
A matrix is super useful in helping us programmatically define such a list without actually having to define each of these contexts individually. This utility mostly comes when you start adding more platforms and versions, but thankfully the overhead of doing that is incredibly low from the configuration side of things (see the following sections for more context)
Relevant docs:
os: [ubuntu-latest, windows-latest, macOS-latest]
The above is effectively a variable that we're assigning to the matrix, which can be dynamically called. In our case, we're just saying that the os
variable on matrix
(so matrix.os
) is going to be each of these. The how is still a bit magic to me, but... it works, seemingly by iterating over each of them when they're called. When used in conjunction with another variable (like node-version
), they're iterated over to create something like the tables above effectively.
Relevant docs:
- Virtual environments for GitHub Actions, which is where you can find information about all of the operating systems currently available.
node-version: [8.x, 10.x, 12.x]
Another variable where we're going to define the Node.js versions we'd want to be running.
Relevant docs:
- actions/setup-node – the GitHub Action we pass versions to, which defines the acceptable syntax for versions
- Software in virtual environments for GitHub Actions – an exhaustive list of software available in each virtual environment (OS) by default
steps:
Each job contains a set of steps
. This specific line is where we indicate that we're going to begin defining the steps.
Relevant docs:
- uses: actions/checkout@v1
Tells our workflow that we're going to be using the GitHub Action that can be found at actions/checkout
which maps to the GitHub org/repo at [gihub.com/actions/checkout]. It's also worth noting that @v1
which is a tagged and released version that can be found in the GitHub Releases for the repo.
Relevant docs:
-
actions/checkout, an action that checks out your repository to
$GITHUB_WORKSPACE
in the virtual environment. -
Workflow syntax docs -
jobs.<job_id>.steps.uses
- name: Use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
The name to display for the job in the UIs that it's rendered within, given the various variables that we've inserted using matrix
.
Note: There seems to be a bug where this does not render properly – instead of rendering as a tagged template literal, the Actions UI renders as a string.
Relevant docs:
uses: actions/setup-node@v1
Defines an external action – in this case, the [github.com/actions/setup-node] action at version 1.x.x (as released via the GitHub repo). In our case, this is an action that provides a super handy interface to install arbitrary versions of Node.js other than the version that comes baked into the VMs that are provided. My guess is that this will be a default action for anyone who is running JavaScript or Node.js builds simply because it handles so much for you by default.
It's worth noting that actions consumed with uses:
can be sourced from within the same repository, from a public repository, and from a Docker image published to Docker Hub.
Relevant docs:
with:
This is a map
(my assumption is that this is a map
in the sense of YAML's definition of a map) of the parameters defined in the action. In our case, actions/setup-node
needs a version to run with.
Relevant docs:
node-version: ${{ matrix.node-version }}
The actions/setup-node
action needs a version of Node.js to run, via the node-version:
property. Since we named the variable for Node.js versions in our Matrix node-versions
, we're able to pass matrix.node-version
to the node-version:
property.
Relevant docs:
- name: npm install and test
We're again defining the name of a job. In this case, there's no dynamic information since the commands we're going to be running are pretty static.
I use npm install
and npm test
, but your applications may vary in install/build/test/ci commands – my recommendation for this is to tweak both the title and the actual commands, so it's extremely clear what's being run.
Relevant docs:
run: |
npm install
npm test
This is an interesting set of lines for those unfamiliar with YAML. We start with using a run
property for the job, which allows us to run any command on the system. In our case, we're going to use this to run npm install
and npm test
... but those are two different commands, that need to be run separately. The pipe (|
) is a tool defined in the YAML spec as Literal Style. In our case, it allows us to write multiple lines that execute independently without having to use multiple run:
commands or multiple jobs. Basically, it's shorthand that enables use to be looser in how we're able to build out our file.
Note: It may be worth using
npm ci
rather thannpm install
to install dependencies, given thatnpm ci
was tailor-made for CI environments. You can find more details onnpm ci
in the official documentation.
Relevant docs:
env:
Allows us to set up environment variables in our virtual environments with relative ease.
Relevant docs:
CI: true
This one is a personal preference, and also happens to be the default for the simplest Node.js workflow suggested by GitHub. Simply sets an environment variable that can be easily picked up on by various tools. GitHub
Relevant docs:
What's next?
Currently, GitHub Actions CI is in a semi-public beta as a part of GitHub Actions v2 – they've invited a bunch of folks who applied to use it. That said, if you feel like this is a repeat of what happened when GitHub Actions initially shipped last year, you'll be happy to know that in the GitHub Special Event in which GitHub Actions CI and GitHub Actions v2 were shared, Nat Friedman said that GitHub Actions CI and GitHub Actions v2, along with GitHub Package Registry, is shipping to everyone on November 13th – the first day of GitHub Universe.
So, in just over a month from the date of publishing this article, you'll be able to start using GitHub Actions CI on any and every public project for free. 🎉
If you've got any questions or comments about what I've talked about in this post, or if there's more you'd like to learn about GitHub Actions CI or GitHub Actions v2, I'd be more than happy to see if I can either answer your questions in the comments directly, make good free and public repos that can help give you answers, or write more posts on the subject if you'd find that helpful!
Posted on September 30, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.