GitHub Actions - CI/CD Without Leaving the Comfort of Your Repo

7 minute read

Not content with threatening the package repository space with the announcement of its Registry offering, GitHub looks to upset the increasingly crowded CI tool sphere with Actions. I’m lucky enough to have been involved in GitHub’s beta program so I thought I’d give a brief rundown of what they can do as well as my initial impressions.

Workflow Overview

GitHub’s Workflows are made up of a number jobs with each job spinning up its own virtual environment. Each job runs in parallel by default but sequential running can be enforced by defining dependencies as well as setting concurrency limits. As each job runs in its own environment they don’t have access to each other’s environment, however data and files can be shared between jobs using the concept of artifacts.

Within a job there are steps. These are tasks that all run within the same virtual environment and can run either actions or commands.

Your repository’s workflows can be viewed in the new Actions tab:

actions main view

From this view you can:

  • View the history of all your workflows
  • Rerun failed runs
  • Cancel runs
  • Download logs and other build artifacts

The view also allows you to directly edit and create workflows.

Example - Maven Build

Before diving into the finer details of workflow configuration we’ll look at a simple example. Namely a basic Maven project build.

Workflows are created by adding their definition, in the form of a yaml file, under the .github/workflows directory. Assuming our Maven project has a parent POM in the java-project folder then a workflow which performs a Maven install will look like this:

name: Build a Maven Thing
on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - name: Build with Maven
      run: mvn install --file java-project/pom.xml

Some points of interest here:

name
An optional label for your workflow, used in various places in the UI. If omitted, the filename of your action will be used
on
Events that will trigger your workflow, in this case we only trigger on a push
jobs

Jobs are where the work happens. Each job is given an identifier (here build) and has a number of properties that define its behaviour. In our case we have:

name: Name of the job for display in the UI

steps: These are the tasks that are carried out as part of the job. These can run actions defined somewhere in GitHub (here we use actions to checkout source code and set up a Java environment) or run commands on the operating system directly (here we run the maven build command).

runs-on: The environment to run your workflow on

uses: this references an action to run. In our case we use externally defined actions to checkout the source code and set up a Java 8 environment.

A run of this workflow looks like the following:

workflow run

Where each step has it’s own expandable section as do in-build job setup and teardown steps

Environments

A number of environments are provided in which your workflows’ jobs will run. At the time of writing the following virtual environments are provided:

  • Windows Server (2016 R2 and 2019)
  • Ubuntu (16.04 and 18.04)
  • macOS X Mojave 10.14

Each of these environments provides a set of common build tools such as various versions of Java, Maven, Gradle and Node. For workflows that require running on multiple environments you can leverage the concept of a matrix:

runs-on: $
strategy:
  matrix:
    os: [ubuntu-16.04, ubuntu-18.04]
steps:
  ...

Your workflow will then run once on each of the specified hosts. Matrix declarations extend beyond just environments and can have multiple dimensions, with runs being carried out on all combinations of the parameters.

As well as running directly on one of GitHub’s environments you can run your workflows within docker containers. This is useful for providing greater control, environments not provided by GitHub and for avoiding lengthy tool setups. Steps are run inside a container either by virtue of a consumed action defining a container or by specifying container rather than runs-on for your job:

jobs:
  my_job:
    container:
      image: node:10.16-jessie
      env:
        NODE_ENV: development
      ports:
        - 80
      volumes:
        - my_docker_volume:/volume_mount
      options: --cpus 1

Actions

Actions provide an atomic, reusable unit of work. They can be defined within the same repo as your workflow, from an external GitHub repository or from a published Docker image. All types can use commits, tags and branches to reference specific versions of actions and this is highly recommended.

Actions are defined by a yaml file containing it’s inputs, outputs, environment and other metadata. They come in two flavours:

  • JavaScript Actions that run directly in the host environment using node.
  • Docker Actions, on the other hand run inside a Docker container which can be a container pulled from the Docker registry or build to order by providing a Docker file with your actions.

A toolkit is provided to aid development of both types of actions as well as guidance on choosing the type best suited to your use case.

Despite being still in beta there are already a number of actions ready to be consumed by your workflow. Some useful existing actions include:

And of course Jenkins Trigger for when you can’t quite leave it behind.

Further examples can be found in the GitHub Marketplace

Running commands

As well as using predefined actions to carry out your workflow, commands can also be run using the operating systems shell, via run. These single or multi-line commands will run in a default shell (bash for non-Windows and cmd for Windows) unless an override is provided via the shell keyword. This keyword allows you to specify one of a number of standard shell alternatives such as powershell for Windows, sh for Linux/MacOS and python for all. Commands can be labelled by specifying a name. If one is not provided the command itself will be used.

Some default behaviour is set by GitHub when running shell commands. These include setting fail-fast behaviour and returning the exit code of last run commands. These behaviours are well documented and can be overridden if desired.

Injecting values

Actions have values passed into them via the with keyword. This is a map providing key/values pairs which are translated so that values provided as:

jobs:
  build:
    steps:
        uses: actions/my_action@master
        with:
          name: Mona
          age: 43

will provide environment variables INPUT_NAME and INPUT_AGE.

For any sensitive data that needs to be consumed by the action, GitHub provides a new Secrets section in your repository settings:

secrets

This allows you to add a number of values which will be encoded and can only be used within GitHub actions. These secrets can then be reference using the $ syntax

Handily, GitHub provides a built-in GITHUB_TOKEN secret to allow your workflow to perform authenticated actions against the repository such as code pushes and deploying to the forthcoming GitHub Package Registry. In addition to this, a number of repository specific variables are provided to exposes details about the repository and current workflow run.

Triggering a Workflow

Workflows can be configured to run based on a number of events. These can include:

  • Webhook events (all standard events are supported)
  • Cron based timer events
  • External triggers using the GitHub API

As well as globally defining events, you can selectively trigger workflows based on additional factors such as branches or affected paths

on:
  push:
    branches:
    - master
    - release/*
on:
  pull_request:
    paths:
    - 'tests/*.js'
    - '!tests/no-run/*'

Note that changes carried out as part of a workflow, itself cannot trigger a workflow. So if your workflow pushes changes to the repository, it will not trigger a new run.

Final Thoughts

After spending some time delving in to the world of GitHub actions, my general impressions are positive. Not having to manage (or pay others to manage) your CI infrastructure as well as having your workflow runs kept right alongside your code are clear advantages. Being built into the GitHub infrastructure also means another set of configurations gone. Adding the number of extensibility options via direct and pre-built actions the power of Actions is clear.

Some obvious places where GitHub actions may not be suitable include:

  • Workflows that run across repos
  • Workflows that require access to internal infrastructure that isn’t visible to GitHub
  • Custom Windows and Mac environment requirements
  • User interactions such as providing inputs and manual confirmations

Overall, though GitHub actions has the making of a very powerful CI/CD tool that will provide some very significant productivity improvements to development and DevOps teams.