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パブリックロードマップで、計画されている将来のサポートに関する詳しい情報を見ることができます。

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の以下の機能を紹介しています:

機能Implementation
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
ノードをランナーにインストール:actions/setup-node
Caching dependencies:actions/cache

ワークフローの例

以下のワークフローは、GitHub Docs Engineeringチームが作成しました。 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 PAT
          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ワークフローの作成時にどのように使われるかを説明しています。

コード Explanation
YAML
name: Node.js Tests

GitHubリポジトリの"Actions"タブに表示されるワークフローの名前。

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. これは、リポジトリをチェックアウトしてランナーにダウンロードし、コードに対してアクション(テストツールなど)を実行できるようにします。 ワークフローがリポジトリのコードに対して実行されるとき、またはリポジトリで定義されたアクションを使用しているときはいつでも、チェックアウトアクションを使用する必要があります。 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.

次のステップ