Files
codex-app-server/tools/chv_merge_precheck.py
host 54f20dfe55 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 账单仍是硬门禁。
2026-10-31 12:15:56 +00:00

145 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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:]))