使用 GitHub Actions CI/CD 工作流发布包发行版本#

GitHub Actions CI/CD 允许你在 GitHub 平台上发生事件时运行一系列命令。一种流行的选择是拥有一个由 push 事件触发的 workflow。本指南展示了如何在你推送标记提交时发布 Python 发行版。它将使用 pypa/gh-action-pypi-publish GitHub Action 进行发布。它还使用 GitHub 的 upload-artifactdownload-artifact 操作来临时存储和下载源包。

注意

本指南假设你已经有一个项目,你知道如何为其构建发行版,并且它位于 GitHub 上。本指南还避免了构建特定于平台的项目的详细信息。如果你有二进制组件,请查看 cibuildwheel 的 GitHub Action 示例。

配置受信任的发布#

本指南依赖于 PyPI 的 受信任的发布 实现来连接到 GitHub Actions CI/CD。出于安全原因,建议这样做,因为生成的令牌是为你的每个项目单独创建的,并且会自动过期。否则,你需要为 PyPI 和 TestPyPI 生成 API 令牌。如果要发布到第三方索引,如 devpi,你可能需要提供用户名/密码组合。

由于本指南将演示如何上传到 PyPI 和 TestPyPI,因此我们需要配置两个受信任的发布者。以下步骤将指导你为你的新 PyPI 项目 创建“待定”发布者。但是,如果你拥有某个预先存在的项目,也可以向其添加 受信任的发布

注意

如果你按照本指南的早期版本进行操作,则已创建了机密 PYPI_API_TOKENTEST_PYPI_API_TOKEN 以直接访问 PyPI 和 TestPyPI。现在这些已过时,如果你要使用新设置替换旧设置,则应将其从 GitHub 存储库中删除,并在 PyPI 和 TestPyPI 帐户设置中撤销它们。

让我们开始吧!🚀

  1. 转到 https://pypi.ac.cn/manage/account/publishing/

  2. 填写你希望在其下发布你的新 PyPI 项目 的名称(setup.cfgpyproject.toml 中的 name 值),GitHub 存储库所有者的名称(组织或用户)和存储库名称,以及 .github/ 文件夹下的发布工作流文件名称,请参阅 创建工作流定义。最后,添加我们将在你的存储库下设置的 GitHub 环境(pypi)的名称。注册受信任的发布者。

  3. 现在,转到 https://test.pypi.org/manage/account/publishing/ 并重复第二步,但这次,输入 testpypi 作为 GitHub 环境的名称。

  4. 您的“待定”发布者现在已准备好首次使用,并且在您首次使用它们后会自动创建您的项目。

    注意

    如果您没有 TestPyPI 帐户,则需要创建一个。它与常规 PyPI 帐户不同。

    注意

    出于安全原因,您必须要求 手动批准 pypi 环境中的每次运行。

创建工作流定义#

GitHub CI/CD 工作流在存储在存储库的 .github/workflows/ 目录中的 YAML 文件中声明。

让我们创建一个 .github/workflows/publish-to-test-pypi.yml 文件。

以一个有意义的名称开始它,并定义应该让 GitHub 运行此工作流的事件

name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI

on: push

签出项目并构建发行版#

我们将不得不定义两个作业,分别发布到 PyPI 和 TestPyPI,以及一个额外的作业来构建发行版包。

首先,我们将定义一个作业来构建项目的 dist 包并将其存储以供以后使用

jobs:
  build:
    name: Build distribution 📦
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.x"

这会将您的存储库下载到 CI 运行程序中,然后安装并激活最新的可用 Python 3 版本。

现在我们可以从源构建 dist 并存储它们。在此示例中,我们将使用 build 包。因此,将此添加到步骤列表中

    - name: Install pypa/build
      run: >-
        python3 -m
        pip install
        build
        --user
    - name: Build a binary wheel and a source tarball
      run: python3 -m build
    - name: Store the distribution packages
      uses: actions/upload-artifact@v3
      with:
        name: python-package-distributions
        path: dist/

定义工作流作业环境#

现在,让我们为将发布到 PyPI 的作业添加初始设置。这是一个将执行我们稍后定义的命令的过程。在本指南中,我们将使用 GitHub Actions 提供的最新稳定 Ubuntu LTS 版本。这也为作业定义了一个 GitHub 环境,以便在其上下文中运行,以及一个 URL,以便在 GitHub 的 UI 中很好地显示。此外,它允许获取 OpenID Connect 令牌,pypi-publish 操作需要它来实现对 PyPI 的无密钥可信发布。

  publish-to-pypi:
    name: >-
      Publish Python 🐍 distribution 📦 to PyPI
    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes
    needs:
    - build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.ac.cn/p/<package-name>  # Replace <package-name> with your PyPI project name
    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

这也将确保仅在当前提交被标记时才触发 PyPI 发布工作流。

将发行版发布到 PyPI#

最后,在末尾添加以下步骤

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to PyPI
      uses: pypa/gh-action-pypi-publish@release/v1

此步骤使用 pypa/gh-action-pypi-publish GitHub 操作:在存储的发行版包被 download-artifact 操作下载后,它无条件地将 dist/ 文件夹的内容上传到 PyPI。

对发行版包进行签名#

以下作业使用 Sigstore 对发行版包进行签名,Sigstore 是用于对 CPython 进行签名的相同工件签名系统

