工程+文档(CI 合回零等待): 轮询等待件与本地回执判定退役,改按需现查 run 终局态

- 缘由(实证): ci_watch.sh 以「commit status 离开 pending」作终局判据——run 37 开跑 36 秒即写
  verdict=red,该 run 三分半后 conclusion=success(现查 API),同 commit 先红后绿;远端时代 16 条
  红回执所引 run 现查 15 条真红 1 条假红。另两条稳定假红源: 超时档(零触发批远端不建任务——现查
  90abdd4a 的 statuses=null,只要挂了轮询必等满 3000s 写红;runner 排队同形)与多路轮询重复覆写
  (fb27dc2f 三条红挤在 15 秒内)。修判据不如撤等待: 判据移到开工前一次现查,竞态与超时两类假红
  结构性消失。
- tools/ci_check.sh(新): 单次读 run 自身 status/conclusion,未 completed 与零触发一律呈报未落定
  不写红;红态列失败 job 与失败步;只呈报不落盘。退出码 0绿/1红/2未落定/3参数与令牌。
  孤立验证四形态: fb27dc2f→红(run 51 conclusion=failure) / 90abdd4a→未落定(零触发) /
  f1840fbc→未落定(run 55 queued) / 39acdcaa→绿(run 48 success)。
- tools/hooks/post-merge: 只推送不再挂后台轮询;push_failed 判定不再落 ci-status.json,改大声呈报。
- 删 tools/ci_watch.sh 与 .zcode/hooks/ci-status-banner.sh(不入 git 面,主区直改)并从 .zcode/config.json
  注销——该横幅在当前 harness 只挂 worktree-guard 的配置下本就不投递(本会话 ci-status 为 red 全程零横幅)。
- 口径同步: AGENTS.md 运行与验证首条、process-conventions §1 第3条(回执等待纪律→合回零等待纪律)与
  第4条执法分层②、§8 第8条、test-conventions §3 与 §8.7 审计证据行(中/低档证据由落盘回执改为
  可复核的现查读数=commit+run号+conclusion)、run_all_gates.sh 头注与 .gates gitignore 模板句、
  迭代技能两镜像三处(入 git 面外另记 .zcode/.qoder)。
- 待办清单条目「ci_watch 中间态误写终局红回执」整条删除(以退役方式了结,非按原判据修复)。
- 在案真红一条随本批呈报未处理: run 51(迭代 51 合回 fb27dc2f) 失败步=导入缓存刷新+跑闸,
  归因待取 job 日志——红未修不开新批的门按用户发令特批越过。
