Prhub

#2584 fix(ci): honor nightly fast-fail bypass

原始 PR 作者 guapisolo 合并时间 2026-08-18 03:31 文件变更 2 提交数 1 评论 0 代码增减 +8 / -7

执行摘要

修复 nightly 快速失败绕过被 Stage A 设置失败吞掉

PR body 指出 Nightly run 32041342404 在 policy 输出 bypass_fastfail=true 的情况下仍跳过了 docker-build,原因是 stage-a-cpu 在设置 astral-sh/setup-uv@v5 时失败,导致所有 GPU 阶段被跳过。根因是 jobs.docker-build.if 与 GPU job 门控把 bypass_fastfail 嵌套在 needs.stage-a-cpu.result == 'failure' 之后,而可复用矩阵的 setup 失败在 caller 门控处并不满足精确的 'failure' 状态,使 bypass 完全失效。

值得 CI 维护者快速精读,是 GitHub Actions 门控条件设计的典型反面教材与修正样例:可复用工作流的 setup 阶段失败不会以普通 job 'failure' 呈现,门控条件应避免对精确状态值耦合,策略输出应作为独立信号消费。核心设计决策是「policy 输出与 job 状态解耦」,可推广到其他 CI 门控场景。

讨论亮点

本 PR 没有实质性的 reviewer 讨论:claude[bot] 仅自动提示本仓库配置了手动 review,yueming-yuan 直接 APPROVED 且未留文字。作者在 PR body 的 Review Focus 中自查了两个验证点:

  • 确认 bypass 应覆盖 Stage A 的每一种非成功结果,但不能绕过 cancellation、policy 失败、镜像失败或阶段选择守卫。
  • 确认 test_run_suite.py 的 seam 断言能拒绝未来重新引入精确 Stage A 'failure' 依赖。
    这两点恰好构成修复的语义边界,可视为作者自证的设计意图。

实现拆解

  1. 定位根因:在 .github/workflows/pr-test.yml 中,docker-build 与五个 GPU 阶段(stage-b-2-gpu-h200、stage-c-8-gpu-h100、stage-c-8-gpu-h200、stage-c-4-gpu-h200、stage-c-2-gpu-h200)的门控条件都写成 (needs.stage-a-cpu.result == 'failure' && needs.resolve-ci-policy.outputs.bypass_fastfail == 'true')。可复用工作流 setup 阶段失败时,caller 侧聚合结果并非精确的 'failure',于是 bypass 分支永不触发。
  2. 放宽门控条件:将上述六处条件统一简化为独立的 needs.resolve-ci-policy.outputs.bypass_fastfail == 'true',使 bypass 成为与 Stage A 成功并列的平等放行路径;Stage A 无论失败、跳过还是其他非成功状态,只要 policy 明确要求快速失败绕过,下游镜像构建与 GPU 测试就继续推进。
  3. 收紧测试 seam:在 tests/ci/test/test_run_suite.py 中,test_docker_build_waits_for_cpu_gate_and_preserves_bypass 把 needs.stage-a-cpu.result == 'failure' 的断言从「必须存在」反转为「不得存在」;test_gpu_gates_consume_shared_bypass_output 对五个 GPU 门控整体追加同样的反向断言,形成防止回归的静态护栏。
  4. 保留原有守卫:cancellation(always() && !cancelled())、PR 关闭(github.event.action != 'closed')、policy 自身成功、镜像构建成功、以及 skipped_stages 阶段选择逻辑均原样保留,bypass 只放宽 Stage A 相关路径,不扩大其他守卫的语义。
  5. 验证配套:作者执行 pytest -q tests/ci/test/test_run_suite.py tests/ci/test/test_ci_policy.py 得到 103 passed,并通过 git diff --check 与含 YAML 校验的 commit hooks。
文件 模块 状态 重要度
.github/workflows/pr-test.yml CI 工作流 modified 3.86
tests/ci/test/test_run_suite.py CI 测试 modified 3.35

关键符号

test_docker_build_waits_for_cpu_gate_and_preserves_bypass test_gpu_gates_consume_shared_bypass_output

关键源码片段

.github/workflows/pr-test.yml infrastructure

修复主体:docker-build 与五个 GPU 阶段的门控条件从「Stage A 失败且 bypass」的嵌套结构改为「bypass_fastfail=true」独立放行,是本次 bugfix 的核心变更文件。

# docker-build 门控:Stage A 无论成败,只要 policy 明确输出 bypass_fastfail=true
# 就继续构建镜像;取消、PR 关闭、policy 自身失败等守卫仍然前置生效。
docker-build:
  needs: [resolve-ci-policy, stage-a-cpu]
  if: |
    always() && !cancelled() &&
    github.event.action != 'closed' &&
    needs.resolve-ci-policy.result == 'success' &&
    (needs.stage-a-cpu.result == 'success' ||
     needs.resolve-ci-policy.outputs.bypass_fastfail == 'true')
  uses: ./.github/workflows/_build-pr-ci-image.yml
  secrets: inherit# 五个 GPU 阶段共用同一组守卫:先要求镜像构建成功、且未被 skip 列表排除,
