Files
gplan-2/tools/ci_watch.sh
T

127 lines
5.9 KiB
Bash
Executable File
Raw 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 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