起因
库需要在 Python 3.10/3.11/3.12 × Linux/Mac/Windows × 9 个组合上测试。
之前老 CI 写 9 个 job 重复代码。GitHub Actions 的 matrix 一行配齐。
1. matrix build
# .github/workflows/test.yml
name: test
on:
push:
branches: [main]
pull_request:
jobs:
test:
strategy:
fail-fast: false
matrix:
python: ['3.10', '3.11', '3.12']
os: [ubuntu-latest, macos-latest, windows-latest]
exclude:
- os: windows-latest
python: '3.10' # 跳某些组合
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: 'pip'
- run: pip install -e .[dev]
- run: pytest -xvs
fail-fast: false:一个 job 失败不取消其它(看完所有再 fail)。
8 个 job 并行跑(GitHub 免费 plan 公开 repo 20 并发)。
2. cache 加速
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.cache/pypoetry
.venv
key: ${{ runner.os }}-py${{ matrix.python }}-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python }}-
key 包含 lockfile hash → lockfile 没变 → cache 命中。
restore-keys fallback:完全不命中时拿前缀匹配最新的(部分有比没有强)。
cache 命中跳过 pip install → CI 时间从 2 分钟 → 20 秒。
Node:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
setup-node action 内置 npm/yarn/pnpm cache,不需要单独 actions/cache。
3. matrix + variables
strategy:
matrix:
include:
- { name: 'python 3.10 / no extras', python: '3.10', extras: '' }
- { name: 'python 3.12 / all extras', python: '3.12', extras: 'all' }
- { name: 'python 3.12 / pgsql', python: '3.12', extras: 'postgres' }
runs-on: ubuntu-latest
steps:
- run: pip install -e .[${{ matrix.extras }}]
- run: pytest
include 比 python: × extras: 笛卡尔积更精细,只跑你 list 的组合。
4. reusable workflows
公共逻辑抽到一个 yml 给多 repo 用:
# .github/workflows/python-test.yml (reusable)
name: python-test
on:
workflow_call:
inputs:
python-version:
type: string
default: '3.12'
coverage:
type: boolean
default: false
secrets:
CODECOV_TOKEN:
required: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- run: pip install -e .[dev]
- run: pytest ${{ inputs.coverage && '--cov' || '' }}
- if: inputs.coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
调用方:
# 某 repo .github/workflows/ci.yml
jobs:
test:
uses: my-org/.github/.github/workflows/python-test.yml@main
with:
python-version: '3.12'
coverage: true
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
更新 reusable yml → 所有调用方下次 run 自动用新版(无需逐个 repo
改)。
5. composite actions(细粒度复用)
复用几个 step 而不是整个 workflow:
# my-action/action.yml
name: 'Setup project'
description: 'Install uv + Python + sync deps'
inputs:
python-version:
default: '3.12'
runs:
using: composite
steps:
- uses: astral-sh/setup-uv@v3
with:
version: 'latest'
enable-cache: true
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- run: uv sync --frozen
shell: bash
# 调用
- uses: ./my-action
with:
python-version: '3.12'
- run: uv run pytest
放在仓库本地 (./.github/actions/setup) 或独立 repo
(my-org/setup-action@v1)。
6. 条件运行
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: ./scripts/deploy.sh
- if: matrix.os == 'ubuntu-latest'
run: ./scripts/coverage.sh
- if: failure()
run: ./scripts/notify-slack.sh
success() / failure() / always() / cancelled() 是 step 级条件。
7. 路径 / 分支过滤
on:
push:
paths:
- 'backend/**'
- '.github/workflows/backend.yml'
branches:
- main
- 'release/**'
monorepo 里只改前端时不跑后端 CI。
8. concurrency 防同一分支同时多 run
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
PR 连续 push 时,老的 in-progress run 被 cancel,只跑最新。
省 CI 资源。
9. secrets / env
env:
NODE_ENV: production
API_BASE_URL: https://api.example.com
jobs:
deploy:
env:
EXTRA: value
steps:
- env:
STEP_LOCAL: x
run: echo "$EXTRA $STEP_LOCAL ${{ secrets.AWS_KEY }}"
secrets 在 repo settings → Secrets and variables → Actions 配。
环境(environment)配的可加 approval gate:
jobs:
deploy-prod:
environment: production # 需要审批员点击 approve 才跑
steps: ...
10. 自托管 runner(cost / hardware 控制)
runs-on: [self-hosted, linux, gpu]
自己机器装 GitHub Actions runner agent → label 标记 → workflow 用 label
选择。免费层时间紧 / 需 GPU / 需大内存时省钱。
11. 经验:调试 workflow
# 本地跑 workflow(用 act)
brew install act
act -j test # 跑 test job
act pull_request # 模拟 pr 事件
act 用 Docker 模拟 GitHub Actions 环境。不 100% 一致(缺一些 GitHub
service),但能本地快速迭代。
或者用 tmate 在 GitHub runner 里开 SSH 会话调试:
- name: SSH into runner if test fails
if: failure()
uses: mxschmitt/action-tmate@v3
with:
detached: true
limit-access-to-actor: true
step 失败时 GitHub 给你 SSH URL,进 runner 看现场。强烈推荐紧急调试用。
12. cost 控制
- GitHub 免费层公开 repo 无限 minutes;私有 repo 每月 2000 minutes
- 用
concurrencycancel 老 run - 选小机器(默认 ubuntu-latest 2-core;linux 私有可以选
ubuntu-24.04-arm
64 cores 但贵 16x) - 把 release / nightly build 放 self-hosted
效果
我们一个开源库 CI:
- matrix build 9 组合并行,总时长 4 分钟(顺序跑要 30 分钟+)
- cache 让 build 时间 80% 来自实际跑测试,不是装依赖
- reusable workflow 让 6 个相关 repo 共享配置,改一处生效全部
- composite action 把"setup uv + sync"封装,每 yml 少 10 行
踩过的坑
-
cache key 不带 lockfile hash:依赖变了 cache 还用老的,
bug 隐蔽。永远hashFiles('package-lock.json')之类。 -
secrets 在 PR 不可见:跨 fork PR 默认不传 secrets(防泄露)。
要 dependabot / fork PR 跑需要 deploy → 用workflow_run
triggered workflow 在 base repo 跑。 -
windows runner 路径分隔:脚本里 hardcode
/在 Windows 上
失败。用 cross-platform shell(bash 在 Windows runner 也有)+
\/都用path.join。 -
timeout 默认 6 小时:偶尔 hang 的 job 把 minutes 烧光。
timeout-minutes: 30给每个 job 设上限。 -
actions 升级 v3 → v4 breaking:node 16 退役大批 actions 强制
升 node 20。看 GitHub deprecation notice + dependabot for actions
自动 PR 升级。
登录后参与评论。