{T}

CI/CD

CI/CD 的目标是把重复、容易漏掉的工程动作交给自动化流程执行,例如格式检查、类型检查、测试和构建。它的核心价值不是"炫",而是减少人为疏漏,让每次提交都更可预测。

阅读提示

CI/CD 流程全景

图表渲染中…

最小 CI 流水线

一个 Python 项目最小可接受的 CI 流程至少包括:lint、type check、test、build。

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev]"

      - name: Lint
        run: ruff check .

      - name: Type check
        run: mypy src

      - name: Test
        run: pytest --cov=src --cov-report=term-missing

      - name: Build
        run: python -m build
本地和 CI 用同一套命令

流水线应该尽量复用本地同一套命令,避免本地一套、CI 一套,最后谁都不信谁。pip install -e ".[dev]" + ruff check . + mypy src + pytest + python -m build —— 这套命令本地和 CI 完全一致。

进阶流水线

多版本测试矩阵

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false  # 一个版本失败不影响其他版本继续
      matrix:
        python-version: ["3.12", "3.13"]
        os: [ubuntu-latest, macos-latest]  # 可选:跨操作系统

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: pip install -e ".[dev]"

      - name: Lint
        run: ruff check .

      - name: Type check
        run: mypy src

      - name: Test
        run: pytest --cov=src

      - name: Build
        run: python -m build
测试矩阵不要过度

初期先单版本(3.12),稳定后再扩展到 3.13。版本越多反馈越慢,不要一开始就把矩阵堆得像圣诞树。

分阶段流水线:质量 → 文档 → 发布

yaml
name: pipeline

on:
  push:
    branches: [main]
  pull_request:

jobs:
  # 阶段一:代码质量门禁
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -e ".[dev]"
      - run: ruff check .
      - run: mypy src
      - run: pytest --cov=src

  # 阶段二:文档构建(仅主分支)
  docs:
    runs-on: ubuntu-latest
    needs: quality
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run build
      - name: Deploy docs
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./docs/.vitepress/dist

  # 阶段三:发布(仅标签触发)
  publish:
    runs-on: ubuntu-latest
    needs: quality
    if: startsWith(github.ref, 'refs/tags/v')
    permissions:
      id-token: write  # Trusted Publishing
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install build
      - run: python -m build
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
图表渲染中…

CI 门禁配置

代码规范(Ruff)

toml
# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # Pyflakes
    "I",    # isort
    "N",    # pep8-naming
    "UP",   # pyupgrade
    "B",    # flake8-bugbear
    "SIM",  # flake8-simplify
    "RUF",  # Ruff-specific rules
]
ignore = ["E501"]  # 行长度由 formatter 处理

[tool.ruff.format]
quote-style = "double"

类型检查(mypy)

toml
# pyproject.toml
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
check_untyped_defs = true
no_implicit_optional = true
strict = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false  # 测试代码不需要严格注解

测试(pytest)

toml
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short --strict-markers"
markers = [
    "slow: 慢测试",
    "integration: 集成测试",
]

[tool.coverage.run]
source = ["src"]
omit = ["tests/*"]

[tool.coverage.report]
fail_under = 80
show_missing = true

分支保护规则

在 GitHub 仓库设置中配置:

  1. Require status checks to pass before merging:勾选所有 CI job
  2. Require branches to be up to date before merging:确保 PR 基于最新代码
  3. Require signed commits(可选):防止伪造提交
  4. Do not allow bypassing the above settings:管理员也不能绕过

实战场景

场景一:Python 包项目的完整 CI/CD

yaml
# .github/workflows/ci-cd.yml
name: CI/CD

on:
  push:
    branches: [main]
  pull_request:
  release:
    types: [published]

jobs:
  # --- CI 阶段 ---
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install ruff
      - run: ruff check .
      - run: ruff format --check .

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -e ".[dev]"
      - run: mypy src

  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.12", "3.13"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -e ".[dev]"
      - run: pytest --cov=src --cov-report=xml
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    needs: [lint, typecheck, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install build
      - run: python -m build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  # --- CD 阶段 ---
  publish:
    runs-on: ubuntu-latest
    needs: build
    if: github.event_name == 'release'
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1

场景二:安全审计自动化

yaml
  # 添加到 CI 流水线中
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pip-audit safety

      - name: Check dependencies for known vulnerabilities
        run: pip-audit

      - name: Safety check
        run: safety check --json

场景三:定时任务 — 依赖升级检测

yaml
# .github/workflows/dependency-update.yml
name: Dependency Update Check

on:
  schedule:
    - cron: '3 9 * * 1'  # 每周一早上 9:03(UTC)

jobs:
  check-updates:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Check for outdated dependencies
        run: |
          pip install pip-outdated
          pip list --outdated --format=json | python -c "
          import json, sys
          data = json.load(sys.stdin)
          for pkg in data:
              print(f'{pkg[\"name\"]}: {pkg[\"version\"]} → latest: {pkg[\"latest_version\"]}')
          "

      - name: Create issue if updates available
        uses: actions/github-script@v7
        with:
          script: |
            const output = process.env.UPDATE_OUTPUT;
            if (output) {
              github.rest.issues.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: '⚠️ 依赖更新提醒',
                body: output,
                labels: ['dependencies']
              });

