🚀 Python 项目 CI/CD 实战:用 GitHub Actions 搭建自动化测试、覆盖率与发布流水线

🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

- [🚀 Python 项目 CI/CD 实战:用 GitHub Actions 搭建自动化测试、覆盖率与发布流水线](#🚀 Python 项目 CI/CD 实战:用 GitHub Actions 搭建自动化测试、覆盖率与发布流水线)
-
- 摘要
- 内容
- 一、前言
- [二、CI/CD 核心概念与 GitHub Actions 定位](#二、CI/CD 核心概念与 GitHub Actions 定位)
-
- [2.1 CI 与 CD 不是一回事](#2.1 CI 与 CD 不是一回事)
- [2.2 GitHub Actions 的核心竞争力](#2.2 GitHub Actions 的核心竞争力)
- [三、Workflow 语法精讲](#三、Workflow 语法精讲)
-
- [3.1 最小可用 Workflow](#3.1 最小可用 Workflow)
- [3.2 触发器(Triggers)详解](#3.2 触发器(Triggers)详解)
- [3.3 Job 与 Step 的关系](#3.3 Job 与 Step 的关系)
- [3.4 矩阵测试(Matrix Strategy)](#3.4 矩阵测试(Matrix Strategy))
- [3.5 Secrets 与环境变量](#3.5 Secrets 与环境变量)
- [3.6 条件执行与 Job 依赖](#3.6 条件执行与 Job 依赖)
- [四、构建高效 Python 测试流水线](#四、构建高效 Python 测试流水线)
-
- [4.1 完整 CI 模板(pytest + coverage + ruff + mypy)](#4.1 完整 CI 模板(pytest + coverage + ruff + mypy))
- [4.2 `pyproject.toml` 完整配置](#4.2
pyproject.toml完整配置) - [4.3 缓存策略:让 CI 时间减半](#4.3 缓存策略:让 CI 时间减半)
-
- [Poetry 项目缓存](#Poetry 项目缓存)
- [Hatch / pdm 项目缓存](#Hatch / pdm 项目缓存)
- [五、自动发布到 PyPI](#五、自动发布到 PyPI)
-
- [5.1 准备 PyPI API Token](#5.1 准备 PyPI API Token)
- [5.2 使用 Trusted Publishers(推荐)](#5.2 使用 Trusted Publishers(推荐))
- [5.3 语义化版本自动管理](#5.3 语义化版本自动管理)
- [5.4 GitHub Release 自动创建](#5.4 GitHub Release 自动创建)
- [六、自动发布到 DockerHub](#六、自动发布到 DockerHub)
-
- [6.1 基础 Docker 镜像构建](#6.1 基础 Docker 镜像构建)
- [6.2 多架构构建(amd64 + arm64)](#6.2 多架构构建(amd64 + arm64))
- [6.3 关键技巧](#6.3 关键技巧)
-
- [(1) GitHub Actions 缓存(GHA cache)](#(1) GitHub Actions 缓存(GHA cache))
- [(2) 安全扫描(Trivy)](#(2) 安全扫描(Trivy))
- [(3) 镜像签名(Cosign)](#(3) 镜像签名(Cosign))
- [七、依赖安全扫描与 Dependabot](#七、依赖安全扫描与 Dependabot)
-
- [7.1 pip-audit 检查已知漏洞](#7.1 pip-audit 检查已知漏洞)
- [7.2 Dependabot 自动升级](#7.2 Dependabot 自动升级)
- [7.3 CodeQL 代码静态安全扫描](#7.3 CodeQL 代码静态安全扫描)
- [八、用 Reusable Workflow 复用流水线](#八、用 Reusable Workflow 复用流水线)
-
- [8.1 定义 Reusable Workflow](#8.1 定义 Reusable Workflow)
- [8.2 调用方仓库使用](#8.2 调用方仓库使用)
- 九、实战:把所有流水线串起来
-
- [完整 release.yml(自动发布到 PyPI + DockerHub)](#完整 release.yml(自动发布到 PyPI + DockerHub))
- [十、CI/CD 最佳实践 Checklist](#十、CI/CD 最佳实践 Checklist)
-
- [10.1 速度](#10.1 速度)
- [10.2 可靠性](#10.2 可靠性)
- [10.3 安全](#10.3 安全)
- [10.4 可观测性](#10.4 可观测性)
- [10.5 可维护性](#10.5 可维护性)
- 十一、常见问题排查
-
- [Q1:CI 本地能跑,GitHub 上失败](#Q1:CI 本地能跑,GitHub 上失败)
- [Q2:矩阵测试中 Windows 慢得多](#Q2:矩阵测试中 Windows 慢得多)
- Q3:私有仓库额度用尽
- [Q4:缓存命中率为 0](#Q4:缓存命中率为 0)
- [Q5:OIDC 发布 PyPI 失败 "403 Forbidden"](#Q5:OIDC 发布 PyPI 失败 "403 Forbidden")
- [Q6:Codecov 显示覆盖率错误下降](#Q6:Codecov 显示覆盖率错误下降)
- [Q7:Docker 多架构构建 OOM](#Q7:Docker 多架构构建 OOM)
- 十二、总结
摘要
CI/CD(持续集成 / 持续交付)是现代软件工程的标配能力,但许多 Python 项目仍在用"本地手动跑一遍 pytest、git tag 后手动上传 PyPI、SSH 到服务器 docker pull 重启"的原始流程。本文以 GitHub Actions 为载体,从 workflow 语法、依赖缓存、矩阵测试、pytest + coverage + ruff/mypy 自动化质量门禁,讲到语义化版本自动发包到 PyPI、构建多架构 Docker 镜像推送 DockerHub、以及依赖安全扫描与 Dependabot 配置。整套方案开箱即用,能让一个 Python 库 / 服务在推到 GitHub 的瞬间完成「测试 → 检查 → 构建 → 发布」的全流程,让开发者专注代码而非流程。
🚀 个人主页 : 有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
内容
一、前言
「我的代码本地能跑啊」------这句话在协作开发中是事故的代名词。当一个 Python 项目走到 3 人以上协作、或者已经开始对外发布时,缺少自动化流水线会立刻暴露以下问题:
- 测试只在开发者笔记本上跑过,merge 到 main 后才暴露回归
- 多个 Python 版本兼容性靠人肉切换,3.10 跑通、3.8 崩溃
- 发版要手动
python setup.py sdist upload,容易漏发、错发 - 依赖出现 CVE 漏洞,没人及时发现
- 新成员不知道代码风格约定,review 时反复挑格式
CI/CD 流水线的本质,就是把上面这些**"应该自动化的"变成代码------和业务代码一起进版本库、一起 review、一起回滚。本文聚焦 GitHub Actions 这一当下最流行的 CI/CD 平台(2024 年 Q3 数据:GitHub Actions 在开源项目使用率超 70%),手把手搭一套生产可用**的 Python 项目流水线。
阅读本文后你将掌握:
- GitHub Actions 的核心概念与 workflow 语法
- 使用
actions/cache缓存 pip / poetry / venv,构建时间缩短 50%+- 矩阵测试(matrix strategy)覆盖多 Python / 多操作系统
- pytest + coverage + ruff + mypy 一体化质量门禁
- 语义化版本自动发布到 PyPI 与 DockerHub
- 依赖安全扫描与 Dependabot 自动升级
二、CI/CD 核心概念与 GitHub Actions 定位
2.1 CI 与 CD 不是一回事
很多人把 CI/CD 当作一个整体,其实两者目标不同:
| 概念 | 全称 | 目标 | 触发条件 | 典型产物 |
|---|---|---|---|---|
| CI | Continuous Integration | 集成代码时自动测试与检查 | 每次 push / PR | 绿/红勾、测试报告、覆盖率 |
| CD-Delivery | Continuous Delivery | 把构建物发布到制品库(随时可部署) | CI 通过 + tag | PyPI 包、Docker 镜像、wheel 文件 |
| CD-Deployment | Continuous Deployment | 把构建物部署到运行环境 | 制品发布后自动触发 | 在线服务可访问 |
2.2 GitHub Actions 的核心竞争力
主流 CI/CD 平台对比:
| 平台 | YAML 配置 | 与 GitHub 集成 | 自定义 Action | 公开仓库免费 | 私有仓库免费额度 |
|---|---|---|---|---|---|
| GitHub Actions | ✅ | ⭐⭐⭐⭐⭐ | 庞大 Marketplace | ✅ 无限 | 2000 分钟/月 |
| GitLab CI/CD | ✅ | 与 GitLab 强绑定 | 较少 | ✅ 无限 | 400 分钟/月 |
| CircleCI | ✅ | 通过 OAuth | 较多 | ✅ | 6000 分钟/月 |
| Jenkins | ❌(Groovy DSL) | 自建 | 插件生态庞大 | 自费 | 不限 |
| Drone | ✅ | 良好 | 一般 | ✅ | 自费 |
GitHub Actions 的最大优势在于:
- 与 GitHub 深度集成:PR 检查、required status、CODEOWNERS、issue trigger 均原生支持
- Marketplace 生态:3 万+ 官方与社区 Action,常见需求(如 docker build、gh release)几乎零代码
- 矩阵构建:原生支持多 OS / 多运行时并行
- 自托管 Runner:私有化场景可接入内部机房硬件
三、Workflow 语法精讲
3.1 最小可用 Workflow
在仓库根目录创建 .github/workflows/ci.yml:
yaml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装 Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: 安装依赖
run: pip install -e ".[dev]"
- name: 跑测试
run: pytest -v
推到 GitHub 后,每次 PR 与 push 到 main/develop 都会自动跑这段流水线,PR 上出现绿勾/红勾。
3.2 触发器(Triggers)详解
yaml
on:
push:
branches:
- main
- 'release/**' # 通配:release/1.0, release/2.0
- '!release/beta' # 排除:release/beta 不触发
tags:
- 'v*' # 推 v1.0.0 等 tag 时触发
paths:
- 'src/**'
- 'tests/**'
- 'pyproject.toml'
- '.github/workflows/**'
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
schedule:
- cron: "0 2 * * 1-5" # 工作日每天凌晨 2 点跑(UTC)
workflow_dispatch: # 手动触发,可加输入参数
inputs:
debug:
description: '启用 debug 模式'
type: boolean
default: false
release:
types: [published]
issue_comment:
types: [created]
高频技巧: 限制 paths 能避免改 README 触发完整 CI,显著节省额度。
3.3 Job 与 Step 的关系
Workflow
├── Job 1: test (ubuntu-latest)
│ ├── Step 1: checkout
│ ├── Step 2: setup python
│ ├── Step 3: install deps
│ └── Step 4: pytest
├── Job 2: lint (ubuntu-latest)
│ └── ...
└── Job 3: build (windows-latest)
- Job :一组 Step 的集合,在同一个 Runner(虚拟机)上执行,可以共享文件系统
- Step :Job 内的最小执行单元,可以是 shell 命令(
run:)或 Action(uses:) - Job 之间默认并行 ,可通过
needs建立依赖关系形成 DAG
3.4 矩阵测试(Matrix Strategy)
矩阵策略让一个 Job 在多个组合上并行执行,是覆盖多版本兼容性的关键:
yaml
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # 单个失败不取消其他
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
exclude:
- os: macos-latest
python-version: "3.8" # 排除特定组合
include:
- os: ubuntu-latest
python-version: "3.13" # 追加组合
experimental: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e ".[dev]"
- run: pytest
上面的配置会生成 3 × 5 = 15 个并行 Job,10 分钟的测试在 15 个 Runner 上 10 分钟跑完。
💡 免费额度提醒:公开仓库无限免费;私有仓库 2000 分钟/月。矩阵策略会成倍消耗额度,建议私有仓库矩阵仅覆盖关键版本(3.9 / 3.11 / 3.12)。
3.5 Secrets 与环境变量
yaml
env: # 全局环境变量
PYTHONUNBUFFERED: "1"
PIP_NO_CACHE_DIR: "1"
jobs:
deploy:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
env: # Job 级环境变量
TWINE_USERNAME: __token__
steps:
- uses: actions/checkout@v4
- name: 上传 PyPI
env:
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} # Step 级
run: |
pip install build twine
python -m build
twine upload dist/*
机密管理 :在仓库 Settings → Secrets and variables → Actions 中添加 PYPI_API_TOKEN。Secrets 在日志中会被自动脱敏为 ***。
3.6 条件执行与 Job 依赖
yaml
jobs:
lint:
runs-on: ubuntu-latest
steps: [...]
test:
needs: lint # 等 lint 通过再跑
runs-on: ubuntu-latest
steps: [...]
build:
needs: test
runs-on: ubuntu-latest
steps: [...]
deploy:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
# 只有打 tag 才部署
steps: [...]
if 表达式常用上下文:
| 上下文 | 含义 |
|---|---|
github.event_name |
触发事件名(push / pull_request / schedule 等) |
github.ref |
完整 ref(refs/heads/main、refs/tags/v1.0.0) |
github.ref_type |
branch 或 tag |
github.actor |
触发者用户名 |
github.event.pull_request.draft |
是否为草稿 PR |
github.repository |
owner/repo |
steps.<id>.conclusion == 'success' |
上一步是否成功 |
四、构建高效 Python 测试流水线
4.1 完整 CI 模板(pytest + coverage + ruff + mypy)
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
concurrency: # 同一 PR 新 push 取消旧 run
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: 安装 ruff 与 mypy
run: pip install ruff mypy
- name: Ruff 检查
run: ruff check .
- name: Ruff 格式检查
run: ruff format --check .
- name: Mypy 类型检查
run: mypy src/
test:
name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
exclude:
- os: windows-latest
python-version: "3.9"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # coverage diff 需要 git 历史
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip # 关键:自动缓存 pip
cache-dependency-path: pyproject.toml
- name: 安装依赖
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: 跑测试 with coverage
run: |
pytest --cov=src --cov-report=xml --cov-report=term-missing --cov-report=html
- name: 上传覆盖率到 Codecov
if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- name: 上传 HTML 覆盖率报告
if: failure()
uses: actions/upload-artifact@v4
with:
name: coverage-html-${{ matrix.python-version }}-${{ matrix.os }}
path: htmlcov/
retention-days: 7
build:
name: Build Distribution
needs: [quality, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: 构建分发包
run: |
pip install build
python -m build
- name: 上传 dist 作为 artifact
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
4.2 pyproject.toml 完整配置
让上面 CI 能跑起来的项目级配置:
toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mypkg"
version = "0.1.0"
description = "示例 Python 包"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [{ name = "Your Name", email = "you@example.com" }]
dependencies = [
"httpx>=0.24",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4",
"pytest-cov>=4.1",
"pytest-asyncio>=0.21",
"ruff>=0.1.0",
"mypy>=1.5",
"types-requests",
]
[tool.hatch.build.targets.wheel]
packages = ["src/mypkg"]
# ============ Ruff ============
[tool.ruff]
target-version = "py39"
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"N", # pep8-naming
"RUF", # ruff-specific
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"PT", # flake8-pytest-style
]
ignore = [
"E501", # 行长由 format 处理
"B008", # 函数默认值调用(如 FastAPI Depends)
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # 测试可以用 assert
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
# ============ Mypy ============
[tool.mypy]
python_version = "3.9"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
ignore_missing_imports = true
exclude = ["docs/", "build/"]
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
# ============ Pytest ============
[tool.pytest.ini_options]
minversion = "7.0"
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
]
testpaths = ["tests"]
markers = [
"slow: 耗时较长的测试",
"integration: 需要外部服务的集成测试",
]
asyncio_mode = "auto"
# ============ Coverage ============
[tool.coverage.run]
source = ["src"]
branch = true
parallel = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
]
fail_under = 80
show_missing = true
4.3 缓存策略:让 CI 时间减半
actions/setup-python@v5 内置了 pip 缓存,只需在 with 中加 cache: pip:
yaml
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: pyproject.toml # 依赖文件 hash 改变时重建缓存
Poetry 项目缓存
yaml
- name: 缓存 Poetry venv
uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry/virtualenvs
~/.local/bin/poetry
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
Hatch / pdm 项目缓存
yaml
# pdm
- uses: actions/cache@v4
with:
path: |
~/.cache/pdm
.venv
key: ${{ runner.os }}-pdm-${{ hashFiles('pdm.lock') }}
# Hatch
- uses: actions/cache@v4
with:
path: |
~/.cache/hatch
key: ${{ runner.os }}-hatch-${{ hashFiles('pyproject.toml') }}
缓存效果实测(中型项目,依赖 50 个):
| 策略 | 首次 install | 缓存命中 install |
|---|---|---|
| 无缓存 | 85s | 85s |
| pip 缓存 | 85s | 18s |
| 完整 venv 缓存 | 85s | 3s |
五、自动发布到 PyPI
5.1 准备 PyPI API Token
- 注册 PyPI 账号
- Account settings → API tokens → Add API token
- Scope 选择 "Entire account"(首次发布)或指定项目
- 复制
pypi-xxxxxxxxxxxxxxxx格式的 token - 在仓库 Settings → Secrets → Actions 中添加
PYPI_API_TOKEN
5.2 使用 Trusted Publishers(推荐)
2024 年 PyPI 推出 Trusted Publishers(OIDC),无需 API Token,安全性更高:
- 在 PyPI 项目设置 → Publishing → 添加 GitHub 仓库(owner/repo + workflow 文件名)
- workflow 中使用
pypa/gh-action-pypi-publish@release/v1,无需 token
yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install build
- run: python -m build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish-pypi:
needs: build
runs-on: ubuntu-latest
environment: pypi # 配置 required reviewers 做手动确认
permissions:
id-token: write # OIDC 必需
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
# 无需 token!
with:
print-hash: true
5.3 语义化版本自动管理
手动维护版本号容易出错,推荐 Commitizen 自动管理:
bash
pip install commitizen
cz init
提交代码时用约定式 commit:
bash
cz commit # 交互式选择 feat/fix/breaking 等
git push
cz bump --yes # 自动根据 commit 历史打 tag
git push --tags
也可以让 CI 在 merge 到 main 时自动 bump:
yaml
# .github/workflows/bump.yml
name: Bump version
on:
push:
branches: [main]
jobs:
bump:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }} # 需要写权限
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install commitizen
- name: 检查是否需要 bump
id: check
run: |
if cz bump --dry-run; then
echo "should_bump=true" >> $GITHUB_OUTPUT
else
echo "should_bump=false" >> $GITHUB_OUTPUT
fi
- name: 执行 bump
if: steps.check.outputs.should_bump == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
cz bump --yes --changelog
git push
git push --tags
5.4 GitHub Release 自动创建
yaml
- name: 创建 GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.whl
dist/*.tar.gz
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') }}
六、自动发布到 DockerHub
6.1 基础 Docker 镜像构建
dockerfile
# Dockerfile
FROM python:3.11-slim AS builder
WORKDIR /app
# 缓存层:先复制依赖文件
COPY pyproject.toml ./
RUN pip install --no-cache-dir build && python -m build
# 运行时镜像
FROM python:3.11-slim
WORKDIR /app
# 安装 uv 提速(替代 pip 的 10~100 倍)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY --from=builder /app/dist/*.whl /tmp/
RUN uv pip install --system /tmp/*.whl && rm /tmp/*.whl
# 创建非 root 用户
RUN useradd -m -u 1000 app && chown -R app:app /app
USER app
COPY --chown=app:app src/ /app/src/
ENTRYPOINT ["python", "-m", "mypkg"]
6.2 多架构构建(amd64 + arm64)
docker/build-push-action 配合 QEMU 实现多架构:
yaml
# .github/workflows/docker.yml
name: Docker
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 设置 QEMU
uses: docker/setup-qemu-action@v3
- name: 设置 Buildx
uses: docker/setup-buildx-action@v3
- name: 登录 DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 提取版本号作为 tag
id: meta
uses: docker/metadata-action@v5
with:
images: myorg/mypkg
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ !startsWith(github.ref, 'refs/tags/v0') }}
- name: 构建并推送
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
6.3 关键技巧
(1) GitHub Actions 缓存(GHA cache)
cache-from: type=gha 利用 Actions 自带缓存机制加速构建,第二次构建时间能从 5 分钟降到 30 秒。
(2) 安全扫描(Trivy)
yaml
- name: Trivy 漏洞扫描
uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/mypkg:${{ steps.meta.outputs.version }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1 # 发现 CRITICAL/HIGH 直接 fail
- name: 上传 SARIF 到 GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
(3) 镜像签名(Cosign)
yaml
- name: 安装 Cosign
uses: sigstore/cosign-installer@v3
- name: 签名镜像(keyless)
env:
COSIGN_EXPERIMENTAL: 1
run: |
cosign sign --yes myorg/mypkg@${{ steps.meta.outputs.digest }}
签名后用户在拉取镜像时可验证其来自可信源,防供应链攻击。
七、依赖安全扫描与 Dependabot
7.1 pip-audit 检查已知漏洞
yaml
- name: 安装 pip-audit
run: pip install pip-audit
- name: 扫描依赖漏洞
run: pip-audit --strict --require-hashes
# 或扫描 lock 文件
# pip-audit -r requirements.txt
# pip-audit --requirement poetry.lock --format json
pip-audit 使用 PyPI 官方维护的 OSV 数据库,覆盖 20 万+ 已知 Python 包漏洞。
7.2 Dependabot 自动升级
在 .github/dependabot.yml 中配置:
yaml
version: 2
updates:
# Python 依赖
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly" # 每周检查
day: "monday"
time: "09:00"
timezone: "Asia/Shanghai"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"
include: "scope"
labels:
- "dependencies"
- "python"
groups:
# 把 patch 升级合并为一个 PR
patch:
update-types: ["patch"]
dev-dependencies:
dependency-type: "development"
# GitHub Actions 版本
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels: ["ci"]
# Docker 基础镜像
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "monthly"
labels: ["docker"]
Dependabot 会自动检测过期依赖,开 PR 升级版本,配合 CI 自动跑测试,绿勾即可 merge。
7.3 CodeQL 代码静态安全扫描
yaml
# .github/workflows/codeql.yml
name: CodeQL
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 0 * * 1"
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
strategy:
matrix:
language: [python]
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- uses: github/codeql-action/analyze@v3
八、用 Reusable Workflow 复用流水线
当组织内有多个 Python 仓库,每个都复制一份 CI YAML 维护成本极高。Reusable Workflow 让你集中维护一份模板,各仓库调用即可:
8.1 定义 Reusable Workflow
在 .github/workflows/python-ci.yml(位于模板仓库如 .github 仓库):
yaml
name: Reusable Python CI
on:
workflow_call:
inputs:
python-versions:
type: string
default: '["3.9", "3.10", "3.11", "3.12"]'
run-coverage:
type: boolean
default: true
secrets:
CODECOV_TOKEN:
required: false
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ${{ fromJSON(inputs.python-versions) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: pip install -e ".[dev]"
- run: pytest ${{ inputs.run-coverage && '--cov=src --cov-report=xml' || '' }}
- if: inputs.run-coverage && matrix.python-version == '3.11'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
8.2 调用方仓库使用
业务仓库只需极简 workflow:
yaml
name: CI
on: [push, pull_request]
jobs:
ci:
uses: myorg/.github/.github/workflows/python-ci.yml@v1
with:
python-versions: '["3.10", "3.11", "3.12"]'
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
九、实战:把所有流水线串起来
最终的 .github/workflows/ 目录结构:
.github/
├── workflows/
│ ├── ci.yml # 主 CI:lint + test + build
│ ├── release.yml # tag 触发:发 PyPI + GitHub Release
│ ├── docker.yml # tag 触发:构建多架构镜像
│ ├── codeql.yml # 每周定时代码安全扫描
│ └── bump.yml # main 触发:自动 bump 版本
└── dependabot.yml # 依赖自动升级
完整 release.yml(自动发布到 PyPI + DockerHub)
yaml
name: Release
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install build
- run: python -m build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish-pypi:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- uses: pypa/gh-action-pypi-publish@release/v1
publish-docker:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: myorg/mypkg
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
github-release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- name: 生成 changelog
id: changelog
run: |
pip install git-cliff
git-cliff --latest --strip header -o CHANGELOG.md
echo "body<<EOF" >> $GITHUB_OUTPUT
cat CHANGELOG.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- uses: softprops/action-gh-release@v2
with:
files: |
dist/*.whl
dist/*.tar.gz
body: ${{ steps.changelog.outputs.body }}
generate_release_notes: true
打 tag 触发完整流水线:
bash
git tag v1.2.3
git push origin v1.2.3
# 5 分钟内完成:PyPI 发布 + DockerHub 多架构镜像 + GitHub Release(含 changelog)
十、CI/CD 最佳实践 Checklist
把流水线当作代码来 review,下面这些原则能显著提升质量:
10.1 速度
- ✅ 矩阵测试只覆盖关键组合(公开仓库除外)
- ✅ 启用 pip / venv 缓存,构建时间砍半
- ✅ 用
concurrency取消过时的 run - ✅ 把慢测试用
pytest.mark.slow分组,PR 默认不跑 - ✅ Docker 用 GHA cache 或 BuildKit inline cache
10.2 可靠性
- ✅ 所有 Action pin 到具体版本(
@v4→@v4.1.2),避免供应链攻击 - ✅ 用
pin@hash形式固定第三方 Action(最安全) - ✅
fail-fast: false让单个组合失败不掩盖其他问题 - ✅ CI 失败时上传 artifact(log、coverage、screenshot)
10.3 安全
- ✅ secrets 全部用 GitHub Secrets,绝不硬编码
- ✅ 用 OIDC Trusted Publishers 替代 PyPI Token
- ✅ 启用 Trivy + CodeQL + pip-audit 三重扫描
- ✅ 用
environment+ required reviewers 保护部署 Job - ✅
permissions: contents: read默认最小权限
10.4 可观测性
- ✅ 上传 coverage 到 Codecov,PR 显示覆盖率变化
- ✅ 失败时 Job Summary 给出清晰错误
- ✅ Slack / 钉钉 / 飞书 webhook 通知(
dawidd6/action-send-mail等)
10.5 可维护性
- ✅ 用 Reusable Workflow 集中维护
- ✅ 复杂 workflow 拆分为多个文件
- ✅ 为关键 Job 加
name,让 UI 易读 - ✅ workflow 文件加注释说明触发场景
十一、常见问题排查
Q1:CI 本地能跑,GitHub 上失败
最常见原因是 路径大小写 (macOS 文件系统不区分大小写,Linux 区分)和 行尾符(Windows CRLF vs Linux LF)。修复:
yaml
- name: 配置 Git
run: git config --global core.autocrlf false
仓库根目录加 .gitattributes:
* text=auto eol=lf
*.ps1 text eol=crlf
*.bat text eol=crlf
Q2:矩阵测试中 Windows 慢得多
Windows Runner 启动比 Linux 慢 2-3 倍。若非必要,可以只在 Linux 跑完整矩阵,Windows/macOS 各跑一个 Python 版本。
Q3:私有仓库额度用尽
- 切换部分 Job 到 self-hosted runner
- 用
paths-ignore跳过文档变更 - 用 Reusable Workflow 复用
- 在 PR 上加
[skip ci]跳过实验性 push
Q4:缓存命中率为 0
检查 cache-dependency-path 是否指向正确的 lock 文件。pyproject.toml 改动会让 hash 变化,这是预期行为。
Q5:OIDC 发布 PyPI 失败 "403 Forbidden"
确认 PyPI 项目设置中 Trusted Publisher 配置的 workflow 文件名与实际一致(区分大小写)。首次发布仍需要手动通过 API Token 或 web 上传。
Q6:Codecov 显示覆盖率错误下降
可能是 branch coverage 未启用,或 source 路径配置错误。在 pyproject.toml 中显式指定:
toml
[tool.coverage.run]
source = ["src.mypkg"] # 而非 ["src"]
Q7:Docker 多架构构建 OOM
arm64 通过 QEMU 模拟,内存消耗大。给 Job 加 continue-on-error: true,或在自托管 arm64 Runner 上构建。
十二、总结
CI/CD 不是"高级技巧",而是现代软件工程的基础设施。本文从 GitHub Actions 的 workflow 语法出发,覆盖了 Python 项目从测试、覆盖率、Lint、类型检查,到自动发布 PyPI / DockerHub、依赖漏洞扫描、自动版本管理的完整链路。
核心要点回顾:
- CI 的本质:把"应该自动化的"写成代码进版本库,让流程可 review、可回滚
- 缓存与矩阵:能让一个完整 CI 从 15 分钟降到 3 分钟
- OIDC Trusted Publisher:放弃长期 token,拥抱短期签名,安全提升一个数量级
- 多架构镜像:QEMU + buildx 一行配置搞定 amd64 + arm64
- Dependabot + CodeQL + pip-audit + Trivy:四道安全防线,让漏洞无所遁形
下一篇将进入 Conda + pip 混合包管理 的世界------讲透两者底层的本质差异、environment.yml 与 requirements.txt 协同策略,以及 pip-tools / poetry / uv 三大锁文件方案的横向对比,敬请关注。
📌 如果本文对你有帮助,欢迎点赞收藏,关注博主获取更多 Python + AI 实战教程!
📕个人领域 :Linux/C++/java/AI🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
