GitHub Actions 矩阵构建避坑指南:多平台多版本并行测试的 5 个真实问题

GitHub Actions 矩阵构建
GitHub Actions 矩阵构建

解决什么问题

GitHub Actions 的 matrix 策略让你可以用一条 workflow 跑遍 Python 3.9/3.10/3.11/3.12 四个版本 + macOS/Linux/Windows 三个系统。但真正用过的人都知道:matrix 跑起来很爽,调试起来很痛。本文记录我在生产环境中遇到的真实问题:环境变量传不进、依赖缓存命中率只有 30%、超时时间设置无效、删除矩阵维度后 CI 反而更慢。

一、基础矩阵配置

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-22.04, macos-14, windows-2022]
        python-version: ["3.9", "3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -r requirements.txt
      - run: pytest tests/

fail-fast: false 是关键——默认 true 导致一个版本失败就取消所有 job,调试时极不友好。

二、问题 1:矩阵 job 收不到环境变量

env 全局定义的变量,矩阵 job 里读不到:

env:
  NODE_ENV: production
  API_URL: https://api.example.com

jobs:
  test:
    runs-on: ubuntu-22.04
    # ❌ 这里 ${{ env.API_URL }} 是空字符串
    steps:
      - run: echo ${{ env.API_URL }}

正确做法是在 job 或 step 级别单独声明:

jobs:
  test:
    runs-on: ubuntu-22.04
    env:
      API_URL: https://api.example.com
    steps:
      - run: echo ${{ env.API_URL }}
      # ✅ 输出 https://api.example.com

踩坑经历:项目从 Travis CI 迁移过来,习惯在全局写 env,GitHub Actions 的 job 级别作用域完全不一样。

三、问题 2:依赖缓存命中率只有 30%

用了 actions/cache 但命中率惨淡,看日志发现每次都 generate 新 key:

steps:
  - uses: actions/cache@v4
    with:
      path: ~/.cache/pip
      key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}

问题是 hashFiles 匹配的是 checkout 之后的文件,但 setup-python 默认不指定 architecture:

# 修复:加入 python 版本到 key
key: pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}

restore-keys: |
  pip-${{ runner.os }}-py${{ matrix.python-version }}-

这样 4 个 Python 版本各自独立缓存,不再互相覆盖。实测从 30% 命中率提升到 85%+。

四、问题 3:macOS _runner 持续时间莫名很长

macOS runner 的启动时间比 Ubuntu 长 3-5 倍,加上 Homebrew 每次更新 brew update,job 时间从 2 分钟变成 12 分钟。

jobs:
  test:
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - name: Skip brew update on macOS
        run: |
          # 避免每次更新 Homebrew 索引,节省 3-5 分钟
          mkdir -p $HOME/.cache/homebrew
          touch $HOME/.cache/homebrew/initial_install_complete
        if: matrix.os == 'macos-14'
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
          cache-dependency-path: requirements.txt
        if: matrix.os != 'macos-14'

对于 macOS,直接用 setup-python 的内置 pip 缓存,省去 actions/cache 的配置复杂度,命中率反而更高。

五、问题 4:删除某个矩阵维度后 CI 更慢了

删掉了 ["3.9", "3.10"] 两个旧版本,job 数量从 12 个减少到 6 个,但总耗时反而增加了 40%。

根因是 GitHub Actions 的免费套餐对并发 job 有队列限制(上限 20 个并行),job 数量减少后没有更快入队,反而因为剩余 job 独占 runner 资源导致调度延迟。

# 查看当前并发状态
# Settings → Actions → Parallelism 确认队列配置

# 如果要控制总并发数,用 concurrency 组
jobs:
  test:
    runs-on: ${{ matrix.os }}
    concurrency:
      group: ${{ github.workflow }}-${{ matrix.os }}-${{ matrix.python-version }}
      cancel-in-progress: true

concurrency 限制最大并发 job 数,比单纯删矩阵维度更可控。

六、问题 5:Windows 路径分隔符导致 shell 脚本失效

steps:
  - name: Run install script
    run: ./scripts/install.sh
    shell: bash
    # ❌ Windows 上 bash 不是默认 shell,需要手动指定
    # 默认是 PowerShell,./ 路径在 PowerShell 里行为不同

Windows runner 的默认 shell 是 PowerShell 或 cmd,跨平台脚本必须显式指定:

steps:
  - name: Run install script
    run: ./scripts/install.sh
    shell: bash
    # ✅ 所有平台统一用 bash,避免路径问题

实战经验:Windows 上跑 Python 项目,路径最好用 pathlibos.path.join,而不是硬编码 /

七、完整示例:Python 多版本测试工作流

name: Python Matrix Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-22.04, macos-14, windows-2022]
        python-version: ["3.9", "3.10", "3.11", "3.12"]
        exclude:
          # Python 3.9 不在 macOS ARM 上测试(无此版本)
          - os: macos-14
            python-version: "3.9"

    env:
      # job 级别环境变量,矩阵内所有 job 都能读到
      PYTHONUNBUFFERED: "1"
      PIP_DISABLE_PIP_VERSION_CHECK: "1"

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
          cache-dependency-path: requirements.txt

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest pytest-cov

      - name: Run tests
        run: pytest tests/ -v --cov=src

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        if: matrix.os == 'ubuntu-22.04'  # 只在 Linux 上传一次
        with:
          file: ./coverage.xml

exclude 用来跳过不兼容的版本组合,比在 step 里加 if 判断更干净。

现在你可以做什么

第一步:打开你的 .github/workflows/*.yml,检查 env 是不是写在 job 级别而不是全局级别。

第二步:在 setup-python 的 cache 配置里加入 python-version 到 key,确保不同版本缓存独立:key: pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}

第三步:给 Windows 平台的 steps 加上 shell: bash,避免跨平台脚本失效。

第四步:如果 job 总数超过 20 个,用 concurrency 控制最大并发数,而不是直接删矩阵维度。

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享