给一个小型 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 在每个版本上重复一遍,问题会在合并之前暴露出来。