- 后端新增 cloud/tests/test_chv_closeup.py 85 项:唤醒失败 (vm.create/vm.boot/API 超时/guest 就绪超时/host key 不匹配/ provisioned 缺钉死)、恢复与快照失败、取消与清理(残留 socket/登记/磁盘/密钥、脏标记)、中途死亡分类、只读 execute 归类、入口校验、preflight 事后改写、防御性 except、worker_adapter 剩余分支;executor.py 覆盖 100%。 - 前端新增 src/api/chv.closeup.test.ts 13 项:显式 false、 undefined、可用性优先级、Workers 旧 bug 文案不复发、 120 秒边界;控制按钮与新页面仍不做。 - 新增 tools/chv_merge_precheck.py(只读):逐个核 base/head/ mergeable/CI/链衔接,实测 11 个 PR 全 NOT-READY(CI 全 FAIL、mergeState 全 UNSTABLE),退出码 1,绝不合并。 - docs/chv-production-readiness.md 新增 §14:审计落盘运维 runbook(定位/轮转/读取/清理,ADR-14 取舍不变)、mypy 默认口径 2 错评估(沙箱验证过的标注修法,本轨不改 其它链文件)、灰度三阶段可执行清单、V-1~V-9 检查表 (全部未验证)、逐项评分。 - 安全语义不变:egress、key seed 投递、磁盘删除、默认 provisioned、tap 校验、前端不绕后端白名单均未放宽。 - 只到 PR OPEN,不合并、不部署;CI 账单仍是硬门禁。
145 lines
5.7 KiB
Python
145 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
||
"""CHV 合并前检查(只读,绝不合并、绝不改 CI、绝不写库)。
|
||
|
||
用途:合并 §13.1/§14 链上 PR 之前,逐个核对 base/head、
|
||
mergeable、CI 状态、与 main 头的关系与堆叠顺序,输出
|
||
诚实结论。CI 仍是 UNSTABLE 就如实报 NOT-READY,不把
|
||
账单模式的秒级失败读成代码失败,也不把 UNSTABLE 读成绿。
|
||
|
||
用法:
|
||
python3 tools/chv_merge_precheck.py [PR号 ...]
|
||
缺省检查整条 CHV 链(不含本轨新 PR;新 PR 建好后把
|
||
它的号追加到参数末尾再跑一次):
|
||
python3 tools/chv_merge_precheck.py 216 221 230 232 234 \\
|
||
239 246 252 254 257 264 <本轨新 PR 号>
|
||
|
||
依赖:gh(已登录)与 git(仅读本地 remote,不 fetch、
|
||
不 checkout、不 merge)。gh 不可用或 API 失败时退出码 2。
|
||
退出码:0 = 全部就绪(可按顺序合并);1 = 未就绪(见原因);
|
||
2 = 工具/查询失败(结论不可信,不要据此合并)。
|
||
"""
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
|
||
DEFAULT_STACK = [216, 221, 230, 232, 234, 239, 246, 252, 254, 257, 264]
|
||
|
||
|
||
def _run(cmd):
|
||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||
if proc.returncode != 0:
|
||
raise RuntimeError("%s 失败:%s" % (
|
||
" ".join(cmd), (proc.stderr or proc.stdout).strip()[:500]))
|
||
return proc.stdout
|
||
|
||
|
||
def _gh_json(args):
|
||
return json.loads(_run(["gh"] + args))
|
||
|
||
|
||
def _check_summary(rollup):
|
||
"""把 statusCheckRollup 归纳成一句话,不猜失败原因。"""
|
||
if not rollup:
|
||
return "NO-CHECKS"
|
||
states = []
|
||
for item in rollup:
|
||
name = item.get("name") or item.get("workflowName") or "?"
|
||
conclusion = item.get("conclusion")
|
||
status = item.get("status")
|
||
states.append("%s:%s" % (name, conclusion or status))
|
||
conclusions = {item.get("conclusion") for item in rollup}
|
||
if "FAILURE" in conclusions or "CANCELLED" in conclusions:
|
||
verdict = "FAIL"
|
||
elif conclusions <= {"SUCCESS", "SKIPPED", "NEUTRAL", None} \
|
||
and all(item.get("status") == "COMPLETED" for item in rollup):
|
||
verdict = "GREEN"
|
||
else:
|
||
verdict = "PENDING"
|
||
return "%s [%s]" % (verdict, "; ".join(states))
|
||
|
||
|
||
def main(argv):
|
||
try:
|
||
prs = [int(a) for a in argv] or list(DEFAULT_STACK)
|
||
except ValueError:
|
||
print("参数须为 PR 号(整数)", file=sys.stderr)
|
||
return 2
|
||
try:
|
||
repo = _gh_json(["repo", "view", "--json", "nameWithOwner"])[
|
||
"nameWithOwner"]
|
||
main_head = _run(
|
||
["gh", "api", "repos/%s/commits/main" % repo, "--jq",
|
||
".sha"]).strip()
|
||
except Exception as exc:
|
||
print("查询仓库/main 头失败:%s" % exc, file=sys.stderr)
|
||
return 2
|
||
print("仓库:%s" % repo)
|
||
print("当前 main 头:%s" % main_head)
|
||
print("链(按合并顺序):%s" % " → ".join(str(p) for p in prs))
|
||
print("")
|
||
problems = []
|
||
tool_failed = False
|
||
prev_head_branch = "main"
|
||
for number in prs:
|
||
try:
|
||
pr = _gh_json(["pr", "view", str(number), "--json",
|
||
"number,title,state,baseRefName,headRefName,"
|
||
"headRefOid,mergeable,mergeStateStatus,"
|
||
"statusCheckRollup,additions,deletions,"
|
||
"changedFiles"])
|
||
except Exception as exc:
|
||
print("#%d:查询失败:%s" % (number, exc))
|
||
tool_failed = True
|
||
continue
|
||
chain_ok = pr["baseRefName"] == prev_head_branch
|
||
ci = _check_summary(pr.get("statusCheckRollup") or [])
|
||
ready = (pr["state"] == "OPEN" and pr["mergeable"] == "MERGEABLE"
|
||
and pr["mergeStateStatus"] == "CLEAN" and chain_ok
|
||
and ci.startswith("GREEN"))
|
||
print("#%(number)d %(title)s" % pr)
|
||
print(" base=%(baseRefName)s head=%(headRefName)s@%(headRefOid)s"
|
||
% pr)
|
||
print(" state=%(state)s mergeable=%(mergeable)s "
|
||
"mergeState=%(mergeStateStatus)s" % pr)
|
||
print(" diff: +%s/-%s, %s files" % (
|
||
pr.get("additions"), pr.get("deletions"),
|
||
pr.get("changedFiles")))
|
||
print(" CI: %s" % ci)
|
||
print(" 链衔接(base 应为上一 PR 的 head 分支 %s):%s" % (
|
||
prev_head_branch, "OK" if chain_ok else "MISMATCH"))
|
||
print(" 本项:%s" % ("READY" if ready else "NOT-READY"))
|
||
if not ready:
|
||
reasons = []
|
||
if pr["state"] != "OPEN":
|
||
reasons.append("state=%s" % pr["state"])
|
||
if pr["mergeable"] != "MERGEABLE":
|
||
reasons.append("mergeable=%s" % pr["mergeable"])
|
||
if pr["mergeStateStatus"] != "CLEAN":
|
||
reasons.append("mergeState=%s"
|
||
% pr["mergeStateStatus"])
|
||
if not chain_ok:
|
||
reasons.append("base 衔接断裂")
|
||
if not ci.startswith("GREEN"):
|
||
reasons.append("CI 未绿(%s)" % ci)
|
||
problems.append("#%d:%s" % (number, "; ".join(reasons)))
|
||
prev_head_branch = pr["headRefName"]
|
||
print("")
|
||
if tool_failed:
|
||
print("结论:查询失败,结论不可信,不要据此合并。")
|
||
return 2
|
||
if problems:
|
||
print("结论:NOT-READY,禁止合并。原因:")
|
||
for problem in problems:
|
||
print(" - %s" % problem)
|
||
print("注意:CI 的秒级 verify 失败此前被定性为账单模式,"
|
||
"脚本不据此改判——账单恢复变绿前,结论保持 NOT-READY。")
|
||
return 1
|
||
print("结论:READY,可按上面顺序逐个合并(合并后仍默认关闭,"
|
||
"再跑 §14 灰度清单)。")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv[1:]))
|