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

🚀 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 的最大优势在于:

  1. 与 GitHub 深度集成:PR 检查、required status、CODEOWNERS、issue trigger 均原生支持
  2. Marketplace 生态:3 万+ 官方与社区 Action,常见需求(如 docker build、gh release)几乎零代码
  3. 矩阵构建:原生支持多 OS / 多运行时并行
  4. 自托管 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

  1. 注册 PyPI 账号
  2. Account settings → API tokens → Add API token
  3. Scope 选择 "Entire account"(首次发布)或指定项目
  4. 复制 pypi-xxxxxxxxxxxxxxxx 格式的 token
  5. 在仓库 Settings → Secrets → Actions 中添加 PYPI_API_TOKEN

5.2 使用 Trusted Publishers(推荐)

2024 年 PyPI 推出 Trusted Publishers(OIDC),无需 API Token,安全性更高:

  1. 在 PyPI 项目设置 → Publishing → 添加 GitHub 仓库(owner/repo + workflow 文件名)
  2. 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、依赖漏洞扫描、自动版本管理的完整链路。

核心要点回顾:

  1. CI 的本质:把"应该自动化的"写成代码进版本库,让流程可 review、可回滚
  2. 缓存与矩阵:能让一个完整 CI 从 15 分钟降到 3 分钟
  3. OIDC Trusted Publisher:放弃长期 token,拥抱短期签名,安全提升一个数量级
  4. 多架构镜像:QEMU + buildx 一行配置搞定 amd64 + arm64
  5. Dependabot + CodeQL + pip-audit + Trivy:四道安全防线,让漏洞无所遁形

下一篇将进入 Conda + pip 混合包管理 的世界------讲透两者底层的本质差异、environment.yml 与 requirements.txt 协同策略,以及 pip-tools / poetry / uv 三大锁文件方案的横向对比,敬请关注。


📌 如果本文对你有帮助,欢迎点赞收藏,关注博主获取更多 Python + AI 实战教程!
📕个人领域 :Linux/C++/java/AI

🚀 个人主页 :有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
miofly27 分钟前
Aleph Alpha 开源 78B 参数 MoE 模型 Kolibri
开源·github
言乐61 小时前
HTML视频审核模型
python·django·virtualenv·pygame·tornado
JackSparrow4141 小时前
和AI一起将全部CSDN博文迁移到个人博客站
人工智能·程序人生·ai·github·cloudflare·astro·静态博客
言乐62 小时前
Python根据无法识别搜索词找出可能输入内容模型
开发语言·python·django·virtualenv·pygame
yl45303 小时前
硫酸泄露处理生产商怎么选才够专业
大数据·人工智能·python
笨笨饿3 小时前
140_AI新手村MCP与Skills是干嘛的
开发语言·人工智能·python·stm32·单片机·嵌入式硬件·物联网
for_ever_love__4 小时前
机器学习入门——手写线性回归与梯度下降
人工智能·python·学习·机器学习·线性回归
打工仔折腾 AI4 小时前
从 Demo 到生产级 Agent:8 个关键设计机制与 Python 实现拆解
java·jvm·人工智能·后端·python·langchain·ai agent 实战
I Am a robert girl4 小时前
当传感器学会“说谎“:拆解可靠性门控的稀疏惯性动捕融合
python·姿态估计·传感器融合·惯性动捕·imu传感器·可靠性门控·可穿戴计算
李航19834 小时前
AI定制柜建模,需要详细的建模规范和标准流程
人工智能·python·计算机视觉·ai·ai编程