98 lines
4.6 KiB
Bash
98 lines
4.6 KiB
Bash
#!/usr/bin/env bash
|
|
# 本地 CI 跑批件——合回 master 后的全量验证执行器(git 钩子触发、后台运行,2026-10-30 用户裁)。
|
|
# 触发语义(钩子只负责拉起,判定在本件):
|
|
# merge 模式=合回验证点:主工作区 git merge 落地后(tools/hooks/post-merge)对 master 尖无条件
|
|
# 跑一次全量域闸;任务 worktree 内的合并不触发(钩子里限主工作区)。
|
|
# commit 模式=红态追绿:仅当最近一次 CI 判红时(tools/hooks/post-commit)本提交触发复验;
|
|
# 绿态与无状态零触发——小批直提不重复跑全量,有意义的重跑只剩「验证修复」一类。
|
|
# 并跑语义=跑完补跑(coalesce,2026-10-30 用户裁):跑批期间新触发不中断当前轮——锁被占时,
|
|
# 在跑实例的尖端就是当前尖端则本触发无事可做,否则落 ci.rerun 标记;当前轮跑完见标记或尖端
|
|
# 前移,对最新尖端自动补跑一轮。绝不逐笔触发各跑一轮,最终只测最新尖端。
|
|
# 产物(均不入库,本目录 .gitignore 已收):ci-status.json=最新一跑判定(commit/verdict/红闸/
|
|
# 关键日志行/耗时),ci-history.jsonl=逐次流水账,ci.log=当轮全量闸完整输出,
|
|
# ci.lock=锁目录(内含在跑尖端),ci.rerun=补跑标记。
|
|
# 用法: tools/ci_runner.sh merge|commit
|
|
set -u
|
|
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
|
GATES_DIR="$REPO/game/tests/.gates"
|
|
LOCK="$GATES_DIR/ci.lock"
|
|
RERUN="$GATES_DIR/ci.rerun"
|
|
STATUS="$GATES_DIR/ci-status.json"
|
|
HISTORY="$GATES_DIR/ci-history.jsonl"
|
|
LOG="$GATES_DIR/ci.log"
|
|
GODOT="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
|
|
export PYTHONIOENCODING=utf-8
|
|
|
|
MODE="${1:-commit}"
|
|
|
|
# ── 触发判定 ──
|
|
if [ "$MODE" = "commit" ]; then
|
|
[ -f "$STATUS" ] || exit 0
|
|
grep -q '"verdict": "red"' "$STATUS" || exit 0
|
|
fi
|
|
|
|
# ── 锁(mkdir 原子);被占时按在跑尖端判定:同尖=已在验这份字节,异尖=落补跑标记 ──
|
|
TIP="$(git -C "$REPO" rev-parse HEAD)"
|
|
if ! mkdir "$LOCK" 2>/dev/null; then
|
|
if [ "$(cat "$LOCK/tip" 2>/dev/null)" = "$TIP" ]; then
|
|
exit 0
|
|
fi
|
|
touch "$RERUN"
|
|
exit 0
|
|
fi
|
|
trap 'rm -rf "$LOCK"' EXIT
|
|
|
|
# ── 跑批主循环(跑完补跑)──
|
|
while :; do
|
|
rm -f "$RERUN"
|
|
TIP="$(git -C "$REPO" rev-parse HEAD)"
|
|
echo "$TIP" > "$LOCK/tip"
|
|
_start="$(date +%s)"
|
|
|
|
# 起手刷类名缓存:.godot 缓存不入 git,与合并后代码失配会假红假绿(在册坑)——
|
|
# 原「合并后零复跑」的秒级动作由本件接管,全量闸跑的就是刷新后的字节
|
|
"$GODOT" --headless --path "$REPO/game" --editor --quit >>"$LOG" 2>&1
|
|
|
|
bash "$REPO/tools/run_all_gates.sh" >"$LOG" 2>&1
|
|
RC=$?
|
|
_end="$(date +%s)"
|
|
|
|
# 判定与记录:读闸脚本写的回执(all_green/各闸退出码),红项关键行取自本轮 ci.log
|
|
python - "$REPO" "$TIP" "$RC" "$((_end - _start))" <<'PY'
|
|
import json, re, sys, datetime
|
|
repo, tip, rc, dur = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
|
gates_dir = repo + "/game/tests/.gates"
|
|
entry = {"commit": tip,
|
|
"timestamp": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
"exit_code": rc, "verdict": "green" if rc == 0 else "red",
|
|
"duration_s": dur, "red_gates": [], "key_lines": []}
|
|
try:
|
|
with open(gates_dir + "/receipt.json", encoding="utf-8") as f:
|
|
r = json.load(f)
|
|
# 回执只有确属本跑(commit 对齐)才采信——闸在起手自检即中止时不写新回执,
|
|
# 盘上旧回执是上一跑的读数,当作本次红项会撒谎(2026-10-31 实跑缺陷修正)
|
|
if r.get("commit") == tip:
|
|
entry["red_gates"] = [g["name"] for g in r.get("gates", []) if str(g.get("exit")) != "0"]
|
|
else:
|
|
entry["note"] = "闸未跑完(起手自检或早期中止,回执为上一跑旧件)——红项读数不可用,看 key_lines 与 ci.log"
|
|
except Exception:
|
|
pass
|
|
try:
|
|
with open(gates_dir + "/ci.log", encoding="utf-8", errors="replace") as f:
|
|
lines = [l.rstrip() for l in f]
|
|
entry["key_lines"] = [l for l in lines if re.search(r"FAIL|红项|Error|error", l)][-12:]
|
|
except Exception:
|
|
pass
|
|
with open(gates_dir + "/ci-status.json", "w", encoding="utf-8") as f:
|
|
json.dump(entry, f, ensure_ascii=False, indent=1)
|
|
with open(gates_dir + "/ci-history.jsonl", "a", encoding="utf-8") as f:
|
|
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
PY
|
|
|
|
# 轮尾补跑判定:跑批期间有新触发(标记在)或尖端前移 → 对最新尖端再来一轮
|
|
_newtip="$(git -C "$REPO" rev-parse HEAD)"
|
|
if [ ! -f "$RERUN" ] && [ "$_newtip" = "$TIP" ]; then
|
|
exit 0
|
|
fi
|
|
done
|