目录

GitHub Actions:简介

GH100 科学 Python 项目推荐的 CI 是 GitHub Actions (GHA),尽管其前身 Azure 也被广泛使用,并且在一些软件包中可能会发现其他流行的服务(Travis、Appveyor 和 Circle CI)。GHA 由于其灵活、可扩展的设计以及与 GitHub 权限模型(和 UI)的紧密集成而成为首选。以下是如何使用 GHA 设置新软件包的指南。

GHA 由 工作流 组成,工作流由操作组成。以下是一些您可能希望在软件包中使用的工作流。这些应该放在名为 .github/workflows/main.yml 或类似的文件中。

您的主要 CI 工作流文件应该以类似以下的方式开头

name: CI

on:
  pull_request:
  push:
    branches:
      - main

jobs:

这为工作流提供了良好的名称 GH101,并定义了其运行条件。这将在所有拉取请求或对主分支的推送上运行。如果您使用开发分支,您可能希望将其包含在内。您还可以为拉取请求指定特定分支,而不是在所有 PR 上运行(仅在针对这些分支的 PR 上运行)。

提交前

如果您使用 pre-commit(您应该使用),并且您还不想/不能使用 pre-commit.ci,那么这是一个将为您检查 pre-commit 的作业

lint:
  name: Lint
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with:
        python-version: "3.x"
    - uses: pre-commit/action@v3.0.1

如果您确实使用了 pre-commit.ci,但您需要此作业运行手动检查,例如 check-manifest,那么您可以保留它,但只需使用 with: extra_args: --all-files --hook-stage manual check-manifest 来运行此检查。您也可以在其他作业中使用 needs: lint 来防止 lint 检查未通过时运行它们。

单元测试

实现单元测试也很容易。由于您应该遵循前面部分列出的最佳实践,因此无论软件包详细信息如何,这都变成了一个几乎可以直接复制粘贴的公式。您可能需要调整 Python 版本以满足您的口味;如果您愿意,您也可以在不同的操作系统上进行测试,方法是将它们添加到矩阵中并将其输入到 runs-on 中。

tests:
  runs-on: ubuntu-latest
  strategy:
    fail-fast: false
    matrix:
      python-version:
        - "3.9"
        - "3.11"
        - "3.13"
  name: Check Python ${{ matrix.python-version }}
  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0 # Only needed if using setuptools-scm

    - name: Setup Python ${{ matrix.python-version }}
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
        allow-prereleases: true

    - name: Install package
      run: python -m pip install -e .[test]

    - name: Test package
      run: python -m pytest

上面需要注意的一些事项

矩阵应包含您感兴趣的版本。如果您正在构建任何扩展或担心软件包在 macOS 或 Windows 上的情况,您也可以在其他操作系统上进行测试。快速失败是可选的。

此处用于安装的公式对于所有用户都应该相同;并且使用 PEP 517/518 构建,您甚至可以保证会生成与构建最终软件包时相同的、一致的轮子。

更新

GH200 GH210 如果您在存储库中使用非默认操作(您将在以下页面中看到一些),那么最好保持它们的最新状态。GitHub 提供了一种使用 dependabot 来做到这一点的方法。只需将以下文件添加为 .github/dependabot.yml

version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns:
          - "*"

这将每周检查操作是否有更新,并在有更新时创建一个 PR,包括 PR 中的变更日志和提交摘要。如果您选择一个像 v1 这样的名称,这应该只查找相同形式的更新(从 2022 年 4 月开始)——不再需要限制“移动标签”更新 GH211。您也可以使用 SHA,dependabot 也会尊重它。并且 groups 将组合操作更新 GH212,这既更简洁,有时也需要依赖操作,例如 upload-artifact/download-artifact

您也可以将其用于其他生态系统,包括 Python。

常见需求

单个操作系统步骤

如果需要仅在特定操作系统上运行步骤,请使用该步骤上的 if 和 runner.os

if: runner.os != 'Windows' # also 'macOS' and 'Linux'

使用 runner.osmatrix.<something> 更好。您还有一个环境变量 $RUNNER_OS。此处需要单引号。

更改步骤中的环境

