mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
## 缺陷(真实存在,已实测复现)
check-ci-gates.py 里「排除表每项必须写原因」这条断言原本写成:
check(any("没有写原因" in r for _n, r in race), ...)
它只检查**提示文案还在**,完全不看校验逻辑本身。
实测取证:把 ci.yml 第 245 行的
if [ -z "${reason// /}" ]; then
改成
if false; then
(提示文案一字未动、校验实际彻底失效):
· 该步骤实跑 EXIT=0 ——「无原因必须失败」这条保证彻底破掉
· 而 check-ci-gates.py 仍然通过(GATES_EXIT=0)
也就是说:判据被文案顶包了。这正是本脚本 docstring 里批判的
「run: | 里的 # 是字符串不是注释 ⇒ 注释能骗过断言」,自己又犯了一次。
## 修法
断言改为在**剥掉 shell 注释后的可执行行**上,同时要求:
· `${reason// /}`(判空变量)存在
· `exit 1` 存在
只有文案、没有真逻辑 ⇒ 不通过。
## 验证(先证能红,再证不误伤)
注入 if false 变异(保留文案)→ 修复前 EXIT=0(漏),修复后 **EXIT=1**(咬住)
还原 → EXIT=0
回归另两个变异(去掉 cmd/gui 剔除、去掉 -count=1)→ 均仍 EXIT=1,未被这次修改削弱
## 门禁
go build ./... = 0|go vet ./... = 0|go test ./internal/kernelboot/modules/ = ok
check-ci-gates.py = 0|gofmt 本线文件干净
边界:只改这一个脚本文件,未动 ci.yml、未动业务逻辑;未 push、未开 PR。
190 lines
9.0 KiB
Python
Executable File
190 lines
9.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""CI 门禁自检:断言 ci.yml 的关键机制仍在。退出码 0 = 通过。
|
||
|
||
## 为什么用 YAML 解析,而不是 grep 文本
|
||
|
||
本次改动的注释里**刻意保留了旧写法的引述**(例如
|
||
「原先这里写死 ./internal/agent/core/ ./cmd/waiter/ 两个包」)。
|
||
于是 grep 会两头骗人:
|
||
· 断言「旧写法不存在」→ 注释里那句引述让它**假红**
|
||
· 断言「新机制存在」 → 注释里写一句同名机制就能让它**假绿**
|
||
yaml.safe_load 丢掉全部注释,只留可执行内容 ⇒ 两类误判同时消失。
|
||
|
||
## 这条判据防的是什么
|
||
|
||
CI 配置是「静默退化」的高发区:把动态枚举改回写死包列表,
|
||
流水线照样绿(只是罩得少了),没人会发现。所以必须有人盯着
|
||
「动态枚举还在」「go test 真的吃了枚举结果」这两件事。
|
||
"""
|
||
import sys
|
||
|
||
import yaml
|
||
|
||
PATH = ".github/workflows/ci.yml"
|
||
fails = []
|
||
|
||
|
||
def check(cond, msg):
|
||
if not cond:
|
||
fails.append(msg)
|
||
|
||
|
||
with open(PATH, encoding="utf-8") as f:
|
||
doc = yaml.safe_load(f)
|
||
|
||
jobs = (doc or {}).get("jobs") or {}
|
||
check(bool(jobs), "YAML 解析后没有 jobs —— 文件结构坏了")
|
||
|
||
LEGACY_RACE_PKGS = "./internal/agent/core/ ./cmd/waiter/"
|
||
|
||
|
||
def code_lines(run):
|
||
"""只保留**可执行**行:剥掉 shell 注释。
|
||
|
||
★ 为什么必须这么干(本条判据自己栽过的坑):
|
||
YAML 解析只丢掉了 YAML 级注释,但 `run: |` 块里的 `# ...`
|
||
是**字符串内容**,照样落在 run 文本里。
|
||
于是 `"cmd/gui" in run` 会被一句说明性注释满足 ——
|
||
真把 `grep -v '^.*/cmd/gui$'` 删掉,断言仍然绿。
|
||
判据自己被注释骗过去,和被测代码骗判据是同一类错误。
|
||
"""
|
||
out = []
|
||
for line in run.splitlines():
|
||
if line.lstrip().startswith("#"):
|
||
continue
|
||
out.append(line)
|
||
return "\n".join(out)
|
||
|
||
|
||
def step_runs(job):
|
||
out = []
|
||
for st in (jobs.get(job) or {}).get("steps") or []:
|
||
run = st.get("run")
|
||
if run:
|
||
out.append((st.get("name") or "", run))
|
||
return out
|
||
|
||
|
||
# ── 1. race job:包列表必须动态枚举,并真的剔除 cmd/gui ──
|
||
race = step_runs("race")
|
||
enum = [(n, r) for n, r in race if "go list ./..." in r and "GITHUB_OUTPUT" in r]
|
||
check(len(enum) == 1, "race job 应恰有一步用 `go list ./...` 动态枚举包(实际 %d 步)" % len(enum))
|
||
if enum:
|
||
# ★ 用 code_lines 剥掉 shell 注释后再断言,否则注释里的 "cmd/gui" 会顶包
|
||
check("cmd/gui" in code_lines(enum[0][1]),
|
||
"race 枚举步骤必须显式剔除 cmd/gui(否则 go test 报 no Go files 的假红)")
|
||
|
||
# ── 2. 真正的 go test -race 必须吃枚举结果,不许写死包列表 ──
|
||
#
|
||
# ★ 只认「全仓主步骤」:race job 里还有一步是**单包取证**(只跑 webui 那一条
|
||
# 已知竞争),它本来就不该吃全仓枚举。初版脚本没区分这两类,把取证步骤
|
||
# 也套上「必须吃枚举」的断言 ⇒ 误报红。判据自己误报同样是缺陷(会让人
|
||
# 习惯性忽略红),所以这里按「步骤是否消费 PKGS 变量」来分流。
|
||
main_race = [(n, r) for n, r in race
|
||
if "go test -race" in r and "$PKGS" in r]
|
||
check(len(main_race) == 1,
|
||
"race job 应恰有一步全仓 go test -race 消费 $PKGS(实际 %d 步)" % len(main_race))
|
||
for _n, r in main_race:
|
||
check("steps.pkgs.outputs.pkgs" in code_lines(r),
|
||
"全仓 go test -race 必须使用枚举结果 steps.pkgs.outputs.pkgs")
|
||
check(LEGACY_RACE_PKGS not in code_lines(r),
|
||
"全仓 go test -race 里出现了旧的写死包列表 " + LEGACY_RACE_PKGS)
|
||
|
||
# 取证步骤:允许单包跑,但必须写明修复单号(否则排除项成了无主的掩盖)
|
||
evid = [(n, r) for n, r in race if "go test -race" in r and "$PKGS" not in r]
|
||
for _n, r in evid:
|
||
check("DATA RACE" in r,
|
||
"取证步骤应显式检查 DATA RACE 报告数(否则取证没意义)")
|
||
|
||
# ── 3. skip 排除表:每项必须写原因,无原因必须步骤自己失败 ──
|
||
#
|
||
# ★ 这里曾经写成 `any("没有写原因" in r ...)` —— 只检查**提示文案还在**。
|
||
# 实测:把 `if [ -z "${reason// /}" ]; then` 改成 `if false; then`
|
||
# (文案全保留、校验实际失效),该断言仍然绿,而该步骤实跑 EXIT=0
|
||
# —— 也就是说「无原因必须失败」这条保证彻底破掉,判据却看不见。
|
||
# 这正是本脚本自己在 docstring 里批判的「被文案顶包」,不能自己再犯。
|
||
# ⇒ 改为剥注释后断言**可执行的判空逻辑**:必须同时出现判空变量与 exit 1。
|
||
race_code = "\n".join(code_lines(r) for _n, r in race)
|
||
check('${reason// /}' in race_code and "exit 1" in race_code,
|
||
"race job 排除表缺少真校验:剥注释后必须能看到 ${reason// /} 判空 + exit 1,"
|
||
"只有提示文案不算(把 if 条件改 false 就能骗过纯文案断言)")
|
||
|
||
# ── 4. go job:整机启动图判据必须单列一步跑 kernelboot/modules ──
|
||
gojob = step_runs("go")
|
||
boot = [r for _n, r in gojob if "kernelboot/modules" in r and "go test" in r]
|
||
check(bool(boot), "go job 缺少「整机启动图判据」步骤(go test ./internal/kernelboot/modules/)")
|
||
for r in boot:
|
||
check("-count=1" in r, "整机启动图判据步骤必须带 -count=1(不许吃 go test 缓存)")
|
||
|
||
# ── 5. cross job:cmd 清单必须实测枚举,不许写死 ──
|
||
cross = step_runs("cross")
|
||
pure = [r for _n, r in cross if "CGO_ENABLED=0 go build" in r and "GITHUB_OUTPUT" in r]
|
||
check(bool(pure), "cross job 缺少「枚举可纯交叉编译的 cmd」步骤")
|
||
builds = [(n, r) for n, r in cross if "go build -trimpath" in r]
|
||
check(bool(builds), "cross job 里没有真正的 go build 步骤")
|
||
for _n, r in builds:
|
||
check("steps.pure.outputs.cmds" in r,
|
||
"交叉构建必须吃枚举结果 steps.pure.outputs.cmds,不许写死 cmd 列表")
|
||
|
||
# ── 5.5 code_lines 自测:这是「注释顶包」的唯一防线,必须自己也被验 ──
|
||
#
|
||
# ★ 为什么需要这段自测(M7 的教训):
|
||
# 原先把「code_lines 不再剥注释」当成一条变异,但它**单独注入时不会红** ——
|
||
# 因为 ci.yml 完好时,`grep -v cmd/gui` 真实存在,剥不剥注释都能匹配上。
|
||
# 那是**空变异**(行为没变,不可能红),拿它当判据缺口是误判。
|
||
# 正确做法不是造更狠的变异,而是让 code_lines 的行为**自己可被验证**:
|
||
# 喂一个"cmd/gui 只出现在注释里"的样本,要求剥离后断言为假。
|
||
# 这样把 code_lines 改成不剥注释,本条立刻失败 —— 单文件、真行为变化。
|
||
_probe = "#!/bin/bash\n# cmd/gui 只是注释里的说明\necho hi\n"
|
||
if "cmd/gui" in code_lines(_probe):
|
||
fails.append("code_lines 没剥掉 shell 注释 —— cmd/gui 断言会被注释顶包(假绿)")
|
||
_keep = "#!/bin/bash\nALL=$(go list ./... | grep -v '^.*/cmd/gui$')\n"
|
||
if "cmd/gui" not in code_lines(_keep):
|
||
fails.append("code_lines 把可执行行也删了 —— 断言会假红")
|
||
|
||
# ── 6. pkg 级排除(整包不进 -race):必须带修复单号 + 兜底断言 ──
|
||
#
|
||
# 整包排除是有代价的:该包所有用例都不再受竞争检测保护。
|
||
# 因此要求:① 排除表每项必须有修复单号;② 剔除后包数有下限兜底,
|
||
# 不许"排除逻辑写坏了导致静默少测";③ 断言排除项确实从待测列表消失。
|
||
allrace = "\n".join(r for _n, r in race)
|
||
if "KERNEL-REGISTRY-SETLOCK" in allrace or "pkg" in allrace:
|
||
check("修复单" in allrace or "修复单号" in allrace,
|
||
"pkg 级排除必须写明修复单号(否则成了无主的静默掩盖)")
|
||
check("-lt 50" in allrace or "lt 50" in allrace,
|
||
"剔除后必须有包数下限兜底(防止排除逻辑写坏导致静默少测)")
|
||
check("仍出现在待测列表里" in allrace or "剔除逻辑失效" in allrace,
|
||
"必须断言被排除的包确实从列表消失(防止写了排除但没生效)")
|
||
|
||
# ── 7. 每个 job 的每个 run 步骤:bash 语法必须过 ──
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import tempfile
|
||
|
||
for jn, job in jobs.items():
|
||
for st in job.get("steps") or []:
|
||
run = st.get("run")
|
||
if not run:
|
||
continue
|
||
body = re.sub(r"\$\{\{[^}]*\}\}", "PLACEHOLDER", run)
|
||
fd, p = tempfile.mkstemp(suffix=".sh")
|
||
try:
|
||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||
f.write(body)
|
||
r = subprocess.run(["bash", "-n", p], capture_output=True, text=True)
|
||
if r.returncode != 0:
|
||
first = (r.stderr or "").strip().splitlines()
|
||
fails.append("shell 语法错误 @ %s / %s: %s"
|
||
% (jn, st.get("name"), first[0] if first else "?"))
|
||
finally:
|
||
os.unlink(p)
|
||
|
||
if fails:
|
||
print("CI 门禁自检失败:")
|
||
for f in fails:
|
||
print(" x " + f)
|
||
sys.exit(1)
|
||
|
||
print("CI 门禁自检通过:动态枚举 / 排除表校验 / 启动图判据步骤 / 交叉枚举 / 全部 run 步骤语法")
|