部署策略

部署方式对比

图表渲染中…
策略停机时间复杂度资源需求适用场景
直接部署有(短)单份内部工具、低流量服务
蓝绿部署双份需要零停机的服务
金丝雀发布双份高流量、高风险变更

回滚机制

bash
# Python 包回滚:发布旧版本
twine upload dist/myapp-1.0.0-py3-none-any.whl  # 重新上传稳定版本

# Docker 部署回滚
docker-compose down
docker-compose up -d myapp:1.0.0  # 使用上一个稳定版本

# GitHub Pages 回滚
git revert HEAD  # 回退到上一个提交
git push origin main

# Kubernetes 回滚
kubectl rollout undo deployment/myapp
回滚不是附属品

回滚是发布设计的一部分。发布前就应该确定"上一版本可恢复"的能力,而不是事故发生时临时想办法。

常见失败与排查

失败类型常见原因排查步骤
Lint 失败格式不符合规范、未使用导入本地运行 ruff check .,确认配置一致
Type check 失败缺少注解、类型不匹配先检查是新代码引入还是旧债,优先修公共接口
Test 失败断言失败、环境差异先本地复现,检查是否依赖时间/网络/文件系统
Build 失败缺文件、依赖声明错误本地运行 python -m build,检查 pyproject.toml
依赖安装失败版本冲突、网络问题检查 requires-pythonpip install 日志
发布失败权限问题、包名冲突检查 PyPI Token、确认包名未被占用

排查流程

图表渲染中…

CI/CD 对比

对比维度持续集成(CI)持续交付/部署(CD)
核心目标自动验证改动质量自动或半自动发布结果
典型动作lint、type check、test、build发布包、部署站点、上线服务
触发时机每次提交、PR主分支、标签、人工审批
失败后果阻止低质量代码进入阻止错误版本被发布
对比维度本地验证CI 验证
反馈速度较慢
环境一致性可能受个人影响更统一
调试便利性
结论两者使用同一套命令互相印证

常见陷阱

陷阱现象原因解决方案
CI 只跑测试不跑构建发布时包打不出来忽略了 build 步骤CI 流水线必须包含 build
本地和 CI 用不同命令CI 失败但本地正常环境不一致统一使用 pyproject.toml 定义
测试矩阵过度CI 跑 20 分钟验证太多 Python 版本先单版本,稳定后扩展
缺少分支保护坏代码直接合并没设置 GitHub 保护规则配置 Require status checks
回滚无路径出事故时无从回退发布前没设计回滚方案每次发布保留上一个版本
硬编码密钥CI 配置暴露敏感信息直接写在 YAML 中使用 GitHub Secrets
CI 是黑盒失败后无从排查神秘脚本没人能复现本地命令和 CI 命令一致

最佳实践速查表

场景推荐做法避免
新项目 CIlint + type + test + build 四步只跑 pytest
命令一致性本地和 CI 用同一套命令本地一套 CI 一套
测试矩阵先 3.12 单版本 → 稳定后加 3.13一开始就堆5个版本
分支保护设置 Required status checks跳过 CI 直接合并
发布流程CI → 构建 → TestPyPI → PyPI直接发布
回滚设计每次发布保留上一版本无回滚能力
密钥管理GitHub Secrets硬编码在 YAML 中
定时任务每周检查依赖更新永远不升级
制品保存upload-artifact 保留构建产物每次重新构建

术语表

术语英文定义
CIContinuous Integration持续集成:每次提交自动验证代码质量
CDContinuous Delivery/Deployment持续交付/部署:验证通过后自动发布
流水线PipelineCI/CD 中一系列自动执行的任务链
门禁Gate/Quality Gate必须通过的检查条件,未通过则阻断后续步骤
测试矩阵Test Matrix多版本/多操作系统组合的测试策略
制品Artifact构建产物(wheel、Docker 镜像、静态站点)
Trusted PublishingPyPI 的 OIDC 认证发布机制,无需手动管理 Token
蓝绿部署Blue-Green Deployment两套环境切换,实现零停机部署
金丝雀发布Canary Release先小流量验证新版本,再逐步放量
回滚Rollback将服务恢复到上一个稳定版本
GitHub ActionsGitHub 提供的 CI/CD 平台
SecretsCI 中存储敏感信息的加密变量

延伸阅读

平台文档

工具文档

推荐阅读