Skip to content

filter job 一旦失败,Test Core / Build Core / Dogfood 会全部 skipped 而分支保护判为通过 —— 隐式 success() 今天已第三次咬人 #4928

Description

@os-zhuang

问题

ci.yml 的 7 个下游 job 都是这个形状:

  test:
    needs: filter
    if: needs.filter.outputs.core == 'true'

if: 里没有状态函数,GitHub 会隐式包一层 success()。于是 filter 这个前置 job 一旦失败(checkout 抖动、dorny/paths-filter 故障、10 分钟超时),下游 test / temporal-conformance / dogfood / dogfood-verify / build-core / build-docs / console-pin 全部被跳过

而聚合闸门无条件接受 skipped:

case "$result" in
  success|skipped|cancelled) echo "Test Core gate satisfied ($result)." ;;

skipped 在分支保护里算通过。合起来:一次 filter 的偶发失败 → 零测试运行 → Test Core / Dogfood Regression Gate 报「gate satisfied」→ PR 全绿可合。而且这个过程没有任何红色信号。

作者意图已经写在代码里了,只是没覆盖到这一层

filter 的 outputs 本身已经做了 fail-safe:

outputs:
  core: "${{ steps.changes.outputs.core || 'true' }}"

—— 存疑就全跑。但这个兜底只覆盖「输出值缺失」,没覆盖「job 整体失败」。job 挂掉时 needs.filter.outputs.core 是空串,'' == 'true' 为假,再叠加隐式 success(),两条路都通向跳过。

dogfood-gatecancelled 的处理是这类推理的正例(有实验记录 run 30271824408,说明真实失败会 dominate 聚合结果所以接受 cancelled 是安全的)。skipped 缺的正是同一层论证:它无法区分「路径过滤器说没碰核心代码」和「过滤器自己炸了」

建议改法

把下游条件从「显式为真才跑」改成「显式为假才跳」,与 || 'true' 的意图完全一致:

    if: ${{ !cancelled() && needs.filter.outputs.core != 'false' }}

filter 失败时 outputs 为空 → '' != 'false' → 跑全量;!cancelled() 压过隐式 success()。存疑就全跑,和作者原本的取舍一模一样。

这是今天同一个 GitHub 语义陷阱的第三次出现

# 位置 后果
1 release.yml 的发布完整性守卫(#4900) changesets 步骤失败后守卫被跳过,恰恰漏掉最需要它的场景
2 release.ymldocker job(#4900) 包已公开之后才发生的故障,把镜像一起带走
3 ci.yml 的 7 个核心 job(本 issue) 核心闸门零运行却报通过

值得沉淀成一条可检查的规则:任何「上游失败时仍须执行」的 step/job,if: 必须显式带状态函数(!cancelled() / always())。这条可以静态扫出来 —— 我审计时就是这么找到全部三处的。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions