Merge branch 'master' into qa-harness

# Conflicts:
#	.gitattributes
This commit is contained in:
DKChan committed 2026-11-06 05:24:31 +08:00
commit be0cab8356
6 files changed
+94 -20

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# 合回前置检查钩子(git pre-merge-commit,2026-11-06 用户裁)——主工作区 git merge 落合并提交前
# 跑最小验证面:tools/run_all_gates.sh --premerge(冒烟测试+单元与集成全套件 4 分片,热导入缓存约 2 分钟)。
# 语义(git 2.45.2.windows.1 本机实测):本钩子拦的是「合并提交」不是「合并动作」——exit≠0 时合并结果
# 已进暂存区、仓库停 MERGE_HEAD 待提交态;处置二选一:修红后 git commit 续合,或 git merge --abort 放弃。
# 快进合并不触发本钩子(git 语义)——本仓合回铁规恒 --no-ff,漏写时远端 CI 兜底仍在。
# --no-verify 可整体跳过(实测)——仅属用户,agent 禁用(worktree-guard 在案)。
# 仅主工作区触发(worktree 内合并不触发,与 post-merge 同款 git-dir 判别)。
# 冷缓存注意:本机首次跑批前先刷导入($GODOT --headless --path game --editor --quit,约 20 秒),
# 否则冷导入可致套件内建 120 秒超时假红;主工作区 .godot 缓存常驻,日常免此步。
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 -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO/tools/run_all_gates.sh" --premerge
RC=$?
if [ "$RC" -ne 0 ]; then
echo "" >&2
echo "[pre-merge-commit] ✗ 前置检查红(读数如上)——合并提交未落,仓库停 MERGE_HEAD 待提交态。" >&2
echo " 处置二选一:① 修红后 git commit 续合本合并;② 放弃整次合并=git merge --abort。" >&2
exit 1
fi
echo "[pre-merge-commit] ✓ 前置检查绿——合并提交继续。"
exit 0
+45 -11
View File
@@ -9,7 +9,11 @@
# 只服务开发期按域跑批;--gates 档另为远端纯闸件/管线批的执行形态(domain_route.sh 判 gates → ci.yml 映射 --gates)。
# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节)。
#
# 用法:tools/run_all_gates.sh [--code|--docs|--gates|--quick]
# 用法:tools/run_all_gates.sh [--code|--docs|--gates|--premerge|--quick]
# --premerge 合回前置检查档(2026-11-06 用户裁):只跑冒烟测试+单元与集成测试全套件(4 分片,
# 热导入缓存约 2 分钟)——主工作区 git merge --no-ff 落合并提交前由 pre-merge-commit
# 钩子自动调用的拦截面。退出码聚合即判定(三硬闸口径),不落回执、不比基线——
# 验证台账归合回后远端 CI,本档只做当场拦截。快进合并不触发钩子;--no-verify 仅属用户。
# --code 只跑代码域闸(冒烟测试/系统测试/单元与集成测试/场景探针/
# 渲染闸/静态扫描+熵增闸)
# ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。
@@ -56,10 +60,10 @@ DOMAIN="full"
while [ $# -gt 0 ]; do
case "$1" in
--quick) QUICK=1 ;;
--code|--docs|--gates)
[ "$DOMAIN" = "full" ] || { echo "用法: --code/--docs/--gates 互斥"; exit 2; }
--code|--docs|--gates|--premerge)
[ "$DOMAIN" = "full" ] || { echo "用法: --code/--docs/--gates/--premerge 互斥"; exit 2; }
DOMAIN="${1#--}" ;;
*) echo "用法: tools/run_all_gates.sh [--code|--docs|--gates|--quick]"; exit 2 ;;
*) echo "用法: tools/run_all_gates.sh [--code|--docs|--gates|--premerge|--quick]"; exit 2 ;;
esac
shift
done
@@ -67,7 +71,7 @@ done
# 每个代码面 worktree 用独立 Godot user 目录(game/override.cfg 由 setup 脚本自愈生成;主区 no-op)。
# 并行批共用一套夹具、日志与截图目录会互相踩盘造成假红——隔离建不起来就拒跑(fail-closed)。
# 隔离只服务代码域(Godot 运行面);文档域与闸件自检档不启引擎,跳过省 0.8 秒。
if [ "$DOMAIN" = "code" ] || [ "$DOMAIN" = "full" ]; then
if [ "$DOMAIN" = "code" ] || [ "$DOMAIN" = "full" ] || [ "$DOMAIN" = "premerge" ]; then
"$REPO/tools/setup_worktree_userdir.sh" "$REPO" || {
echo "[gates] FAIL — user 目录隔离未建立,拒绝在共享目录上跑闸。"
exit 1
@@ -82,7 +86,8 @@ export PYTHONIOENCODING=utf-8
# 代码面(game/ 下非 .md 的行为载体)是否带着未提交改动(只算已跟踪文件——闸件自身产物都落在
# gitignore 面)。脏树跑出来的回执记的是 HEAD,验的却是别的字节,回执标 dirty_game 由 CI 落账留痕。
DIRTY_GAME=0
if [ -n "$(git -C "$REPO" -c core.quotepath=false diff HEAD --name-only -- game/ 2>/dev/null | grep -viE '\.md$')" ]; then
# premerge 档不查脏树:钩子跑在合并提交落笔前,合并内容本就未提交(diff HEAD=待合内容),无回执可比。
if [ "$DOMAIN" != "premerge" ] && [ -n "$(git -C "$REPO" -c core.quotepath=false diff HEAD --name-only -- game/ 2>/dev/null | grep -viE '\.md$')" ]; then
DIRTY_GAME=1
echo "[gates] 警告:代码面(game/ 下非 .md)有未提交改动——本次回执将标 dirty_game,不能用于合回(先提交再跑)。"
fi
@@ -130,7 +135,7 @@ in_domain() { # in_domain <code|docs|both>
# 所以 git worktree 检出的工程天生缺全部美术素材:main_theme.tres 解析失败 → 按钮五个样式盒全空
# → 冒烟 V1 成片假红;渲染闸又会因素材缺位把两段 golden 直接跳过(见 render_gate.sh)。
# 缺位即拒跑——禁带着空盘跑完,把「什么都没比」的回执当全绿合回。
if in_domain code; then
if in_domain code || [ "$DOMAIN" = "premerge" ]; then
_miss=""
for a in assets/art/ui/button_stone.png assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/board/seal_mark.png assets/art/character/character_hero_idle.png \
@@ -405,8 +410,8 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分
# ══ 闸件自检段(gates 档专属)══
# 闸壳自检:bash -n 过全部 bash 闸件与管线件——闸件批改的就是这些件,语法坏=下一批真跑闸时才炸,
# 闸件批当场拦住。收件面动态收全:tools/ 全部 .sh+game/tests 顶层 .sh(静态扫描)+hooks/post-merge
# (无后缀件);新增 bash 闸件自动入检面,无需维护清单。
# 闸件批当场拦住。收件面动态收全:tools/ 全部 .sh+game/tests 顶层 .sh(静态扫描)+hooks/ 无后缀件
# (post-merge/pre-merge-commit);新增 bash 闸件自动入检面,无需维护清单。
gate_shell_selfcheck() {
local name="闸壳自检"
local rc=0 _n=0 _bad=0 _s
@@ -414,7 +419,7 @@ gate_shell_selfcheck() {
while IFS= read -r _s; do [ -n "$_s" ] && _shells+=("$_s"); done < <(
{ find "$REPO/tools" -name '*.sh' 2>/dev/null
find "$REPO/game/tests" -maxdepth 1 -name '*.sh' 2>/dev/null
echo "$REPO/tools/hooks/post-merge"; } | sort -u
find "$REPO/tools/hooks" -maxdepth 1 -type f ! -name '*.sh' 2>/dev/null; } | sort -u
)
echo ""
echo "════════════════════════════════════════════"
@@ -442,6 +447,15 @@ gate_shell_selfcheck() {
return 0
}
# ══ 合回前置检查档(premerge,2026-11-06 用户裁)══
# 冒烟+单元与集成全套件(4 分片)——合回 merge 点的最小人工拦截面;不是全量、不替代远端 CI。
# 选闸推导整段跳过(本档不按 diff 选闸——合回是最后拦截点,恒全套件);退出码聚合即判定,
# 不落回执不比基线(回执与基线面归远端 CI,本档读数只进合并现场)。
if [ "$DOMAIN" = "premerge" ]; then
gate "冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py"
gate_unit
fi
# ══ 代码域 ══
if in_domain code; then
# ── 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)──
@@ -493,7 +507,10 @@ fi
# ── 静态扫描(代码域与闸件自检档)+ 熵增闸(各档都跑:它查顶层目录/根文件/图像白名单/退役路径,
# 文档批新增或挪动 design/ 下的文件同样会绊它;纯文本扫描,秒级)──
if [ $QUICK -eq 0 ]; then
# premerge 档不入此面:前置检查只盯冒烟+单元,静态扫描与熵增闸由合回后远端 CI 盯。
if [ "$DOMAIN" = "premerge" ]; then
echo ""; echo "[gates] 静态扫描/熵增闸 → 不入前置检查面(premerge;闸况归合回后远端 CI)"
elif [ $QUICK -eq 0 ]; then
if in_domain code || [ "$DOMAIN" = "gates" ]; then
gate "静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh"
fi
@@ -506,6 +523,23 @@ else
echo ""; echo "[gates] 静态扫描(代码域)/ 熵增闸 → SKIP(--quick)"
fi
# ── premerge 档汇总:退出码聚合即判定(三硬闸口径),不落回执不比基线 ──
if [ "$DOMAIN" = "premerge" ]; then
RC=0
for _e in "${EXITS[@]}"; do
[ "$_e" -ne 0 ] && RC=1
done
echo ""
echo "════════════════════════════════════════════"
echo " 前置检查汇总(premerge——退出码即判定,不落回执)"
for _i in "${!NAMES[@]}"; do
_s="绿"; [ "${EXITS[$_i]}" -ne 0 ] && _s="红"
printf ' %s:%s(exit=%s)\n' "${NAMES[$_i]}" "$_s" "${EXITS[$_i]}"
done
echo "════════════════════════════════════════════"
exit $RC
fi
# ── 与存量基线比对 ──
"$PY" - "$BASELINE" "$RECEIPT" "$QUICK" "$DOMAIN" "$REPO" "$DIRTY_GAME" "$SCOPE" "$ONLY_TERMS" "${NAMES[@]}" -- "${EXITS[@]}" -- "${LOGS[@]}" <<'PYEOF'
import hashlib, json, os, re, subprocess, sys