开发技巧

给一个小型 Python 库配上 GitHub Actions 持续集成

给一个小型 Python 库配上 GitHub Actions 持续集成:每次推送和 PR 自动在 Python 3.11/3.12/3.13 上跑代码检查和测试,覆盖率低于 90% 直接失败,并把覆盖率报告上传为构件。

BINBHO · 图文教程

给一个小型 Python 库配上 GitHub Actions 持续集成:每次推送和 PR 自动在 Python 3.11/3.12/3.13 上跑代码检查和测试,覆盖率低于 90% 直接失败,并把覆盖率报告上传为构件。

先看清成品

库代码和测试已在本机运行:ruff 0.17.1 检查通过(先修复了一次导入排序问题,见调试示例),pytest 3 个测试通过,覆盖率 100%。workflow 文件已用 actionlint 1.7.12 和 check-jsonschema(GitHub workflow schema)校验通过。没有推送到 GitHub 实际运行 Actions,矩阵和缓存的效果以你仓库里的运行记录为准。

注意:遵守 GitHub 服务条款。不要在仓库、日志或 issue 里提交密钥和个人数据;不要用 Actions 挖矿或做与仓库无关的大量计算;不要共享账号。

做完之后你会得到什么?

成果 说明
textkit 库 一个 slugify 函数,带测试
CI workflow 三个 Python 版本并行
质量门槛 ruff 检查 + 覆盖率 ≥ 90%
构件 coverage-xml,保留 7 天

需要准备哪些环境?

项目 要求 说明
GitHub 仓库 公开或私有 私有仓库的 Actions 分钟数有额度,以 GitHub 计费页为准
Python 3.11 以上 本地先跑通
actionlint 可选 本地检查 workflow 语法

文件结构

19-github-A-src/
├── pyproject.toml
├── src/textkit/__init__.py
├── tests/test_slugify.py
└── .github/workflows/ci.yml

第 1 步:写库和测试

src/textkit/__init__.py:

import re
import unicodedata


def slugify(text: str, max_len: int = 60) -> str:
    """把标题转成 URL 片段:保留中文、字母、数字,其他字符换成连字符。"""
    text = unicodedata.normalize("NFKC", text).lower()
    text = re.sub(r"[^\w\u4e00-\u9fff]+", "-", text).strip("-")
    text = re.sub(r"-{2,}", "-", text)
    return text[:max_len].rstrip("-")

tests/test_slugify.py:

from textkit import slugify


def test_basic():
    assert slugify("Hello, World!") == "hello-world"

def test_chinese_and_fullwidth():
    assert slugify("GitHub Actions 入门(一)") == "github-actions-入门-一"

def test_max_len():
    assert slugify("a-" * 50, max_len=10) == "a-a-a-a-a"

pyproject.toml:

[project]
name = "textkit"
version = "0.1.0"
requires-python = ">=3.11"

[project.optional-dependencies]
dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6"]

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
addopts = "-q --cov=textkit --cov-report=term-missing --cov-report=xml --cov-fail-under=90"

第 2 步:本地跑通

python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
ruff check .
pytest

检查结果(本机实际输出节选):

src/textkit/__init__.py       7      0   100%
Required test coverage of 90% reached. Total coverage: 100.00%
3 passed

第 3 步:写 workflow

.github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

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

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
          cache-dependency-path: pyproject.toml
      - name: 安装
        run: pip install -e ".[dev]"
      - name: 代码检查
        run: ruff check .
      - name: 测试
        run: pytest
      - name: 上传覆盖率报告
        if: matrix.python-version == '3.13'
        uses: actions/upload-artifact@v7
        with:
          name: coverage-xml
          path: coverage.xml
          retention-days: 7

要点:

  • permissions: contents: read:GITHUB_TOKEN 只给读权限。
  • concurrency:同一分支新提交会取消旧的运行。
  • fail-fast: false:一个版本失败不影响其他版本跑完,方便看是哪个版本的问题。
  • cache: pip + cache-dependency-path:依赖不变时复用缓存。

第 4 步:本地校验 workflow

actionlint .github/workflows/ci.yml

没有输出就是通过。

第 5 步:推送并查看结果

git add . && git commit -m "ci: 添加测试 workflow" && git push

到仓库 Actions 标签页,应看到 test (3.11)、test (3.12)、test (3.13) 三个任务;3.13 的运行页底部有 coverage-xml 构件。

建议在 Settings → Branches 给 main 加分支保护,把三个任务设为必须通过的检查。

调试示例

第一次本地运行 ruff check . 实际报错:

I001 Import block is un-sorted or un-formatted
 --> src/textkit/__init__.py:1:1
Found 2 errors.
[*] 2 fixable with `--fix`.

ruff 0.17.1 默认规则包含导入排序(I001),要求导入块后空两行。执行 ruff check --fix . 后通过。CI 里同样会因此失败,所以先在本地跑。

出错了怎么办?

现象 原因 解决
Actions 标签页没有运行 文件不在 .github/workflows/ 或分支不是 main 检查路径和 on.push.branches
ModuleNotFoundError: textkit 没有用 pip install -e . 安装 保留安装步骤
Coverage failure: total of 80 is less than fail-under=90 新代码没测试 补测试,不要随便降门槛
只有一个版本失败 该版本语法或依赖差异 打开对应任务日志看
缓存一直没命中 cache-dependency-path 指向的文件每次都变 指向依赖声明文件

模板规则

  • 先本地 ruff check . && pytest,再推送。
  • workflow 默认只读权限,需要写权限的任务单独声明。
  • Action 版本用大版本标签(如 @v7),由 Dependabot 更新(见本工具 C 篇)。

项目资源

用途 地址
GitHub Actions 文档 https://docs.github.com/en/actions
Actions 安全使用指南 https://docs.github.com/en/actions/reference/security/secure-use
actions/setup-python https://github.com/actions/setup-python
actionlint https://github.com/rhysd/actionlint
ruff 文档 https://docs.astral.sh/ruff/
GitHub 服务条款 https://docs.github.com/en/site-policy/github-terms/github-terms-of-service

CI 的价值在于让“能不能合并”有一个客观标准。先把检查跑在本地,再让 Actions 在每个版本上重复一遍,问题会在合并之前暴露出来。