首先,它使用 sigstore/gh-action-sigstore-python GitHub 操作 对发行版包进行签名。在下一步中,使用 gh CLI 从当前标记创建了一个空的 GitHub 版本。请注意,此步骤可以进一步自定义。请参阅 gh 版本文档 作为参考。

提示

您可能需要管理您的 GITHUB_TOKEN 权限以启用创建 GitHub 版本。请参阅 GitHub 文档 以获取说明。具体来说,令牌需要 contents: write 权限。

最后,将签名的发行版上传到 GitHub 版本。

  github-release:
    name: >-
      Sign the Python 🐍 distribution 📦 with Sigstore
      and upload them to GitHub Release
    needs:
    - publish-to-pypi
    runs-on: ubuntu-latest

    permissions:
      contents: write  # IMPORTANT: mandatory for making GitHub Releases
      id-token: write  # IMPORTANT: mandatory for sigstore

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Sign the dists with Sigstore
      uses: sigstore/gh-action-sigstore-python@v2.1.1
      with:
        inputs: >-
          ./dist/*.tar.gz
          ./dist/*.whl
    - name: Create GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      run: >-
        gh release create
        '${{ github.ref_name }}'
        --repo '${{ github.repository }}'
        --notes ""
    - name: Upload artifact signatures to GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      # Upload to GitHub Release using the `gh` CLI.
      # `dist/` contains the built packages, and the
      # sigstore-produced signatures and certificates.
      run: >-
        gh release upload
        '${{ github.ref_name }}' dist/**
        --repo '${{ github.repository }}'

注意

这是 GPG 签名的替代方案,PyPI 已 从 PyPI 中删除 了对它的支持。但是,此作业对于上传到 PyPI 并不是必需的,可以省略。

单独的工作流用于发布到 TestPyPI#

现在,重复这些步骤并在 jobs 部分下为发布到 TestPyPI 包索引创建另一个作业

  publish-to-testpypi:
    name: Publish Python 🐍 distribution 📦 to TestPyPI
    needs:
    - build
    runs-on: ubuntu-latest

    environment:
      name: testpypi
      url: https://test.pypi.org/p/<package-name>

    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to TestPyPI
      uses: pypa/gh-action-pypi-publish@release/v1
      with:
        repository-url: https://test.pypi.org/legacy/

提示

通常不需要在 testpypi GitHub 环境中要求手动批准,因为它旨在在每次提交到主分支时运行,并且通常用于指示健康的发布发布管道。

整个 CI/CD 工作流#

在按照上述指南操作后,本段展示了整个工作流。

点击此处显示整个 GitHub Actions CI/CD 工作流定义
name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI

on: push

jobs:
  build:
    name: Build distribution 📦
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.x"
    - name: Install pypa/build
      run: >-
        python3 -m
        pip install
        build
        --user
    - name: Build a binary wheel and a source tarball
      run: python3 -m build
    - name: Store the distribution packages
      uses: actions/upload-artifact@v3
      with:
        name: python-package-distributions
        path: dist/

  publish-to-pypi:
    name: >-
      Publish Python 🐍 distribution 📦 to PyPI
    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes
    needs:
    - build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.ac.cn/p/<package-name>  # Replace <package-name> with your PyPI project name
    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to PyPI
      uses: pypa/gh-action-pypi-publish@release/v1

  github-release:
    name: >-
      Sign the Python 🐍 distribution 📦 with Sigstore
      and upload them to GitHub Release
    needs:
    - publish-to-pypi
    runs-on: ubuntu-latest

    permissions:
      contents: write  # IMPORTANT: mandatory for making GitHub Releases
      id-token: write  # IMPORTANT: mandatory for sigstore

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Sign the dists with Sigstore
      uses: sigstore/gh-action-sigstore-python@v2.1.1
      with:
        inputs: >-
          ./dist/*.tar.gz
          ./dist/*.whl
    - name: Create GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      run: >-
        gh release create
        '${{ github.ref_name }}'
        --repo '${{ github.repository }}'
        --notes ""
    - name: Upload artifact signatures to GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      # Upload to GitHub Release using the `gh` CLI.
      # `dist/` contains the built packages, and the
      # sigstore-produced signatures and certificates.
      run: >-
        gh release upload
        '${{ github.ref_name }}' dist/**
        --repo '${{ github.repository }}'

  publish-to-testpypi:
    name: Publish Python 🐍 distribution 📦 to TestPyPI
    needs:
    - build
    runs-on: ubuntu-latest

    environment:
      name: testpypi
      url: https://test.pypi.org/p/<package-name>

    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to TestPyPI
      uses: pypa/gh-action-pypi-publish@release/v1
      with:
        repository-url: https://test.pypi.org/legacy/

好了,伙计们!#

现在,每当你将标记提交推送到 GitHub 上的 Git 存储库远程时,此工作流都会将其发布到 PyPI。它还会发布任何推送到 TestPyPI 的内容,这对于向你的 Alpha 用户提供测试版本以及确保你的发布管道保持健康很有用!

注意

如果你的存储库有频繁的提交活动,并且如上所述每次推送都上传到 TestPyPI,该项目可能会超过PyPI 项目大小限制。可以增加限制,但更好的解决方案可能是将 PyPI 兼容服务器(如 pypiserver)用于测试目的的 CI。

注意

建议将集成的 GitHub Actions 保持在最新版本,并经常更新它们。