Skip to main content
Last updated: 18 Apr 2023

Test & build a project with GitHub Actions

GitHub Actions is an automated workflow system that GOV.UK uses for Continuous Integration (CI). We also have Jenkins (and previously had Concourse) which provides similar functionality. In GOV.UK RFC 123 we decided that GitHub Actions is the preferred platform for GOV.UK CI usage where the wider platform integration of Jenkins is not required.

If your workflow requires the use of secrets, please talk to GOV.UK senior tech before deploying it. This is to help GOV.UK establish consistent and effective secret management for GitHub Actions across the wider alphagov GitHub organisation.

GOV.UK Conventions for GitHub Actions

Name of CI workflow file

You should name the file used for configuring the CI workflow ci.yml.

When the CI workflow should run

The workflow should be configured to run when there is a push to the default branch (typically “main”) and when a pull_request opened. This means CI runs when a branch is merged (the push to main) and it runs against any changes introduced in a pull request. We prefer to run against a pull_request event to all push events, as this allows external contributors to have pull requests tested and because pull_request runs against a version of the codebase that is merged with the repository default branch.

Example configuration:

# ./github/workflows/ci.yml
on:
  push:
    branches:
      - main
  pull_request:

To run CI on-demand, outside a pull request, for example before opening a PR, you may configure a repository to have a workflow_dispatch event so you can run it from the GitHub Actions user interface. If you do this you will need to configure the checkout action to reference the appropriate commit.

Example configuration:

on:
  push:
    branches:
      - main
  pull_request:
  workflow_dispatch:
    inputs:
      ref:
        description: 'The branch, tag or SHA to checkout'
        default: main
        type: string

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ inputs.ref || github.ref }}

We previously recommended CI jobs to run on [push, pull_request] as per RFC-123 however this caused duplicate runs of the CI jobs of a pull request (one for the push another for the pull_request) which were both superfluous for our testing, a point of confusion and were depleting our allowance quotas.

Avoid superfluous naming of workflows, jobs and steps

In workflow files you have the option to specify a name attribute for the workflow itself, the jobs and each individual step.

The workflow and any jobs should not be given a name as the workflow filename and job_id should be sufficiently explanatory.

Steps should also not be named, as the text in the run command is normally sufficiently descriptive. We only should add names to steps where they are used to either explain a particularly cryptic set of commands or where there are identically appearing steps that we wish to disambiguate between.

Prefer a single or small number of steps within a CI workflow

Rather than having a number of granular steps (such as: “Lint Ruby”, “Lint JS”, “Run Ruby tests”, “Run JS Tests”) prefer a single step (such as bundle exec rake) where more of the CI task configuration is handled in application code.

This is so we can:

  • keep CI configurations simple and consistent;
  • make it easier to replicate CI checks in a development environment.

Branch protection rules

Pull Requests cannot be merged until the test job has passed. Your workflow should always define a job called test.

Base your workflow on one of our documented examples

GitHub Actions currently lack templating functionality to enable re-use of workflow configuration across projects. Therefore, in order to maintain consistency across GOV.UK projects, it is beneficial that we document and re-use common practices.

This documentation contains examples of a number of common build needs for GOV.UK projects. You are encouraged to base your project’s configuration on these and to follow their conventions. If your preferences, or common action conventions, deviate from these examples you are encouraged to open a PR to this repository to propose a change here. This is preferential to taking a project in a different direction and losing consistency.

GitHub Action Examples

Simple Ruby Application

# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - run: bundle exec rake

Notes:

  • ruby/setup-ruby will automatically select the Ruby version based on a .ruby-version file and will install bundler if a necessary.

A Ruby Gem

This differs to a simple Ruby application because Ruby gems are not tied to particular Ruby or Rails versions. Therefore we should test against various combinations of supported versions.

To do this, we use a build matrix. In this example, we test against three Ruby versions (2.7, 3.0, 3.1) and two Rails versions (6, 7). Combined, this results in 6 variations for the workflow to test against.

# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
  # Run the test suite against multiple Ruby and Rails versions
  test_matrix:
    strategy:
      fail-fast: false
      matrix:
        # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
        ruby: [2.7, '3.0', 3.1]
        # Test against multiple Rails versions
        gemfile: [rails_6, rails_7]
    runs-on: ubuntu-latest
    env:
      BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
    steps:
    - uses: actions/checkout@v3
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{ matrix.ruby }}
        bundler-cache: true
    - run: bundle exec rake

  # Branch protection rules cannot directly depend on status checks from matrix jobs.
  # So instead we define `test` as a dummy job which only runs after the preceding `test_matrix` checks have passed.
  # Solution inspired by: https://github.community/t/status-check-for-a-matrix-jobs/127354/3
  test:
    needs: test_matrix
    runs-on: ubuntu-latest
    steps:
      - run: echo "All matrix tests have passed 🚀"

  # We have a shared workflow that can be used for most gem publishing needs. You may have to write
  # your own if you have a gem that is released in a complex way.
  publish:
    needs: test
    if: ${{ github.ref == 'refs/heads/main' }}
    permissions:
      contents: write
    uses: alphagov/govuk-infrastructure/.github/workflows/publish-rubygem.yaml@main
    secrets:
      GEM_HOST_API_KEY: ${{ secrets.ALPHAGOV_RUBYGEMS_API_KEY }}

For each Rails version, a *.gemfile should exist in a top-level directory called gemfiles.

# gemfiles/rails_7.gemfile
source "https://rubygems.org"

gem "rails", "~> 7"

gemspec path: "../"

Notes:

GOV.UK Rails application with Postgres, Redis, Yarn and GOV.UK Content Schemas dependencies

This is an example that suits a relatively complex GOV.UK Rails application (this was written for Content Publisher) that has software dependencies (provided by Docker containers), cloning of a supplementary repository, and the use of Node.js for JavaScript dependency management.

on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:9.4
        ports:
          - 5432:5432
        env:
          POSTGRES_HOST_AUTH_METHOD: trust
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
      redis:
        image: redis
        ports:
          - 6379:6379
        options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
    env:
      RAILS_ENV: test
      REDIS_URL: redis://localhost:6379/0
      TEST_DATABASE_URL: postgresql://postgres@localhost/content-publisher
      GOVUK_CONTENT_SCHEMAS_PATH: vendor/publishing-api/content_schemas
    steps:
      - name: Clone project
        uses: actions/checkout@v2
      - name: Clone publishing api for content schemas
        uses: actions/checkout@v2
        with:
          repository: alphagov/publishing-api
          ref: deployed-to-production
          path: vendor/publishing-api
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - uses: actions/setup-node@v1
      - name: Check for cached node modules
        uses: actions/cache@v1
        with:
          path: node_modules
          key: yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: yarn
      - run: yarn install --frozen-lockfile
      - run: bundle exec rails db:setup
      - run: bundle exec rails assets:precompile
      - run: bundle exec rake

Notes:

  • Similar service configurations to PostgreSQL and Redis can be applied to other software dependencies such as MySQL, MongoDB or Rabbit MQ.
  • The health-cmd checks are used on services to ensure the build does not continue until the service is running.
  • Additional repositories are cloned to the vendor directory for a consistent location for third party code, this is consistent with Bundler.
  • Since the same actions are used in different contexts the name option has been used in steps to distinguish the different steps.
  • The bundle exec rails assets:precompile task is used to build a warm cache of assets before starting tests to reduce risk of timeouts, this is due to an underlying performance issue with GOV.UK asset compilation.
  • In the underlying project (Content Publisher) bundle exec rake performs: Ruby & JS unit testing; Ruby, SCSS, JavaScript and FactoryBot linting; and brakeman security audit.
  • This example does not include tagging the main branch with a particular release tag when there is a successful main build.