Skip to main content

This version of GitHub Enterprise was discontinued on 2023-07-06. No patch releases will be made, even for critical security issues. For better performance, improved security, and new features, upgrade to the latest version of GitHub Enterprise. For help with the upgrade, contact GitHub Enterprise support.

Using concurrency, expressions, and a test matrix

How to use advanced GitHub Actions features for continuous integration (CI).

Note: GitHub-hosted runners are not currently supported on GitHub Enterprise Server. You can see more information about planned future support on the GitHub public roadmap.

Example overview

This article uses an example workflow to demonstrate some of the main CI features of GitHub Actions. When this workflow is triggered, it tests your code using a matrix of test combinations with npm test.

The following diagram shows a high level view of the workflow's steps and how they run within the job:

Diagram of an event triggering a workflow that uses a test matrix.

Features used in this example

The example workflow demonstrates the following capabilities of GitHub Actions.

FeatureImplementation
Manually running a workflow from the UIworkflow_dispatch
Triggering a workflow to run automaticallypull_request
Running a workflow at regular intervalsschedule
Setting permissions for the tokenpermissions
Controlling how many workflow runs or jobs can run at the same timeconcurrency
Running the job on different runners, depending on the repositoryruns-on
Preventing a job from running unless specific conditions are metif
Using a matrix to create different test configurationsmatrix
Cloning your repository to the runneractions/checkout
Installing node on the runneractions/setup-node
Caching dependenciesactions/cache
Running tests on the runnernpm test

Example workflow

The following workflow was created by the GitHub Docs Engineering team. To review the latest version of this file in the github/docs repository, see test.yml.

Note: Each line of this workflow is explained in the next section at "Understanding the example."

YAML
name: Node.js Tests

# **What it does**: Runs our tests.
# **Why we have it**: We want our tests to pass before merging code.
# **Who does it impact**: Docs engineering, open-source engineering contributors.

on:
  workflow_dispatch:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read
  # Needed for the 'trilom/file-changes-action' action
  pull-requests: read

# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
  group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
  cancel-in-progress: true

jobs:
  test:
    # Run on self-hosted if the private repo or ubuntu-latest if the public repo
    # See pull # 17442 in the private repo for context
    runs-on: ${{ fromJSON('["ubuntu-latest", "self-hosted"]')[github.repository == 'github/docs-internal'] }}
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        # The same array lives in test-windows.yml, so make any updates there too.
        test-group:
          [
            content,
            graphql,
            meta,
            rendering,
            routing,
            unit,
            linting,
            translations,
          ]
    steps:
      # Each of these ifs needs to be repeated at each step to make sure the required check still runs
      # Even if if doesn't do anything
      - name: Check out repo
        uses: actions/checkout@v3
        with:
          # Not all test suites need the LFS files. So instead, we opt to
          # NOT clone them initially and instead, include them manually
          # only for the test groups that we know need the files.
          lfs: ${{ matrix.test-group == 'content' }}
          # Enables cloning the Early Access repo later with the relevant personal access token
          persist-credentials: 'false'

      - name: Figure out which docs-early-access branch to checkout, if internal repo
        if: ${{ github.repository == 'github/docs-internal' }}
        id: check-early-access
        uses: actions/github-script@v6
        env:
          BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
        with:
          github-token: ${{ secrets.DOCUBOT_REPO_PAT }}
          result-encoding: string
          script: |
            // If being run from a PR, this becomes 'my-cool-branch'.
            // If run on main, with the `workflow_dispatch` action for
            // example, the value becomes 'main'.
            const { BRANCH_NAME } = process.env
            try {
              const response = await github.repos.getBranch({
                owner: 'github',
                repo: 'docs-early-access',
                BRANCH_NAME,
              })
              console.log(`Using docs-early-access branch called '${BRANCH_NAME}'.`)
              return BRANCH_NAME
            } catch (err) {
              if (err.status === 404) {
                console.log(`There is no docs-early-access branch called '${BRANCH_NAME}' so checking out 'main' instead.`)
                return 'main'
              }
              throw err
            }

      - name: Check out docs-early-access too, if internal repo
        if: ${{ github.repository == 'github/docs-internal' }}
        uses: actions/checkout@v3
        with:
          repository: github/docs-early-access
          token: ${{ secrets.DOCUBOT_REPO_PAT }}
          path: docs-early-access
          ref: ${{ steps.check-early-access.outputs.result }}

      - name: Merge docs-early-access repo's folders
        if: ${{ github.repository == 'github/docs-internal' }}
        run: |
          mv docs-early-access/assets assets/images/early-access
          mv docs-early-access/content content/early-access
          mv docs-early-access/data data/early-access
          rm -r docs-early-access

      # This is necessary when LFS files where cloned but does nothing
      # if actions/checkout was run with `lfs:false`.
      - name: Checkout LFS objects
        run: git lfs checkout

      - name: Gather files changed
        uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b
        id: get_diff_files
        with:
          # So that `steps.get_diff_files.outputs.files` becomes
          # a string like `foo.js path/bar.md`
          output: ' '

      - name: Insight into changed files
        run: |

          # Must to do this because the list of files can be HUGE. Especially
          # in a repo-sync when there are lots of translation files involved.
          echo "${{ steps.get_diff_files.outputs.files }}" > get_diff_files.txt

      - name: Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 16.14.x
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Cache nextjs build
        uses: actions/cache@v3
        with:
          path: .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('package*.json') }}

      - name: Run build script
        run: npm run build

      - name: Run tests
        env:
          DIFF_FILE: get_diff_files.txt
          CHANGELOG_CACHE_FILE_PATH: tests/fixtures/changelog-feed.json
        run: npm test -- tests/${{ matrix.test-group }}/

Understanding the example

 The following table explains how each of these features are used when creating a GitHub Actions workflow.

Code Explanation
YAML
name: Node.js Tests

The name of the workflow as it will appear in the "Actions" tab of the GitHub repository.

YAML
on:

The on keyword lets you define the events that trigger when the workflow is run. You can define multiple events here. For more information, see "Triggering a workflow."

YAML
  workflow_dispatch:

Add the workflow_dispatch event if you want to be able to manually run this workflow in the UI. For more information, see workflow_dispatch.

YAML
  pull_request:

Add the pull_request event, so that the workflow runs automatically every time a pull request is created or updated. For more information, see pull_request.

YAML
  push:
    branches:
      - main

Add the push event, so that the workflow runs automatically every time a commit is pushed to a branch matching the filter main. For more information, see push.

YAML
permissions:
  contents: read
  pull-requests: read

Modifies the default permissions granted to GITHUB_TOKEN. This will vary depending on the needs of your workflow. For more information, see "Assigning permissions to jobs."

YAML
concurrency:
  group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'

Creates a concurrency group for specific events, and uses the || operator to define fallback values. For more information, see "Using concurrency."

YAML
  cancel-in-progress: true

Cancels any currently running job or workflow in the same concurrency group.

YAML
jobs:

Groups together all the jobs that run in the workflow file.

YAML
  test:

Defines a job with the ID test that is stored within the jobs key.

YAML
    runs-on: ${{ fromJSON('["ubuntu-latest", "self-hosted"]')[github.repository == 'github/docs-internal'] }}

Configures the job to run on a GitHub-hosted runner or a self-hosted runner, depending on the repository running the workflow. In this example, the job will run on a self-hosted runner if the repository is named docs-internal and is within the github organization. If the repository doesn't match this path, then it will run on an ubuntu-latest runner hosted by GitHub. For more information on these options see "Choosing the runner for a job."

YAML
    timeout-minutes: 60

Sets the maximum number of minutes to let the job run before it is automatically canceled. For more information, see timeout-minutes.

YAML
    strategy:
This section defines the build matrix for your jobs.
YAML
      fail-fast: false

Setting fail-fast to false prevents GitHub from cancelling all in-progress jobs if any matrix job fails.

YAML
      matrix:
        test-group:
          [
            content,
            graphql,
            meta,
            rendering,
            routing,
            unit,
            linting,
            translations,
          ]

Creates a matrix named test-group, with an array of test groups. These values match the names of test groups that will be run by npm test.

YAML
    steps:

Groups together all the steps that will run as part of the test job. Each job in a workflow has its own steps section.

YAML
      - name: Check out repo
        uses: actions/checkout@v3
        with:
          lfs: ${{ matrix.test-group == 'content' }}
          persist-credentials: 'false'

