Skip to main content

使用并发、表达式和测试矩阵

如何使用高级 GitHub Actions 功能进行持续集成 (CI)。

示例概述

This article uses an example workflow to demonstrate some of the main CI features of GitHub Actions. 触发此工作流程时,它将使用具有 npm test 的测试组合矩阵来测试代码。

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

工作流程步骤概览图

此示例中使用的功能

The example workflow demonstrates the following capabilities of GitHub Actions:

功能实现
Manually running a workflow from the UI:workflow_dispatch
Triggering a workflow to run automatically:pull_request
Running a workflow at regular intervals:schedule
Setting permissions for the token:permissions
Controlling how many workflow runs or jobs can run at the same time:concurrency
在不同的运行器上运行作业,具体取决于存储库:runs-on
Preventing a job from running unless specific conditions are met:if
使用矩阵创建不同的测试配置:matrix
Cloning your repository to the runner:actions/checkout
Installing node on the runner:actions/setup-node
缓存依赖项:actions/cache

示例工作流程

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 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 }}/

了解示例

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

代码 说明
YAML
name: Node.js Tests

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

YAML
on:

“on”关键字允许您定义在工作流程运行时触发的事件。 您可以在此处定义多个事件。 更多信息请参阅“触发工作流程”。

YAML
  workflow_dispatch:

如果您希望能够在 UI 中手动运行此工作流程,请添加“workflow_dispatch”事件。 更多信息请参阅 workflow_dispatch

YAML
  pull_request:

添加“pull_request”事件,以便每次创建或更新拉取请求时工作流程自动运行。 更多信息请参阅 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. 更多信息请参阅 push

YAML
permissions:
  contents: read
  pull-requests: read

修改授予“GITHUB_TOKEN”的默认权限。 这将因工作流程的需求而异。 更多信息请参阅“为作业分配权限”。

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

为特定事件创建并发组,并使用“||' 运算符来定义回退值。 更多信息请参阅“使用并发”。

YAML
  cancel-in-progress: true

取消同一并发组中任何当前正在运行的作业或工作流程。

YAML
jobs:

将工作流程文件中运行的所有作业组合在一起。

YAML
  test:

定义 ID 为“test”的作业,该作业存储在“jobs”键中。

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

将作业配置为在 GitHub 托管的运行器或自托管运行器上运行,具体取决于运行工作流程的存储库。 在此示例中,如果存储库名为“docs-internal”并且位于“github”组织内,则作业将在自托管运行器上运行。 如果存储库与此路径不匹配,则它将在 GitHub 托管的“ubuntu-latest”运行器上运行。 有关这些选项的更多信息,请参阅“为作业选择运行器”。

YAML
    timeout-minutes: 60

设置在自动取消作业之前让作业运行的最大分钟数。 更多信息请参阅 timeout-minutes

YAML
    strategy:
本节定义作业的生成矩阵。
YAML
      fail-fast: false

将“fail-fast”设置为“false”可防止 GitHub 在任何矩阵作业失败时取消所有正在进行的作业。

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

创建一个名为“test-group”的矩阵,其中包含一个测试组数组。 这些值匹配将由“npm test”运行的测试组名称。

YAML
    steps:

组合将作为“test”作业一部分运行的所有步骤。 工作流程中的每个作业都有自己的“steps”部分。

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

“uses”关键字告诉作业检索名为“actions/checkout”的操作。 这是检出仓库并将其下载到运行器的操作,允许针对您的代码运行操作(例如测试工具)。 只要工作流程针对仓库的代码运行,或者您使用仓库中定义的操作,您都必须使用检出操作。 使用“with”键为操作提供了一些额外的选项。

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
            }

如果当前存储库是“github/docs-internal”存储库,则此步骤使用“actions/github-script”操作来运行脚本以检查是否存在名为“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 }}

如果当前存储库是“github/docs-internal”存储库,则此步骤将从上一步中标识的“github/docs-early-access”中签出分支。

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

如果当前存储库是“github/docs-internal”存储库,则此步骤使用“run”关键字执行shell命令,以将“docs-early-access”存储库的文件夹移动到主存储库的文件夹中。

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

此步骤运行命令以从存储库中签出 LFS 对象。

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: ' '

此步骤使用“trilom/file-changes-action”操作来收集在拉取请求中更改的文件,以便可以在下一步中分析它们。 此示例使用 'a6ca26c14274c33b15e6499323aac178af06ad4b' SHA固定到操作的特定版本。

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

此步骤运行 shell 命令,以使用上一步的输出来创建文件,其中包含在拉取请求中更改的文件列表。

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

此步骤使用“actions/setup-node”操作在运行器上安装指定版本的“node”软件包,这使您可以访问“npm”命令。

YAML
      - name: Install dependencies
        run: npm ci

此步骤运行 npm ci shell 命令来安装项目的 npm 软件包。

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

此步骤使用“actions/cache”操作来缓存 Next.js 构建,以便工作流程尝试检索构建的缓存,而不是每次都从头开始重新构建。 更多信息请参阅“缓存依赖关系以加快工作流程”。

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

此步骤运行构建脚本。

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 }}/

此步骤使用“npm test”运行测试,并且测试矩阵为矩阵中的每个作业提供不同的 ${{ matrix.test-group }} 值。 它使用“DIFF_FILE”环境变量来了解哪些文件已更改,并将“CHANGELOG_CACHE_FILE_PATH”环境变量用于更改日志缓存文件。

后续步骤