feat-chv-closeup:收尾(executor 覆盖到 100%、合并/灰度准备,堆叠于 #264)

- 后端新增 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 账单仍是硬门禁。
This commit is contained in:
host committed 2026-10-31 12:15:56 +00:00
1 parent 2f3ff8b3ec
commit 54f20dfe55
4 files changed
+1759

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
#!/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:]))