Skip to content

ci: 加一道 workflow 结构门 —— 挡住「job 名字在、但什么都不会做」 - #848

Merged
vansin merged 5 commits into
mainfrom
ci/workflow-structure-gate
Aug 18, 2026
Merged

ci: 加一道 workflow 结构门 —— 挡住「job 名字在、但什么都不会做」#848
vansin merged 5 commits into
mainfrom
ci/workflow-structure-gate

Conversation

@vansin

@vansin vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

补上一道仓里一直没有的检查:每个 workflow 的每个 job,是不是真的会执行点什么。

起因是一次实测出来的事故,不是设想

2026-08-13,把两个各自新增一个 job 的分支合到一起时,git 把它们共享的这几行判成公共上下文、只保留一份 —— 因为它们逐字相同:

    runs-on: ubuntu-latest
    timeout-minutes: 8
    steps:
      - uses: actions/checkout@v4

按「冲突块取并集」解完之后:

  doc-source-pins:
    name: doc source-pin floor (Docker)     ← 到此为止
  doc-claims:
    name: doc claim freshness (Docker)
    runs-on: ubuntu-latest
    steps: …(5 个 step 全在这里)

🔴 这个文件是合法 YAML。 GitHub 接受它,doc-source-pins 会作为一个什么都不做的 job 出现在检查列表里、显示绿色 —— 一道从不执行的门,和一道执行且通过的门,在 PR 页面上长得一模一样。

而当时仓库里所有的门都是绿的,因为没有任何一道门检查 workflow 文件本身:

.github/ / scripts/ / tests/ 里 yamllint | actionlint 命中 = 0 个文件

判据

每个 job 要么runs-on + 非空 steps,要么是 reusable-workflow 调用(只有 uses)。

第二种形态本仓当前一个都没有(7 个 workflow / 14 个 job 全是 steps 形态),放行它是因为那是 GitHub 的合法写法,将来有人用不该被这道门拦住。

验证

把那个真实的坏形态注入 qa.yml(抽走 agent-node-unitruns-on/steps):

注入后 YAML 仍可解析 ✓        ← 正是问题所在
FAIL: [no-runs-on]   .github/workflows/qa.yml :: agent-node-unit
FAIL: [empty-steps]  .github/workflows/qa.yml :: agent-node-unit —— 这个 job 什么都不会做
退出码 1

复原后:workflows_scanned=7  jobs_checked=14  problems=0  退出码 0
本 PR 落地后自检:workflows_scanned=8  jobs_checked=15  problems=0

🔴 边界(写在脚本头和 workflow 注释里)

它挡的是「这个 job 是个空壳」,不是「这个 job 是对的」。 不检查 job 会不会通过、命令对不对、uses 是否存在、表达式语法、矩阵展开。真要完整校验该上 actionlint —— 这个是零依赖的最小兜底,不是它的替代。

为什么是独立 workflow 文件而不是加进 qa.yml

