Skip to main content

Using concurrency, expressions, and a test matrix

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

注: GitHub ホステッド ランナーは、現在 GitHub Enterprise Server でサポートされていません。 GitHub public roadmap で、今後の計画的なサポートの詳細を確認できます。

Example overview

この記事では、ワークフローの例を使って、GitHub Actions の主な CI 機能の一部を示します。 When this workflow is triggered, it tests your code using a matrix of test combinations with npm test.

次の図は、ワークフローの手順とジョブ内でそれを実行する方法の概要を示したものです。

Overview diagram of workflow steps

Features used in this example

このワークフロー例は、GitHub Actions の次の機能を示しています。

FeatureImplementation
UI からワークフローを手動で実行する:workflow_dispatch
ワークフローをトリガーして自動的に実行する:pull_request
定期的にワークフローを実行する:schedule
トークンのアクセス許可の設定:permissions
同時に実行できるワークフロー実行またはジョブの数を制御する:concurrency
Running the job on different runners, depending on the repository:runs-on
特定の条件が満たされない限り、ジョブが実行されないようにする:if
Using a matrix to create different test configurations:matrix
リポジトリをランナーにクローンする:actions/checkout
ランナーへの node のインストール:actions/setup-node
Caching dependencies:actions/cache
Running tests on the runner:npm test

Example workflow

次のワークフローは、GitHub ドキュメント エンジニアリング チームによって作成されました。 github/docs リポジトリ内でこのファイルの最新バージョンを確認するには、次を参照してください。 test.yml.

: このワークフローの各行については、次のセクションの「例の説明」に説明があります。

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

 次の表では、GitHub Actions ワークフローを作成するときにこれらの各機能がどのように使われるかを説明します。

Code Explanation
YAML
name: Node.js Tests

GitHub リポジトリの [アクション] タブに表示されるワークフローの名前。

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