The uses keyword tells the job to retrieve the action named actions/checkout. This is an action that checks out your repository and downloads it to the runner, allowing you to run actions against your code (such as testing tools). You must use the checkout action any time your workflow will run against the repository's code or you are using an action defined in the repository. Some extra options are provided to the action using the with key.

YAML
      - name: Figure out which docs-early-access branch to checkout, if internal repo
        if: ${{ github.repository == 'github/docs-internal' }}
        id: check-early-access
        uses: actions/github-script@v6
        env:
          BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
        with:
          github-token: ${{ secrets.DOCUBOT_REPO_PAT }}
          result-encoding: string
          script: |
            // If being run from a PR, this becomes 'my-cool-branch'.
            // If run on main, with the `workflow_dispatch` action for
            // example, the value becomes 'main'.
            const { BRANCH_NAME } = process.env
            try {
              const response = await github.repos.getBranch({
                owner: 'github',
                repo: 'docs-early-access',
                BRANCH_NAME,
              })
              console.log(`Using docs-early-access branch called '${BRANCH_NAME}'.`)
              return BRANCH_NAME
            } catch (err) {
              if (err.status === 404) {
                console.log(`There is no docs-early-access branch called '${BRANCH_NAME}' so checking out 'main' instead.`)
                return 'main'
              }
              throw err
            }

If the current repository is the github/docs-internal repository, this step uses the actions/github-script action to run a script to check if there is a branch called docs-early-access.

YAML
      - name: Check out docs-early-access too, if internal repo
        if: ${{ github.repository == 'github/docs-internal' }}
        uses: actions/checkout@v3
        with:
          repository: github/docs-early-access
          token: ${{ secrets.DOCUBOT_REPO_PAT }}
          path: docs-early-access
          ref: ${{ steps.check-early-access.outputs.result }}

If the current repository is the github/docs-internal repository, this step checks out the branch from the github/docs-early-access that was identified in the previous step.

YAML
      - name: Merge docs-early-access repo's folders
        if: ${{ github.repository == 'github/docs-internal' }}
        run: |
          mv docs-early-access/assets assets/images/early-access
          mv docs-early-access/content content/early-access
          mv docs-early-access/data data/early-access
          rm -r docs-early-access

If the current repository is the github/docs-internal repository, this step uses the run keyword to execute shell commands to move the docs-early-access repository's folders into the main repository's folders.

YAML
      - name: Checkout LFS objects
        run: git lfs checkout

This step runs a command to check out LFS objects from the repository.

YAML
      - name: Gather files changed
        uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b
        id: get_diff_files
        with:
          # So that `steps.get_diff_files.outputs.files` becomes
          # a string like `foo.js path/bar.md`
          output: ' '

This step uses the trilom/file-changes-action action to gather the files changed in the pull request, so they can be analyzed in the next step. This example is pinned to a specific version of the action, using the a6ca26c14274c33b15e6499323aac178af06ad4b SHA.

YAML
      - name: Insight into changed files
        run: |
          echo "${{ steps.get_diff_files.outputs.files }}" > get_diff_files.txt

This step runs a shell command that uses an output from the previous step to create a file containing the list of files changed in the pull request.

YAML
      - name: Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 16.14.x
          cache: npm

This step uses the actions/setup-node action to install the specified version of the node software package on the runner, which gives you access to the npm command.

YAML
      - name: Install dependencies
        run: npm ci

This step runs the npm ci shell command to install the npm software packages for the project.

YAML
      - name: Cache nextjs build
        uses: actions/cache@v3
        with:
          path: .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('package*.json') }}

This step uses the actions/cache action to cache the Next.js build, so that the workflow will attempt to retrieve a cache of the build, and not rebuild it from scratch every time. For more information, see "Caching dependencies to speed up workflows."

YAML
      - name: Run build script
        run: npm run build

This step runs the build script.

YAML
      - name: Run tests
        env:
          DIFF_FILE: get_diff_files.txt
          CHANGELOG_CACHE_FILE_PATH: tests/fixtures/changelog-feed.json
        run: npm test -- tests/${{ matrix.test-group }}/

This step runs the tests using npm test, and the test matrix provides a different value for ${{ matrix.test-group }} for each job in the matrix. It uses the DIFF_FILE environment variable to know which files have changed, and uses the CHANGELOG_CACHE_FILE_PATH environment variable for the changelog cache file.

Next steps