如果需要更改后续步骤的环境变量,例如将 if 条件与仅适用于一个操作系统的条件组合,则将其添加到一个特殊文件中

- run: echo "MY_VAR=1" >> $GITHUB_ENV

后续步骤将看到此环境变量。

步骤之间通信

您还可以通过设置 id: 来直接在步骤之间进行通信。一些操作有输出,并且 bash 操作可以手动写入输出

- id: someid
  run: echo "something=true" >> $GITHUB_OUTPUT

您现在可以使用 ${{ steps.someid.something }} 在后续步骤中引用此步骤。您也可以使用 ${{ needs.<jobname>.outputs.something }} 从另一个作业中获取它。 toJson() 函数对于输入 JSON 很有用——您甚至可以以此方式动态生成矩阵!

美观输出

您可以将 GitHub 风格的 Markdown 写入 $GITHUB_STEP_SUMMARY,它将显示在摘要页面上。

您也可以输出注释;这些注释在 PR 中的代码中内联显示。这可以通过 设置特殊的双冒号输出 来完成,例如 echo "::error file=app.js,line=1::Missing semicolon"。有关使用 pytest 执行此操作的插件,请参阅 pytest-github-actions-annotate-failures

您也可以通过 提供匹配器 来执行此操作,匹配器会告诉 GitHub 查找某些模式。请记住,每个类型每个步骤最多只能看到 10 个匹配项,总共 50 个匹配器。

常用有用操作

有各种有用的操作。有 GitHub 提供的操作

以及许多其他有用的操作

Python 开发人员还有几个;请注意,这些没有像官方操作和大多数其他操作那样提供 vX 移动标签,而是具有您可以使用的 release/vX 分支。

还安装了一些有用的工具,可以真正简化您的工作流程或添加自定义操作。这包括系统包管理器(如 brew、chocolaty、NuGet、Vcpkg 等),以及一个很棒的跨平台工具

  • pipx:所有运行器上都预安装了此工具(GitHub 用于设置其他内容),并且会保持最新状态。它使您能够使用 pipx run <app> 一行代码使用任何 PyPI 应用程序。

您也可以在本地运行 GitHub Actions

  • act:在本地 Docker 镜像中运行 GitHub Actions。

高级用法

以下是一些您可能需要的内容。

取消现有运行

GH102 如果添加以下内容,您可以确保每次 PR/分支只运行一次,并在新的运行开始时取消旧的运行

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

任何具有匹配组名称的内容都将计入同一组 - ref 是 PR 的“来自”名称。如果需要,您可以将 github.ref 替换为 github.event.pull_request.number || github.sha;这仍然会在 PR 推送时取消,但会在 main 上构建每个提交。

传递作业

如果希望支持 GitHub 的“通过时合并”功能,则应设置传递作业,而不是列出您想要要求的每个作业。除了使添加和删除作业变得更容易之外,它还意味着添加新的必需作业不会使您过去已合并的所有 PR 从绿色复选标记更改为橙色“挂起”符号(因为有一些它们未通过的新要求)。

例如,如果您有 lintchecks 作业,请使用以下代码

pass:
  if: always()
  needs: [lint, checks]
  runs-on: ubuntu-latest
  steps:
    - uses: re-actors/alls-green@release/v1
      with:
        jobs: ${{ toJSON(needs) }}

我们希望作业始终运行,因此我们设置 if: always()。否则,如果它依赖的任何作业被跳过,它可能会被跳过,并且跳过的作业会被计为 GitHub 的自动合并的“通过”(糟糕!)。作业的重要部分是 needs: 列表;这告诉它需要什么。

我们使用 re-actors/alls-green 来评估必需的作业是否已通过。您需要告诉它哪些作业是必需的,您可以通过获取 needs 列表并将其作为 json 输入到 with: jobs: 中来实现,而无需重复 needs 列表。

这也将支持允许失败的作业(allowed-failures:)和允许跳过的作业(allowed-skips:)。

只需在主分支的必需检查中设置此 pass 作业。然后您将能够使用 GitHub 的自动合并功能。

自定义操作

您可以在本地或在共享的 GitHub 存储库中编写自己的操作,可以使用 GitHub Actions 语法本身(称为“复合”)、JavaScript 或 Docker。结合 pipx,复合操作非常容易编写!

要创建自定义操作,可以将其放在 .github/actions 中(仅供该存储库工作流程内部使用),或者如果要允许其他人将您的存储库用作操作,则放在 action.yml 中。文件开头如下所示

name: <some name>
description: <Some description>

您还可以设置输入,用户将在 with: 中放置这些输入

inputs:
  some-input:
    description: <Some description>
    required: true

然后,您指定该操作是复合的,并为其提供要运行的步骤

runs:
  using: composite
  steps:

如果指定了 runs: 步骤,则必须指定 shell: <something>。否则,它基本上与您习惯的相同;您可以使用 if: 等。

一个常见的用例是使用 Python。除非它是您操作的重点,否则理想情况下您不应该更改用户的环境;突然更改活动 Python 版本可能会令人意外。但是,您可以使用 setup-pythonpipx 中的 update-environment: false 来实现。

- uses: actions/setup-python@v5
  id: python
  with:
    python-version: "3.11"
    update-environment: false

- name: Run some local program
  shell: bash
  run:
    pipx run --python '${{ steps.python.outputs.python-path }}' '${{
    github.action_path }}' ${{ inputs.some-input }}

您可以使用 setup-python 中的 python-path 输出获取您激活的 Python。您可以使用 github.action_path 获取检出的操作的路径。

自定义复合操作的示例包括

可重用工作流程

您还可以创建可重用的工作流程。这样做的一个原因是它允许您使用 needs 或在工作流程之间传递值。这是一种使一个工作流程(可以包含多个作业,甚至矩阵)依赖于另一个工作流程的简单方法。

要使用可重用的工作流程,请将触发器替换为

on:
  workflow_call:

如果将 outputs: 表添加到工作流程调用表中,则可以指定其他工作流程读取的输出。请参阅文档中的其他选项

条件工作流程

有时,您拥有的作业取决于存储库中的某些文件。也许您只希望在代码或测试文件更改时运行测试,在文档更改时运行文档等。虽然 GitHub 允许您在触发器中指定文件,但它与必需检查或跨多个工作流程的使用配合不佳。以下是一种与这些内容配合良好的设置方法

将您的工作流程编写为可重用的工作流程。这意味着它们以允许其他工作流程调用的触发器开头

# reusable-tests.yml, for example
on:
  workflow_call:

否则,它们看起来像普通的工作流程。然后,您需要另一个可重用的工作流程文件来决定何时运行特定情况。

# reusable-change-detection.yml
on:
  workflow_call:
    outputs:
      run-tests:
        value: ${{ jobs.change-detection.outputs.run-tests || false }}
      # More here if you have more situations to detect

您首先在运行此文件时指定输出。您需要为每个要检测的情况创建一个输出。该值将从我们下面的 change-detection 作业输出,如果我们没有输出任何内容,则默认为“false”。

现在,我们需要我们的作业

jobs:
  change-detection:
    runs-on: ubuntu-latest
    outputs:
      run-tests: ${{ steps.cookie-changes.outputs.run-tests || false }}
      # more here if you have more situations to detect

    steps:
      - uses: actions/checkout@v4

      - name: Changed test-related files
        if: github.event_name == 'pull_request'
        id: changed-tests-files
        uses: Ana06/get-changed-files@v2.3.0
        with:
          format: "json"
          filter: |
            tests/**
            src/**.py
            .github/workflows/ci.yml
            .github/workflows/reusable-tests.yml

      - name: Set a flag for running the tests
        if: >-
          github.event_name != 'pull_request' ||
          steps.changed-tests-files.outputs.added_modified_renamed != '[]'
        id: tests-changes
        run: echo "run-tests=true" >> "${GITHUB_OUTPUT}"

      # Add 2 more steps per situation you have to detect

这有点样板代码(主要是在传递变量),但它所做的事情相当简单。与其逐步介绍它,不如看看它试图做什么。首先,您需要找到当前 PR 中所有已更改文件的列表。这是使用 Ana06/get-changed-files 完成的(它不提供 v2v2.2 移动标签)。然后使用 filter: 过滤该列表。它以 json 格式返回(否则您将无法支持文件名中包含空格)。返回的列表实际上并不重要;在下一步中,我们只需检查它是否为空(json 中的 [])。如果我们不在 PR 中或存在返回的文件,则设置 run-tests=true;否则,我们不设置(如果我们在 PR 中且没有匹配项)。

作业中的其他所有内容都与将步骤 changed-tests-files 的输出获取到 tests-changes,然后从那里输入到可重用工作流程输出作为 run-tests 有关。

可能有人可以编写一个操作(甚至可以使用 ghshell: python 的复合操作),可以直接报告 true/false 更改而不是文件列表,从而节省两步过程并大大简化此操作。

如果有多种情况,只需使用不同的 id 和输入重复这两个步骤。

最后,您编写结合了可重用工作流程的总体 CI 工作流程,例如 ci.yml

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

jobs:
  change-detection:
    uses: ./.github/workflows/reusable-change-detection.yml

  tests:
    needs: change-detection
    if: fromJSON(needs.change-detection.outputs.run-tests)
    uses: ./.github/workflows/reusable-tests.yml

  # more here if you need more

  pass:
    if: always()
    needs:
      - change-detection
      - tests
    runs-on: ubuntu-latest

    steps:
      - name: Decide whether the needed jobs succeeded or failed
        uses: re-actors/alls-green@release/v1
        with:
          allowed-skips: >-
            ${{
              fromJSON(needs.change-detection.outputs.run-tests)
              && ''
              || '
              tests,
              '
            }}
          jobs: ${{ toJSON(needs) }}

如果有多种情况,请在第一个之后添加另一个 ${{ ... }},并将它们添加到 needs 列表中。这实际上只是在“tests”作业被跳过时将“tests”注入 allowed-skips 中。

使用此方法的一些存储库示例包括

GitHub Pages

GitHub 已完成将其页面构建基础设施迁移到 Actions 的工作,并且他们现在提供直接从 Actions 推送到 Pages 的功能。这取代了旧的解决方法(强制)将输出推送到分支或单独的存储库。

在开始之前,请确保在 Pages 设置中将源设置为“Actions”。

您可能希望此作业在您的主分支以及 workflow_dispatch 上运行,以防您想手动触发重建。您应该设置权限,以便内置的 GITHUB_TOKEN 可以写入页面

permissions:
  contents: read
  pages: write
  id-token: write

GH103 您可能每次只希望部署一次,因此您可以使用

concurrency:
  group: "pages"
  cancel-in-progress: true

现在您需要在 steps: 中使用三个自定义操作。首先,您需要配置 Pages。

- name: Setup Pages
  id: pages
  uses: actions/configure-pages@v5

注意此操作设置了一个id:;这将允许您稍后使用此操作的输出;具体来说,在构建时可能需要使用${{ steps.pages.outputs.base_path }}(您也可以获取originbase_urlhost - 请参阅操作配置)。

- name: Upload artifact
  uses: actions/upload-pages-artifact@v3

此操作默认为上传_site,但如果需要,您可以提供任何with: path:,包括".",即整个仓库。

最后,您需要将工件(名为github-pages)部署到 Pages。您可以将其设置为一个自定义作业,使用needs:指向您之前的作业(在本例中,之前的作业称为build)。

deploy:
  environment:
    name: github-pages
    url: ${{ steps.deployment.outputs.page_url }}
  runs-on: ubuntu-latest
  needs: [build]
  steps:
    - name: Deploy to GitHub Pages
      id: deployment
      uses: actions/deploy-pages@v4

deploy-pages 作业提供一个page_url,它与配置步骤中的base_url相同,并且可以在environment中设置。如果您想在一个作业中完成所有操作,则只需要其中一个。

请参阅官方入门工作流以获取示例。其他一些示例包括

变更日志生成

不是 Actions 的直接一部分,但也在.github中是.github/release.yml,它允许您在发布时[配置变更日志生成][gh-changelog]按钮。以下配置将为您删除 dependabot 和 pre-commit-ci PR。

changelog:
  exclude:
    authors:
      - dependabot
      - pre-commit-ci

[gh-changelog]: https://githubdocs.cn/en/repositories/releasing-projects-on-github/automatically-generated-release-notes