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/mainrefs/tags/v1.0.0
github.ref_type branchtag
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.ymlrequirements.txt 协同策略,以及 pip-tools / poetry / uv 三大锁文件方案的横向对比,敬请关注。


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

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

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

相关推荐
西瓜太郎12341 小时前
request_id 如何串起请求、路由、计费和日志
后端·python
troy1281 小时前
Python 基础语法(一):变量、数据类型与运算符
java·服务器·python
dong_junshuai2 小时前
每天一个开源项目#97 LLM Wiki:把RAG结果变成可维护知识资产
开源·llm·github
用户298698530142 小时前
Python 将 Word 文档转换为图片的实践指南
后端·python·api
弈栈录2 小时前
LangChain Agent 实战:快速构建可调用工具的智能体
python·架构
Java后端的Ai之路2 小时前
一文搞懂 GitHub Actions-CICD
开发语言·大模型·github·cicd·action
宸津-代码粉碎机2 小时前
Spring AI 高危CVE漏洞深度复盘|生产禁跑版本汇总+临时防御+修复方案
java·大数据·人工智能·python·spring
承渊政道3 小时前
Python IDLE鸿蒙PC适配全记录:用 ArkUI 重建编辑、运行、Shell 与基础调试闭环
python·microsoft·harmonyos·鸿蒙系统·pc端
zjh9005303 小时前
Kotlin黑科技:空安全与Java兼容性深度解析
python