# 然后 Stage A 成功或者 bypass_fastfail=true 均可放行,覆盖 setup 失败的边界场景。
stage-b-2-gpu-h200:
  needs: [resolve-ci-policy, stage-a-cpu, docker-build, resolve-ci-image]
  if: |
    always() && !cancelled() &&
    github.event.action != 'closed' &&
    needs.resolve-ci-policy.result == 'success' &&
    needs.resolve-ci-image.result == 'success' &&
    !contains(fromJSON(needs.resolve-ci-policy.outputs.skipped_stages || '[]'), 'stage-b-2-gpu-h200') &&
    (needs.stage-a-cpu.result == 'success' ||
     needs.resolve-ci-policy.outputs.bypass_fastfail == 'true')
  uses: ./.github/workflows/_run-ci.yml
  with:
    runs_on: '["h200", "2gpu"]'
  strategy:
    fail-fast: false
    max-parallel: ${{ needs.resolve-ci-policy.outputs.cadence == 'weekly' && 1 || 2 }}
tests/ci/test/test_run_suite.py test-coverage

测试防护:将 failure 断言反转为否定断言,并对 GPU 门控整体追加同款检查,防止未来重新引入对精确 'failure' 状态的嵌套依赖。

    def test_docker_build_waits_for_cpu_gate_and_preserves_bypass(self):
        workflow = self._workflow()
        caller = workflow.split("  docker-build:", 1)[1].split("  resolve-ci-image:", 1)[0]
​
        # 核心守卫仍然保留:取消、PR 关闭、policy 成功、Stage A 成功
        assert "needs: [resolve-ci-policy, stage-a-cpu]" in caller
        assert "always() && !cancelled()" in caller
        assert "github.event.action != 'closed'" in caller
        assert "needs.resolve-ci-policy.result == 'success'" in caller
        assert "needs.stage-a-cpu.result == 'success'" in caller
        # 关键 seam:bypass 必须是独立替代条件,禁止重新耦合精确 'failure' 状态
        assert "needs.resolve-ci-policy.outputs.bypass_fastfail == 'true'" in caller
        assert "needs.stage-a-cpu.result == 'failure'" not in caller
        assert "stage-b-cpu" not in caller
        assert "uses: ./.github/workflows/_build-pr-ci-image.yml" in caller
        assert "secrets: inherit" in caller
​
    def test_gpu_gates_consume_shared_bypass_output(self):
        workflow = self._workflow()
        gpu_stages = workflow.split("  stage-b-2-gpu-h200:", 1)[1]
        bypass_gate = "needs.resolve-ci-policy.outputs.bypass_fastfail == 'true'"
        # 五个 GPU 阶段必须全部消费 shared bypass 输出,且不得依赖精确 'failure'
        assert gpu_stages.count(bypass_gate) == 5
        assert gpu_stages.count("needs.resolve-ci-policy.result == 'success'") == 5
        assert gpu_stages.count("needs.resolve-ci-image.result == 'success'") == 5
        assert "needs.stage-a-cpu.result == 'failure'" not in gpu_stages

评论区精华

bypass 语义边界的自查验证 question

来源为 PR body 的 Review Focus(作者自列,非 reviewer 提出):确认 bypass 应覆盖 Stage A 的每一种非成功结果,但不能绕过 cancellation、policy failure、image failure 或 stage selection;同时确认测试 seam 能拒绝重新引入精确 Failure 依赖。

结论:最终实现将 bypass 提升为独立替代条件,并保留全部原有守卫,测试断言反转为否定形式。 · 已解决

风险与影响

  1. fail-open 语义扩大:修复后只要 policy 输出 bypass_fastfail=true,Stage A 的真实回归(如代码级测试失败)也会被放行到 GPU 阶段,失败暴露延迟且浪费 GPU 资源;该风险由 ci_policy 的输出正确性兜底,本次未改动 policy 生成逻辑。
  2. 测试覆盖的静态局限:tests/ci/test/test_run_suite.py 只做字符串级断言,无法模拟 GitHub 在 reusable workflow setup 失败时 caller 侧真实的 job result 语义,也无法验证「非 failure 状态确实能被 bypass 覆盖」这一运行时行为。
  3. 核心 CI 门控路径变更:.github/workflows/pr-test.yml 是 PR 与 nightly 共用的主干工作流,若条件语法或守卫搭配有误,会直接导致镜像构建或 GPU 阶段异常放行;但改动仅删去一层嵌套,风险相对可控。

影响范围限定在 CI 基础设施层:nightly 与 PR 运行在 Stage A 基础设施故障(如 setup-uv 安装失败)时不再中断整轮验证,docker-build 与五个 GPU 阶段能按 nightly 策略继续推进。对开发团队而言,CI 可靠性提升、夜间结果更稳定;对产品用户无任何功能影响。改动文件仅两个,共 15 行,回滚成本低。

门控放宽 fail-open 静态断言无法覆盖运行时语义 核心 CI 门控路径变更

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论