This commit is contained in:
DKChan committed 2026-11-04 06:14:44 +08:00
1 parent f1840fbc80
commit 3c1e3ddf07
9 files changed
+120 -162

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# CI 现查件——按需读远端 run 的终局态,只呈报、不落盘(2026-11-04 用户裁:回执等待件与本地缓存判定退役)。
# 用法: tools/ci_check.sh [<提交hash>](缺省=当前 HEAD)
# 判据:Gitea commit status 只用来取本次任务的 run 链接,绿红一律认 run 自身的 status/conclusion——
# run 未 completed(排队/在跑/尝试级中间态)与零触发批(纯闸件/管线批不建任务)一律呈报「未落定」,不写红。
# 调用面:开工前读态(红未修不开新批)、合回后想知道结果时手动查——合回流程不等待、不唤醒,呈报即回合止。
# 授权:GITEA_TOKEN 环境变量优先,否则读 git 凭据库(host=git.kuaimeo.com)。
# 退出码: 0=绿 1=红 2=未落定/零触发/查不到 3=参数或令牌错误
set -u
COMMON="$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \
|| { echo "不在 git 仓库内"; exit 3; }
REPO="$(dirname "$COMMON")"
BASE="${1:-}"
[ -n "$BASE" ] || BASE="$(git -C "$REPO" rev-parse HEAD)"
BASE="$(git -C "$REPO" rev-parse --verify "${BASE}^{commit}")" || { echo "提交不存在: $BASE"; exit 3; }
_origin="$(git -C "$REPO" remote get-url origin 2>/dev/null || true)"
REPO_SLUG="$(printf '%s' "$_origin" | grep -oE '[^/:]+/[^/]+\.git$' | sed 's/\.git$//')"
[ -n "$REPO_SLUG" ] || { echo "无法从 origin 解析 owner/repo: $_origin"; exit 3; }
TOKEN="${GITEA_TOKEN:-}"
if [ -z "$TOKEN" ]; then
TOKEN="$(printf 'protocol=https\nhost=git.kuaimeo.com\n\n' | git credential fill 2>/dev/null | sed -n 's/^password=//p' | head -1)"
fi
[ -n "$TOKEN" ] || { echo "取不到 Gitea 令牌(设 GITEA_TOKEN 或先 git credential approve 存储)"; exit 3; }
API="https://git.kuaimeo.com/api/v1/repos/$REPO_SLUG"
WEB="https://git.kuaimeo.com/$REPO_SLUG"
export PYTHONIOENCODING=utf-8
python -I - "$API" "$WEB" "$BASE" "$TOKEN" <<'PYEOF'
import json, re, sys, urllib.request, urllib.error
api, web, base, token = sys.argv[1:5]
H = {"Authorization": "token " + token}
def get(url, t=20):
req = urllib.request.Request(url, headers=H)
return json.load(urllib.request.urlopen(req, timeout=t))
def short(sha):
return sha[:8]
def undetermined(msg):
print("CI现查: 未落定 | commit %s | %s" % (short(base), msg))
sys.exit(2)
try:
s = get(api + "/commits/%s/status" % base)
except urllib.error.HTTPError as e:
undetermined("查 commit status 失败 HTTP %s" % e.code)
except Exception as e:
undetermined("查 commit status 失败:%s" % e)
statuses = s.get("statuses") or []
target = next((x.get("target_url", "") for x in statuses if x.get("target_url")), "")
if not statuses:
undetermined("远端无该提交的任务——零触发批(纯闸件/管线批不建任务),或推送未触发 CI")
m = re.search(r"/actions/runs/(\d+)", target or "")
if not m:
undetermined("commit status 无 run 链接(state=%s),查 %s/actions" % (s.get("state"), web))
run_id = m.group(1)
try:
run = get(api + "/actions/runs/%s" % run_id)
except Exception as e:
undetermined("查 run %s 失败:%s" % (run_id, e))
rstatus, rconclusion = run.get("status"), run.get("conclusion")
if rstatus != "completed":
undetermined("run %s status=%s(runner 需开机在线;队列与日志见 %s/actions)" % (run_id, rstatus, web))
if rconclusion == "success":
print("CI现查: 绿 | commit %s | run %s conclusion=success" % (short(base), run_id))
sys.exit(0)
red_jobs = []
try:
jobs = get(api + "/actions/runs/%s/jobs" % run_id)
for j in jobs.get("jobs", []):
if j.get("conclusion") in ("success", "skipped", None):
continue
bad_steps = [x.get("name") for x in (j.get("steps") or []) if x.get("conclusion") not in ("success", "skipped")]
red_jobs.append((j.get("name"), bad_steps))
except Exception as e:
red_jobs.append(("<job明细获取失败:%s>" % e, []))
print("CI现查: 红 | commit %s | run %s conclusion=%s" % (short(base), run_id, rconclusion))
for name, steps in red_jobs:
print(" 红 job:%s" % name)
for st in steps:
print(" 失败步:%s" % st)
print(" 日志:%s" % (target or "%s/actions/runs/%s" % (web, run_id)))
sys.exit(1)
PYEOF
-126
View File
@@ -1,126 +0,0 @@
#!/usr/bin/env bash
# 回执等待件——合回后等远端 CI 回执的后台轮询器(2026-11-03 用户裁:CI 迁远端,本件改轮询 Gitea API)。
# 用法: tools/ci_watch.sh remote <推送提交hash> [超时秒,默认 3000]
# 调用面: post-merge 钩子推送 master 成功后自动后台挂起;人手等待亦可(agent 会话以 run_in_background 运行,
# 命令结束时 harness 唤醒——绿→删区收口;红→呈报红闸名单走修复批;超时→呈报「回执未落」)。
# 「本次回执已落」判据:Gitea commit status 的 state 离开 pending(success/failure/error),
# 且 status 的 target_url 指向本仓库 Actions(防认错仓)。远端跑批 20-35 分钟量级,默认超时给足 50 分钟。
# 产物:沿用本地 CI 时代的回执合同——game/tests/.gates/ci-status.json(commit/verdict/timestamp/
# duration_s/red_gates/key_lines/note)覆写 + ci-history.jsonl 追加;红态 red_gates=失败的 job 名单
# (来自 runs/<id>/jobs API),job 日志链接在 key_lines,细粒度红闸清单以 job 日志为准。
# 授权:GITEA_TOKEN 环境变量优先,否则读 git 凭据库(host=git.kuaimeo.com)。
# 退出码: 0=绿 1=红 2=超时/回执未落 3=参数错误
set -u
COMMON="$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \
|| { echo "不在 git 仓库内"; exit 3; }
REPO="$(dirname "$COMMON")"
GATES_DIR="$REPO/game/tests/.gates"
STATUS="$GATES_DIR/ci-status.json"
HISTORY="$GATES_DIR/ci-history.jsonl"
MODE="${1:-}"
BASE="${2:-}"
TIMEOUT="${3:-3000}"
INTERVAL=30
case "$MODE" in
remote) ;;
*)
echo "用法: ci_watch.sh remote <推送提交hash> [超时秒](本地 CI 模式已退役——回执现由远端产生)"
exit 3 ;;
esac
[ -n "$BASE" ] || { echo "用法: ci_watch.sh remote <推送提交hash> [超时秒]"; exit 3; }
BASE="$(git -C "$REPO" rev-parse --verify "${BASE}^{commit}")" || { echo "提交不存在: $BASE"; exit 3; }
# 仓库全名(owner/repo)从 origin URL 解析(HTTPS 与 SSH 形式都兼容)
_origin="$(git -C "$REPO" remote get-url origin 2>/dev/null || true)"
REPO_SLUG="$(printf '%s' "$_origin" | grep -oE '[^/:]+/[^/]+\.git$' | sed 's/\.git$//')"
[ -n "$REPO_SLUG" ] || { echo "无法从 origin 解析 owner/repo: $_origin"; exit 3; }
# 令牌:环境变量优先,回落 git 凭据库
TOKEN="${GITEA_TOKEN:-}"
if [ -z "$TOKEN" ]; then
TOKEN="$(printf 'protocol=https\nhost=git.kuaimeo.com\n\n' | git credential fill 2>/dev/null | sed -n 's/^password=//p' | head -1)"
fi
[ -n "$TOKEN" ] || { echo "取不到 Gitea 令牌(设 GITEA_TOKEN 或先 git credential approve 存储)"; exit 3; }
API="https://git.kuaimeo.com/api/v1/repos/$REPO_SLUG"
export PYTHONIOENCODING=utf-8
python -I - "$API" "$BASE" "$TOKEN" "$TIMEOUT" "$INTERVAL" "$STATUS" "$HISTORY" <<'PYEOF'
import json, os, re, subprocess, sys, time, urllib.request, urllib.error, datetime
api, base, token, timeout, interval, status_path, history_path = sys.argv[1:8]
timeout, interval = int(timeout), int(interval)
deadline = time.time() + timeout
start = time.time()
H = {"Authorization": "token " + token}
def get(url, t=20):
req = urllib.request.Request(url, headers=H)
return json.load(urllib.request.urlopen(req, timeout=t))
def combined_status(sha):
return get(api + "/commits/%s/status" % sha)
def write_receipt(verdict, note, red_gates, key_lines):
now = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
dur = int(time.time() - start)
d = {"commit": base, "verdict": verdict, "timestamp": now,
"duration_s": dur, "note": note, "red_gates": red_gates, "key_lines": key_lines}
os.makedirs(os.path.dirname(status_path), exist_ok=True)
tmp = status_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False, indent=1)
os.replace(tmp, status_path)
with open(history_path, "a", encoding="utf-8") as f:
f.write(json.dumps(d, ensure_ascii=False) + "\n")
return d
def emit(d):
print("CI回执:", d.get("verdict"), "| commit", d.get("commit", "")[:8], "| 耗时", d.get("duration_s"), "s")
if d.get("note"):
print("note:", d["note"])
for g in d.get("red_gates", []):
print("红闸:", g)
for l in d.get("key_lines", []):
print("key:", l)
last_err = ""
while True:
try:
s = combined_status(base)
state = s.get("state", "")
statuses = s.get("statuses", [])
target = next((x.get("target_url", "") for x in statuses if x.get("target_url")), "")
if state in ("success", "failure", "error", "warning"):
red = state != "success"
red_gates, run_id = [], None
m = re.search(r"/actions/runs/(\d+)", target)
if m and red:
run_id = m.group(1)
try:
jobs = get(api + "/actions/runs/%s/jobs" % run_id)
red_gates = sorted({j["name"] for j in jobs.get("jobs", []) if j.get("status") not in ("success", "skipped")})
except Exception as e:
red_gates = ["<job明细获取失败:%s>" % e]
note = "远端CI state=%s%s" % (state, ("(run %s)" % run_id) if run_id else "")
key_lines = ["job日志: %s" % target] if target else []
d = write_receipt("green" if not red else "red", note, red_gates, key_lines)
emit(d)
sys.exit(0 if not red else 1)
# pending:继续等
except urllib.error.HTTPError as e:
last_err = "HTTP %s" % e.code
except Exception as e:
last_err = str(e)
if time.time() > deadline:
note = "超时(%ds)远端回执未落——最后错误 %s;查 runner 在线态与 %s/actions" % (timeout, last_err or "无", api.rsplit("/repos/", 1)[0])
d = write_receipt("red", note, ["<回执未落>"], [])
emit(d)
sys.exit(2)
time.sleep(interval)
PYEOF
+8 -16
View File
@@ -1,27 +1,19 @@
#!/usr/bin/env bash
# 合回验证点(远端 CI 触发钩子,2026-11-03 用户裁)——主工作区 git merge 落地后:
# ① push origin master(触发远端 CI:Gitea 收到 push → 派发 mac-ci runner 按改动域跑闸)
# ② 后台挂 ci_watch.sh remote 等回执(回执落 ci-status.json,agent 下次开工读态零改动)
# 推送失败=远端验证不可能发生——大声呈报并落 push_failed 判定进 ci-status.json(防静默,下游开工读态即见)。
# 合回验证触发件(远端 CI 推送钩子,2026-11-03 用户裁;2026-11-04 用户裁去掉回执等待)。
# 主工作区 git merge 落地后做一件事:`git push origin master` 触发远端 CI(Gitea Actions,
# runner=Mac 本机 host 模式,按改动域分流跑闸)。判定结果不在本钩子取——合回即呈报本批总结,
# 回合止;要看结果随时跑 tools/ci_check.sh <合并提交>(只呈报、不落盘,2026-11-04 取代原后台轮询件)。
# 推送失败=远端验证不可能发生——大声呈报并给出处置(先修推送再手动 git push,之后用现查件读态),禁静默。
# 任务 worktree 内的合并不触发(CI 只认 master);CI 红了不回滚合并——修复批合回后本钩子再推再验(追绿同链)。
set -u
_GD="$(cd "$(git rev-parse --git-dir)" && pwd)"
_CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
[ "$_GD" = "$_CD" ] || exit 0
REPO="$(git rev-parse --show-toplevel)"
REPO="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || pwd)"
TIP="$(git -C "$REPO" rev-parse HEAD)"
export PYTHONIOENCODING=utf-8
if ! git -C "$REPO" push origin master >/dev/null 2>&1; then
echo "[post-merge] ✗ push origin master 失败——远端 CI 无法触发!先修推送(凭据/网络)后手动 git push,再手动挂 tools/ci_watch.sh remote $TIP"
python -I -c "
import json, os, datetime
p = r'$REPO/game/tests/.gates/ci-status.json'
d = {'commit': r'$TIP', 'verdict': 'push_failed', 'timestamp': datetime.datetime.now().astimezone().isoformat(timespec='seconds'),
'duration_s': 0, 'note': 'post-merge 推送失败,远端 CI 未触发——修复推送后手动 git push + ci_watch remote', 'red_gates': [], 'key_lines': []}
os.makedirs(os.path.dirname(p), exist_ok=True)
json.dump(d, open(p, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
" 2>/dev/null
echo "[post-merge] ✗ push origin master 失败——远端 CI 无法触发!先修推送(凭据/网络),再手动 git push,然后用 tools/ci_check.sh $TIP 读态。"
exit 0
fi
( nohup bash "$REPO/tools/ci_watch.sh" remote "$TIP" >/dev/null 2>&1 & )
echo "[post-merge] ✓ 已推送 origin master(commit ${TIP:0:8})——远端 CI 已触发;结果不等待,随时用 tools/ci_check.sh ${TIP:0:8} 现查。"
exit 0
+4 -4
View File
@@ -4,9 +4,9 @@
# 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。
#
# 跑完写回执 game/tests/.gates/receipt.json(commit+内容树哈希+各闸退出码+存量基线比对+时间戳);
# 远端 CI(2026-11-03 用户裁)读回执判定,回执经 tools/ci_watch.sh remote 轮询 Gitea API 写 ci-status.json——
# 强制验证点=合回 master 后 post-merge 钩子推送触发的远端跑批;本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。
# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节),CI 判定照常落账留痕。
# 强制验证点=合回 master 后 post-merge 钩子推送触发的远端跑批;远端结果用 tools/ci_check.sh 现查(只呈报、
# 不落盘——2026-11-04 用户裁去掉回执等待件与本地判定缓存)。本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。
# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节)。
#
# 用法:tools/run_all_gates.sh [--code|--docs|--quick]
# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/
@@ -86,7 +86,7 @@ fi
# 每次跑批前自愈重建,避免「清目录重跑」把 .gitignore 一起带走。
mkdir -p "$STAMP_DIR"
if [ ! -f "$STAMP_DIR/.gitignore" ]; then
printf '# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。\n# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是 CI 回执判定与流水账(判定由远端 CI 产生)、\n# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。\nreceipt.json\nci-status.json\nci-history.jsonl\nci.lock\nci.rerun\n*.log\n' \
printf '# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。\n# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是已退役的 CI 回执判定与流水账(判定只在远端,本机残留)、\n# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。\nreceipt.json\nci-status.json\nci-history.jsonl\nci.lock\nci.rerun\n*.log\n' \
> "$STAMP_DIR/.gitignore"
echo "[gates] 已自愈重建 $STAMP_DIR/.gitignore"
fi