知识广场
按学科筛选:计算机科学 / 软件工程 / 构建与CI
«计算机科学 / 软件工程 / 构建与CI» 分类下共 1 篇帖子
## 起因 库需要在 Python 3.10/3.11/3.12 × Linux/Mac/Windows × 9 个组合上测试。 之前老 CI 写 9 个 job 重复代码。GitHub Actions 的 matrix 一行配齐。 ## 1. matrix build ```yaml # .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 加速 ```yaml - 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: ```yaml - 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 ```yaml 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 用: ```yaml # .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 }} ``` 调用方: ```yaml # 某 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: ```yaml # 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 ``` ```yaml # 调用 - uses: ./my-action with: python-version: '3.12' - run: uv run pytest ``` 放在仓库本地 (`./.github/actions/setup`) 或独立 repo (`my-org/setup-action@v1`)。 ## 6. 条件运行 ```yaml - 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. 路径 / 分支过滤 ```yaml on: push: paths: - 'backend/**' - '.github/workflows/backend.yml' branches: - main - 'release/**' ``` monorepo 里只改前端时不跑后端 CI。 ## 8. concurrency 防同一分支同时多 run ```yaml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true ``` PR 连续 push 时,老的 in-progress run 被 cancel,只跑最新。 省 CI 资源。 ## 9. secrets / env ```yaml 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: ```yaml jobs: deploy-prod: environment: production # 需要审批员点击 approve 才跑 steps: ... ``` ## 10. 自托管 runner(cost / hardware 控制) ```yaml runs-on: [self-hosted, linux, gpu] ``` 自己机器装 GitHub Actions runner agent → label 标记 → workflow 用 label 选择。免费层时间紧 / 需 GPU / 需大内存时省钱。 ## 11. 经验:调试 workflow ```bash # 本地跑 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 会话调试: ```yaml - 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 - 用 `concurrency` cancel 老 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 行 ## 踩过的坑 1. **cache key 不带 lockfile hash**:依赖变了 cache 还用老的, bug 隐蔽。永远 `hashFiles('package-lock.json')` 之类。 2. **secrets 在 PR 不可见**:跨 fork PR 默认不传 secrets(防泄露)。 要 dependabot / fork PR 跑需要 deploy → 用 `workflow_run` triggered workflow 在 base repo 跑。 3. **windows runner 路径分隔**:脚本里 hardcode `/` 在 Windows 上 失败。用 cross-platform shell(bash 在 Windows runner 也有)+ `\` `/` 都用 `path.join`。 4. **timeout 默认 6 小时**:偶尔 hang 的 job 把 minutes 烧光。 `timeout-minutes: 30` 给每个 job 设上限。 5. **actions 升级 v3 → v4 breaking**:node 16 退役大批 actions 强制 升 node 20。看 GitHub deprecation notice + dependabot for actions 自动 PR 升级。