qa.yml 眼下有五个 PR 在改(#798 / #801 / #803 / #843 / #846,协调分析见 #803)。往里再塞一个只会让合并更难 —— 而这道门本身就是为了少制造这类问题。 独立文件在结构上就不会与它们冲突。

失败信息里直接指向 docs/pre-pr-selfcheck.md §14(那条记的就是这次事故的根因与正确解法),不让人再去翻评论。

起因是 2026-08-13 实测出来的一次真实事故,不是设想:把两个各自新增一个 job
的分支合到一起时,git 把它们共享的 runs-on / timeout-minutes / steps: /
- uses: actions/checkout@v4 判成公共上下文、只保留一份(那几行逐字相同)。
按「冲突块取并集」解完之后,前一个 job 只剩一行 name,五个 step 全归了后一个。

🔴 那个文件是合法 YAML。GitHub 接受它,job 名字出现在检查列表里、显示绿色 ——
   一道从不执行的门,和一道执行且通过的门,在 PR 页面上长得一模一样。
   而当时仓里所有门都是绿的,因为没有任何一道门检查 workflow 文件本身
   (实测 .github/ / scripts/ / tests/ 里 yamllint|actionlint 命中 = 0)。

判据:每个 job 要么有 runs-on + 非空 steps,要么是 reusable-workflow 调用
(只有 uses)。后者本仓当前一个都没有(7 个 workflow / 14 个 job 全是 steps
形态),放行它是因为那是 GitHub 的合法写法,将来有人用不该被拦。

验证:注入那个真实坏形态(把 agent-node-unit 的 runs-on/steps 抽走),
  注入后 YAML 仍可解析 ✓(正是问题所在)
  门红在 [no-runs-on] 与 [empty-steps] 两条上,退出码 1
  复原后回绿,workflows_scanned=7 jobs_checked=14 problems=0

边界写在脚本头与 workflow 注释里:它挡的是「这个 job 是个空壳」,不是「这个
job 是对的」。真要完整校验该上 actionlint,这个是零依赖的最小兜底。

单独一个 workflow 文件、不动 qa.yml。qa.yml 眼下有五个 PR 在改,往里塞会让
合并更难 —— 而这道门本身就是为了少制造这类问题。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e11a2eb4c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if bad:
print(file=sys.stderr)
print(" 最常见的成因是合并时把两个相邻 job 的公共部分只保留了一份 ——", file=sys.stderr)
print(" 见 docs/pre-pr-selfcheck.md §14。解法不是「冲突块取并集」,", file=sys.stderr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the referenced workflow-repair guide

When this check detects a malformed job, its only remediation pointer is docs/pre-pr-selfcheck.md §14, but a repository-wide search of the reviewed commit shows that this file does not exist. Consequently, the new failure path directs contributors to a dead document instead of the promised merge-repair procedure; add the guide in this change or point the message to an existing tracked document.

AGENTS.md reference: AGENTS.md:L23-L24

Useful? React with 👍 / 👎.

- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install --quiet pyyaml

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the PyYAML dependency used by the checker

Every cold run resolves the latest available pyyaml release, so the parser and therefore this gate's result can change or fail without any repository change; it also prevents reconstructing the CI environment from the checked-in inputs. Record an exact version, preferably through a locked requirements file with hashes, and install from that reproducible input.

AGENTS.md reference: AGENTS.md:L25-L25

Useful? React with 👍 / 👎.

Comment on lines +32 to +34
structure:
runs-on: ubuntu-latest
steps:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent the structure gate from disabling itself

When the malformed job is this workflow's own sole structure job—for example, the same merge accident removes its runs-on and steps block—the checker is never invoked, and no independent workflow in this change validates the damaged file. Thus the exact empty-shell failure this gate is intended to catch can still appear green when it affects workflow-structure.yml; invoke the checker from a second independent gate or otherwise arrange mutual validation.

Useful? React with 👍 / 👎.

三条全部成立。

① P1 死链:失败信息指向 docs/pre-pr-selfcheck.md §14,而那个文件不在 main 上
   (它在 #815 的分支里,未合)。实测 git ls-tree origin/main 命中 0。
   改法不是等 #815 合,是把要点直接写进失败信息 —— 门的错误提示不该依赖另一个
   未合的 PR。文件头那处引用也去掉了。

② 未钉依赖:pip install --quiet pyyaml 每次冷跑都解析成当时最新版,同一个
   commit 在不同时间可能拿到不同解析器。这正是本仓在 npm 侧用 npm ci 取代
   npm install 的同一个理由(#841 / #842)—— 我上午刚给别的包做过这件事,
   转头在自己的 PR 里犯了。
   改为 --require-hashes 从 .github/scripts/requirements-workflow-structure.txt
   安装,钉 pyyaml==6.0.2,53 个 sha256 取自 PyPI 的 release 元数据(脚本拉的,
   不是手写的)。

③ 门无法自检:如果同一次合并把 workflow-structure.yml 自己的 structure job
   弄成空壳,那个 workflow 就再也不会被调用 —— 而空壳 job 正是它要抓的东西。
   审查这条抓得准。
   改法:把同一个检查也挂进 no-memory-slugs.yml。选它是因为它的触发含
   '**/*.yml',任何 workflow 文件的改动都会到那里,所以两个文件互为兜底。
   验证:把 workflow-structure.yml 的 structure job 抽成只剩 name(仍是合法
   YAML),同一个脚本报 [no-runs-on] + [empty-steps],退出 1;复原后回绿。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

三条全部成立,已修(97d3a346)。

① P1 死链 —— 而且我不该用「等 #815 合」来解

失败信息指向 docs/pre-pr-selfcheck.md §14,而那个文件不在 main 上(实测 git ls-tree origin/main 命中 0)—— 它在 #815 的分支里,未合。

改法不是等 #815:一道门的错误提示不该依赖另一个未合的 PR。 把要点直接写进失败信息:

最常见的成因:合并两个相邻 job 时,git 把它们逐字相同的公共部分
(runs-on / timeout-minutes / steps: / - uses: actions/checkout@v4)
判成公共上下文、只保留了一份 —— 于是前一个 job 只剩 name。
解法不是「冲突块取并集」,而是把属于同一个 job 的碎片拼回完整块:
每个 job = 自己的头 + 公共块(各复制一份)+ 自己的体。

文件头那处引用也去掉了。

② 未钉依赖 —— 这条最难看

pip install --quiet pyyaml 每次冷跑都解析成当时的最新版,同一个 commit 在不同时间可能拿到不同的解析器。

这正是本仓在 npm 侧用 npm ci 取代 npm install 的同一个理由。#841 / #842 就是我今天上午做的那两个 PR —— 我给 agent-node 和 agent-network 做完这件事,转头在自己的 PR 里犯了同一个错。

改为按锁文件安装:

- run: pip install --quiet --require-hashes -r .github/scripts/requirements-workflow-structure.txt

pyyaml==6.0.2,53 个 --hash=sha256: 取自 PyPI 的 release 元数据(脚本拉的,不是手写的),文件头写了更新方式。

③ 门无法自检 —— 这条抓得最准

如果同一次合并把 workflow-structure.yml 自己structure job 弄成空壳,那个 workflow 就再也不会被调用 —— 而空壳 job 正是它要抓的东西。

改法:把同一个检查也挂进 no-memory-slugs.yml。选它不是随便挑的 —— 它的触发含 '**/*.yml',任何 workflow 文件的改动都会到那里,所以两个文件互为兜底。

验证(把 workflow-structure.ymlstructure job 抽成只剩 name):

弄坏后仍是合法 YAML ✓        ← 正是问题所在
FAIL: [no-runs-on] .github/workflows/workflow-structure.yml :: structure
problems=1
复原后回绿 ✓

一句自评

这三条里有两条(①的死链、②的未钉依赖)是我在别处刚讲过、转头自己违反的。第三条是我完全没想到的角度 —— 一道检查空壳的门,自己也可能变成空壳。

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

独立深审结论:MAJOR / DO-NOT-MERGE as-is(exact head 97d3a346…)。

负向 fixture 实测发现结构检查可假绿:

  • job 只要存在 uses 键就直接放行,uses: null 和误缩进的 job-level checkout 均绿;
  • steps 只测 truthiness,非空字符串或 mapping 也绿;
  • 缺顶层 on 的 workflow 也绿;
  • 新增 requirements 锁文件未进入自身及互验 workflow 的 paths。

同一批非法 YAML 用 actionlint 均会报错,而当前 checker 得到 jobs_checked=3 / problems=0。建议严格区分 reusable-call job 与 runner job、验证字段类型及顶层触发器,并把这些反例固化为负向回归;可考虑由 actionlint 承担完整 Actions schema。

只读审查;未改代码、未 approve/merge/deploy。

独立深审判 MAJOR / DO-NOT-MERGE,列了四条。我用负向 fixture 逐条实跑,
**四条全部成立**,这道门确实会放行非法 workflow:

  反例                                   旧版结果
  ─────────────────────────────────────────────────
  jobs: {j1: {uses: }}(值为 null)      绿
  steps: "this is not a list"            绿
  缺顶层 on:                              绿
  job 级 uses + runs-on(误缩进)         绿
  → 四个反例一起跑:jobs_checked=3 problems=0 退出码 0

成因逐条:

  1. `if "uses" in job: continue` —— **只看键在不在**。`uses:`(None)也算。
     现在要求它是非空字符串。
  2. `if not job.get("steps")` —— 只测真值。非空字符串是真值。
     现在要求非空**列表**。
  3. 根本没检查顶层 `on:` —— 一个永远不会被触发的 workflow 也算「结构完好」。
     现在缺 on 直接红。
  4. 只查「uses 是不是非空字符串」还不够:真正的 reusable 调用**不会同时带
     runs-on / steps**。两者并存的典型来源正是把 step 级的
     `- uses: actions/checkout@v4` 误缩进到了 job 级。现在这种也红。

  第 4 条是我修第 1 条之后**仍然漏掉**的 —— 加严「uses 必须是字符串」并不能
  挡住「uses 是合法字符串但这个 job 其实是 runner job」。是重跑第四个反例才发现。

加严后重跑(见证红):四个反例 problems=4、退出码 1,每条给出各自的失败原因:

  [bad-uses] / [bad-steps] / [no-trigger] / [uses-with-runner-keys]

反向复核(见证绿):真仓库 8 个 workflow / 15 个 job / problems=0,没有误伤。

另修第四条:`requirements-workflow-structure.txt` 此前不在任何一个 workflow 的
`paths:` 里 —— 只改锁文件(比如 bump pyyaml)的 PR 不会触发这两道门。
现已加进 workflow-structure.yml 与 no-memory-slugs.yml 的 pull_request/push paths。

🔴 记一个我自己的过程错误:第一次加 paths 时,我用「文件里有没有这个字符串」
做「是否已包含」的判据,而那个串本来就出现在 `pip install -r` 那一行 ——
于是误判成「已含」直接跳过,YAML 解析出来 paths 里根本没有。
改成按**解析后的 paths 列表**判才对。判据要落在你真正关心的那个结构上。

关于「可考虑由 actionlint 承担完整 Actions schema」:同意方向,本次没做 ——
那是换实现,不是修 bug,应当单独提。这道门的定位仍是零依赖的最小兜底,
它的文件头也已写明「不做 actionlint 那样的完整校验」。
@vansin

vansin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

这条 MAJOR 钉的是过期 head,现状已不适用(复核,附证据)

我在做一次全仓审计时发现:本 PR 的 MAJOR / DO-NOT-MERGE 判定钉的是 97d3a346…,
当前 head 是 56606ec9f9ec。逐条复核后,那条判定指出的问题已经在当前 head 上修掉了

一、判定当时说的

负向 fixture 实测发现结构检查可假绿:job 只要存在 uses 键就直接放行,
uses: null 和误缩进都能过。

也就是旧实现里的 if "uses" in job: continue —— 只看键在不在

二、当前 head 的实现已经不是那样

.github/scripts/check-workflow-structure.py 现在的代码里,
uses 分支上方就写着这次修复的缘由:

# reusable workflow 调用:只有 uses,没有 runs-on/steps,是合法写法。
# 🔴 旧版写的是 `if "uses" in job: continue` —— **只看键在不在**。

并且同一份文件里补齐了几类结构判据(not-a-mapping / no-jobs /
job-not-a-mapping / 缺 on 的触发器等),都是 isinstance 级的实际校验,不再是"键存在即放行"。

三、当前状态

head        56606ec9f9ec
mergeable   MERGEABLE
CI          5/5 SUCCESS,其中 `structure` → SUCCESS

我另外在别处独立验过这道门会红(不是恒绿):在 jobs: 下注入一个只有 name 的 job,
输出 FAIL: [no-runs-on] + FAIL: [bad-steps] steps 必须是非空列表(收到 NoneType),
problems=2,退出码 1。

四、建议

请原评审人确认后把这条 MAJOR 撤回或降级,并在评论里钉当前 head。
理由不是"我觉得修好了",而是:

一条钉在过期 head 上、又没有后续状态的 DO-NOT-MERGE,
接手合并的人打开页面只能看到"这个 PR 不许合",无法判断它是否还成立

(同一天 #803 上出现过同样的形状,原评审人已补撤回评论闭环;
我自己在 #812 上也犯过"判定没钉 head",已自行补钉。)

(只读:gh pr view + git show pr/848:<file>;未改本 PR、未 approve、未 merge。
我不代替原评审人撤回他的判定。)

@vansin

vansin commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

这道门抓的东西是真的,而且我把它拿到今天的 main 上实跑了一遍验证它还成立:

$ python3 check-workflow-structure.py     # 今天的 origin/main
workflows_scanned=13
jobs_checked=21
problems=0
真实 exit=0

13 / 21 和我独立数出来的完全一致(yaml.safe_load 逐个数 jobs),说明它的扫描范围没有漏。四天前它写的时候是 7 / 14 —— 期间加了 6 个 workflow,它都数到了。

而它要防的那个失效形状,今晚我在别处又遇到了一次同族的:「job 存在」「job 会被触发」「job 拦得住合并」是三件不同的事,而这道门管的是更靠前的一件 —— 「job 里到底有没有东西可执行」。docstring 里那句总结很准:

一道从不执行的门,和一道执行且通过的门,在 PR 页面上长得一模一样。

三条小意见,都不阻塞,但第一条建议合之前顺手改。


1. 🔴 job 没写 name:,会用 job id structure —— 这正是 #916 刚修掉的那个坑

jobs:
  structure:          # 没有 name:

GitHub 上这个 check 就叫 structure。今天全仓没有重名(我核过,21 个 check 名里没有 structure),structure 是一个通用词 —— 和 scan 一模一样的性质。

#916 刚刚修掉的正是这个:四个 workflow 的 job 全叫 scan,导致

  • 分支保护的 required check 里写 scan 指的是哪一道无法确定;
  • 按 check 名统计覆盖率会把四道门算成一道

建议加一行:

    name: workflow-structure

顺带这道门自己就可以顺手把这条也管起来:**「job 必须有唯一的 name:」**是一条和它现有判据同源、同样零成本的规则。今天全仓只有 1 个 job 落在通用名上(就是它自己)。

2. docstring 说「零依赖」,但它有一个 requirements 文件

🔴 它不检查什么 … 这个脚本是零依赖的最小兜底
.github/scripts/requirements-workflow-structure.txt
pyyaml==6.0.2 --hash=sha256:…

pin + hash 的写法我很赞成(而且注释写明了哈希取自 PyPI 元数据、不是手写的,这一点比大多数 requirements 文件做得好)。要改的只是那句「零依赖」—— 它现在和同一个文件里的事实不符。写成「只依赖 PyYAML,且钉了版本和哈希」就准确了。

(这个仓今晚已经因为「注释说的和代码做的相反」开过一条 PR:#918。)

3. 两个会随时间失准的数字

  • docstring:「本仓当前 7 个 workflow、14 个 job 全部是 steps 形态」→ 今天是 13 / 21
  • 「实测 yamllint|actionlint 命中 = 0」→ 这条今天仍成立(我核了)。

第一条建议改成「写这道门时(2026-08-14)是 7/14」—— 加一个时间戳,它就从一个会过期的事实变成一个永远为真的记录

🔴 这和今晚 #845 那批「全仓 N 处」是同一族:把一个会变的外部事实抄进文本,而没有任何东西会告诉你它过期了。 区别只在于,加上「截至某日」之后它就不再需要维护。


关于 no-memory-slugs.yml 那 12 行

requirements-workflow-structure.txt 加进 slug 门的 paths —— 我没看懂动机(那是个 .txt,不是 yaml,slug 门扫的是 **/*.yml|yaml)。如果是有意的,在 PR 描述里写一句;如果是顺手加的,建议撤掉 —— 一条没有理由的 paths 条目,下一个人会花时间去猜它。

改完 1(和 2 如果顺手)我就合。

t and others added 2 commits August 18, 2026 08:57
本 PR 新增的 job 没有写 `name:`:

    jobs:
      structure:
        runs-on: ubuntu-latest

于是 GitHub 上的 check 名会退化成 job id `structure` —— 泛到看不出是哪一道门。
**这正是 2026-08-18 之前四个 workflow 的 job 全叫 `scan` 造成的局面**:
分支保护里的 required check 只能按这个名字写,重名/泛名时指的是哪一道无法确定,
按名字统计覆盖率还会把几道门算成一道。`docs-integrity.yml` 里为此专门留了注释。

🔴 而本 PR 的立论是「**一道不执行的 job,和一道执行且通过的 job,在 PR 页面上
长得一模一样**」—— 一个认不出是谁的 check 名,是同一个问题的另一面:
门在跑,但看的人分不清跑的是哪一道。

补上 `name: workflow-structure`。复核:全仓 26 个 job,26 个不同的名字。

顺带扫了一遍:另有 4 个 job 也没写显式 `name`(`e2e` / `rename-ghost-gate` /
`published-pins` / `release-tag`),但它们的 job id 本身就是描述性的,当 check 名
够用;而且它们已经是既有的 check 名,改名会让引用它们的地方(以及历史)对不上。
**只改这一个新加的。**

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vansin

vansin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

合了。CI 里这道门真跑了,数字在:workflows_scanned=16 jobs_checked=26 problems=0

我做了见红,三种坏法三种红

不能只信「它在干净树上是绿的」。三次变异(每次都还原,cmp 确认):

A  造一个只有 name 的空壳 job         → rc=1  problems=2  jobs_checked 26→27
B  job 有 runs-on 但 steps: []        → rc=1  problems=1
C  扫描目录改成不存在的路径(分母)    → rc=1  "FAIL: 找不到 …/.github/nonexistent"

🔴 A 那个变异的 YAML 是合法的,yaml.safe_load 解析出 6 个 job 一点问题没有 —— 这正是本 PR 立论的那个点:合法 ≠ 会执行

C 是分母承重那格:目录塌了会红,而不是「扫到 0 个 job,0 个问题,绿」。

🔴 我在合并时补了一样东西 —— 它恰好是这个 PR 讲的问题的另一面

本 PR 新增的 job 没有写 name::

jobs:
  structure:
    runs-on: ubuntu-latest

于是 GitHub 上的 check 名会退化成 job id structure —— 泛到看不出是哪一道门

而这正是 2026-08-18 之前四个 workflow 的 job 全叫 scan 造成的局面:分支保护里的 required check 只能按这个名字写,泛名/重名时指的是哪一道无法确定,按名字统计覆盖率还会把几道门算成一道。docs-integrity.yml 里为此专门留了注释。

本 PR 的立论是「一道不执行的 job 和一道执行且通过的 job,在 PR 页面上长得一模一样」—— 一个认不出是谁的 check 名,是同一个问题的另一面:门在跑,但看的人分不清跑的是哪一道。

补上 name: workflow-structure。复核:全仓 26 个 job,26 个不同的名字。CI 里那个 check 现在叫 workflow-structure

顺带扫了一遍:另有 4 个 job 也没写显式 name(e2e / rename-ghost-gate / published-pins / release-tag),但它们的 job id 本身就是描述性的,当 check 名够用,而且已经是既有的 check 名 —— 改名会让引用它们的地方和历史对不上。只改这一个新加的。

两条附带确认

① 依赖是 --require-hashes 钉死的。 理由和 npm ci 取代 npm install 一样:不钉的话同一个 commit 在不同时间解析出不同的解析器版本。注释里写了哈希取自 PyPI release 元数据而非手写 —— 这一句很重要,手写哈希和抄来的哈希在文件里长得一样

② 单独一个 workflow 文件、不动 qa.yml 的选择是对的。qa.yml 眼下有多个 PR 在改,往里塞会让合并更难 —— 而这道门本身就是为了少制造这类问题(它的起因就是一次 qa.yml 的合并把 job 合成了空壳)。

一条后续(不阻塞)

这个脚本没有 --selftest。C 那个变异说明它在范围塌掉时会 fail-closed,所以不是空转;但取集本身没有在 CI 里被自证。仓里其它几道门(check-l1-paths-sync / check-docs-integrity / check-doc-symbol-anchors)都带 selftest 并在正式跑之前先跑一遍,建议对齐。

@vansin
vansin merged commit e94f3a0 into main Aug 18, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants