iter50-卡1:卡A(敌方状态个体化): 容器换代owner_key多桶(个体键单点/授予写点3处/死亡清/消费面随动/呈现逐卡)+断言V1-V8+armor_roll申报换代(个体键读数语义保留)+主角侧回归绿; 单元71套件+冒烟15项全绿

This commit is contained in:
DKChan committed 2026-11-03 06:31:04 +08:00
1 parent ab5d75a208
commit e658492703
300 files changed
+609 -6039

No files matched your search

-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env python3
r"""chunk_dl.py — 分片并行下载器(断点续传),2026-11-03 CI 迁移批收编。
背景:GitHub 直连在本项目两台机器均不可用,加速镜像站速度慢且会掐连接/回错误页。
本件把大文件切成 N 片经镜像并行拉取(单片断了自动续传重试),全部齐后拼合。
已实证:74MB Godot Linux 包约 15 分钟拉完(2026-11-03 mac_setup 依赖此件拉 Godot macOS 包)。
用法: python3 tools/ci/chunk_dl.py <URL> <输出路径> [分片数=16]
镜像回落顺序:直连 → gh-proxy.net → gh-proxy.com(HEAD 探测 Content-Length 决定实际用的源)。
校验:分片按字节范围落盘、按序拼合;zip 结构校验由调用方(mac_setup.sh 的 unzip -tq)承担。
退出码:0=成功 1=失败。
"""
import concurrent.futures as cf
import os
import subprocess
import sys
MIRRORS = ["", "https://gh-proxy.net/", "https://gh-proxy.com/"]
def pick_mirror(url: str):
"""HEAD 探测取 Content-Length + 前 4 字节 zip 魔数校验(镜像站会回 200 错误页体,
只看 Content-Length 会把 195 字节的假体当真——2026-11-03 gh-proxy.net 实证)。
返回(实际可用 URL, 字节数)或 (None, 0)。非 zip 目标禁用本件。"""
for m in MIRRORS:
u = m + url
r = subprocess.run(["curl", "-sIL", "-m", "30", u], capture_output=True, text=True)
size = 0
for line in r.stdout.splitlines():
if line.lower().startswith("content-length:"):
try:
size = int(line.split(":", 1)[1].strip())
except ValueError:
size = 0
if size <= 65536:
continue
head = subprocess.run(
["curl", "-sL", "-m", "20", "-r", "0-3", "-o", "-", u],
capture_output=True)
if head.stdout[:2] != b"PK":
continue
return u, size
return None, 0
def fetch(job) -> bool:
i, s, e, url, out = job
p = "%s.part%02d" % (out, i)
want = e - s + 1
for _attempt in range(200):
have = os.path.getsize(p) if os.path.exists(p) else 0
if have >= want:
return True
r0 = s + have
subprocess.run(
["curl", "-sL", "--speed-limit", "1024", "--speed-time", "30",
"-m", "600", "-r", "%d-%d" % (r0, e), "-o", p + ".new", url],
capture_output=True)
if os.path.exists(p + ".new"):
with open(p, "ab") as f, open(p + ".new", "rb") as g:
f.write(g.read())
os.remove(p + ".new")
return False
def main() -> int:
if len(sys.argv) < 3:
print("用法: python3 chunk_dl.py <URL> <输出路径> [分片数=16]")
return 1
url, out = sys.argv[1], sys.argv[2]
n = int(sys.argv[3]) if len(sys.argv) > 3 else 16
real, size = pick_mirror(url)
if not real:
print("全部下载源失败")
return 1
print("size=%d url=%s" % (size, real), flush=True)
chunk = (size + n - 1) // n
jobs = [(i, i * chunk, min((i + 1) * chunk, size) - 1, real, out)
for i in range(n) if i * chunk < size]
with cf.ThreadPoolExecutor(n) as ex:
for ok in ex.map(fetch, jobs):
if not ok:
print("分片重试耗尽")
return 1
with open(out, "wb") as f:
for i, _, _, _, _ in jobs:
with open("%s.part%02d" % (out, i), "rb") as g:
f.write(g.read())
for i, _, _, _, _ in jobs:
os.remove("%s.part%02d" % (out, i))
print("done %d" % os.path.getsize(out))
return 0
if __name__ == "__main__":
sys.exit(main())
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env bash
# 乙案域分流——push 触发的远端 CI 按改动域选闸(2026-11-03 用户裁)。
# 用法: tools/ci/domain_route.sh <base_sha> <head_sha>
# 输出: stdout 打印 full|docs|skip;GITHUB_OUTPUT 在位时另写 mode= 行(Actions 步骤输出)。
# 规则(唯一事实源,乙案语义):
# 1. diff 含 game/ 下非 .md 文件(行为载体:代码/场景/数据表/素材)→ full(两域全量,渲染闸随 code 域)
# 2. 否则 diff 非空(design/ 文档、reports/、根 md、game/ 下 .md 等一切非代码面)→ docs
# (乙案:文档批只跑文档域秒级闸——gdd_lint/doc_link/doc_term/pipeline/doc_ref/capability+熵增)
# 例外:diff 只含 tools/ 与 .gitea/(闸件与管线自身改动)→ skip——闸件改动不占合回全量,
# 质量义务=落批人工确认可跑通(run_all_gates.sh 头注既有口径,不在远端重复烧引擎)。
# 3. diff 空 → skip。
# 判定面说明:game/tests/** 与 game/sim/** 属 game/ 行为载体(.md 除外)→ full,
# 与 run_all_gates.sh 关联档口径一致(测量面批 02 照跑全套件)。
set -u
base="${1:-}"
head="${2:-HEAD}"
[ -n "$base" ] || { echo "用法: domain_route.sh <base_sha> <head_sha>" >&2; exit 2; }
head="$(git rev-parse --verify "${head}^{commit}" 2>/dev/null)" || { echo "坏 head: $head" >&2; exit 2; }
# base 不可用(新分支 zero-sha/被 force-push 掉)→ 回落 HEAD~1;孤提交(无父)→ docs 保守最小跑。
git cat-file -e "$base^{commit}" 2>/dev/null || base="$(git rev-parse "$head~1" 2>/dev/null || true)"
if [ -z "$base" ]; then
mode="docs"
else
files="$(git diff --name-only "$base" "$head" 2>/dev/null)"
if [ -z "$files" ]; then
mode="skip"
elif echo "$files" | grep -vE '\.md$' | grep -qE '^game/'; then
mode="full"
else
non_tools="$(echo "$files" | grep -vE '^(tools/|\.gitea/)' || true)"
if [ -n "$non_tools" ]; then mode="docs"; else mode="skip"; fi
fi
fi
if [ -n "${GITHUB_OUTPUT:-}" ]; then printf 'mode=%s\n' "$mode" >> "$GITHUB_OUTPUT"; fi
echo "$mode"
exit 0
-234
View File
@@ -1,234 +0,0 @@
#!/usr/bin/env bash
# mac_setup.sh — CI runner 自举件(Mac 本机 host 模式+专用账户图形会话;2026-11-03 用户裁)。
# 一条命令把一台 macOS 变成 gplan 的远端 CI runner:专用账户+自动登录图形会话+Godot+gdstyle+
# Pillow+act_runner 用户级常驻(LaunchAgent)。
# 形态要点(渲染闸的硬需求):渲染闸要真窗口 = 必须有图形会话(WindowServer 只存在于已登录会话)。
# 方案 = ci-runner 账户开机自动登录(owns 控制台图形会话)→ 该会话内 LaunchAgent 拉起 runner →
# 测试作业全部以 ci-runner 身份运行(无 admin、可写面限定);日常使用经「快速用户切换」到本人账户,
# 两会话并存互不干扰(后台会话保留约 1-2GB 内存;后台会话渲染行为在端到端验收第一轮实证)。
# 用法(sudo 必须——建账户/写 /etc/kcpassword/装 /usr/local/bin 都要 root):
# sudo bash tools/ci/mac_setup.sh [--godot-zip <本地已下载的 macOS 版 zip 路径>]
# 环境前提:
# ① macOS(Intel/Apple Silicon 皆可,Godot 走 universal 包)② Xcode Command Line Tools 已装
# (python3/git 在位;缺则先 xcode-select --install)③ 调用账户的 ~/gitea-runner/ 下已有
# act_runner(darwin 包,gitea.com gitea/runner releases)与已注册的 .runner(一次性注册动作)。
# 幂等:重复执行只补缺件;账户密码首次生成后存 /var/db/gplan-ci-runner.pw(root 600),重跑复用。
set -euo pipefail
GODOT_VER="4.7.2"
GODOT_URL="https://github.com/godotengine/godot/releases/download/${GODOT_VER}-stable/Godot_v${GODOT_VER}-stable_macos.universal.zip"
GDSTYLE_VER="0.4.0"
GDSTYLE_URL="https://github.com/atelico/gdstyle/releases/download/v${GDSTYLE_VER}/gdstyle-x86_64-apple-darwin.tar.gz"
MIRRORS=("" "https://gh-proxy.net/" "https://gh-proxy.com/") # 直连优先,镜像回落
CI_USER="ci-runner"
CI_HOME="/Users/${CI_USER}"
PW_FILE="/var/db/gplan-ci-runner.pw"
log() { echo "[mac_setup] $*"; }
die() { echo "[mac_setup] ✗ $*" >&2; exit 1; }
[ "$(uname -s)" = "Darwin" ] || die "仅支持 macOS"
[ "$(id -u)" -eq 0 ] || die "请用 sudo 运行(建账户/写 /etc/kcpassword/装 /usr/local/bin 需要 root)"
for t in git curl python3 unzip dscl; do command -v "$t" >/dev/null 2>&1 || die "缺 ${t}(先 xcode-select --install)"; done
INVOKER="${SUDO_USER:-$(whoami)}"
RUNNER_SRC="/Users/${INVOKER}/gitea-runner"
[ -x "${RUNNER_SRC}/act_runner" ] || die "缺 ${RUNNER_SRC}/act_runner——先从 gitea.com/gitea/runner releases 下载 darwin 包"
[ -f "${RUNNER_SRC}/.runner" ] || die "缺 ${RUNNER_SRC}/.runner——act_runner 尚未注册(register 子命令,见 Gitea 文档)"
GODOT_ZIP=""
while [ $# -gt 0 ]; do
case "$1" in
--godot-zip) GODOT_ZIP="${2:-}"; shift 2 ;;
*) die "未知参数: $1" ;;
esac
done
rm -f /tmp/godot_mac_XXXXXX.zip /tmp/gdstyle_XXXXXX.tar.gz 2>/dev/null || true # 早期 BSD-mktemp 缺陷的字面残留清理
# ── 1. 专用账户+密码落盘(自动登录需要密码编码进 /etc/kcpassword,root 600)──
if dscl . -read "${CI_HOME}" NFSHomeDirectory >/dev/null 2>&1; then
log "账户 ${CI_USER} 已存在"
[ -f "$PW_FILE" ] || die "账户已存在但 $PW_FILE 缺失(无法配置自动登录)——删除账户重跑,或手工补密码文件"
CI_PW="$(cat "$PW_FILE")"
else
CI_PW="$(uuidgen | tr -d '-')"
/usr/sbin/sysadminctl -addUser "$CI_USER" -password "$CI_PW" -home "$CI_HOME" \
|| die "账户创建失败(sysadminctl)"
printf '%s' "$CI_PW" > "$PW_FILE"
chmod 600 "$PW_FILE"
log "账户 ${CI_USER} 已创建(密码仅存 ${PW_FILE},root 600,不对外告知)"
fi
# 家目录兜底:sysadminctl 只登记不创建(首次运行输出 "not created!" 在案)——家目录缺失时
# 自动登录与任何以该用户执行的操作都会失败(Pillow --user 失败即此根因)。
if [ ! -d "${CI_HOME}" ]; then
mkdir -p "${CI_HOME}"
chown -R "${CI_USER}:staff" "${CI_HOME}"
createhomedir -c -u "$CI_USER" >/dev/null 2>&1 || true
log "家目录 ${CI_HOME} 已补建"
fi
# ── 2. 自动登录(ci-runner 拥有控制台图形会话——渲染闸的窗口来源)──
_KCP="/etc/kcpassword"
_need_kcp=1
[ -f "$_KCP" ] && _need_kcp=0
if defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null | grep -q "$CI_USER"; then
log "自动登录已指向 ${CI_USER},跳过"
_need_kcp=0
fi
if [ "$_need_kcp" = 1 ]; then
python3 - "$CI_PW" <<'PYEOF'
import sys
pwd = sys.argv[1].encode()
key = bytes([0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F])
enc = bytes(b ^ key[i % len(key)] for i, b in enumerate(pwd))
enc += b"\x00" * ((12 - len(enc) % 12) % 12 + 1)
open("/etc/kcpassword", "wb").write(enc)
PYEOF
chmod 600 /etc/kcpassword
defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "$CI_USER"
log "自动登录已配置:开机直接进入 ${CI_USER} 图形会话(日常使用=快速用户切换到本人账户)"
fi
# ── 3. Godot macOS 版 → 完整 .app 装 /usr/local/lib + /usr/local/bin/godot 包装转发 ──
# 纪律:官方签名封 bundle 的 Info.plist——从 .app 抽裸二进制会被 taskgated 判
# 「Code Signature Invalid」SIGKILL(2026-11-03 实证),必须整 bundle 安装保持签名校验路径。
if /usr/local/bin/godot --version >/dev/null 2>&1 && /usr/local/bin/godot --version 2>/dev/null | grep -q "$GODOT_VER"; then
log "Godot ${GODOT_VER} 已在位,跳过"
else
ZIP=""
SRC_ZIP="/Users/${INVOKER}/godot-linux/Godot_v${GODOT_VER}-stable_macos.universal.zip"
if [ -n "$GODOT_ZIP" ] && [ -s "$GODOT_ZIP" ]; then
ZIP="$GODOT_ZIP"; log "使用本地 zip: $ZIP"
elif [ -z "$GODOT_ZIP" ] && [ -s "$SRC_ZIP" ]; then
ZIP="$SRC_ZIP"; log "复用已下载 zip: $ZIP"
else
ZIP="$(mktemp "${TMPDIR:-/tmp}/godot_mac_XXXXXX")" # macOS BSD mktemp 禁 X 后带后缀
_ok=0
for m in "${MIRRORS[@]}"; do
log "下载 Godot(${m:+经 $m})"
# 镜像站可能回 200 的错误页体——下载后必须验 zip 结构,假成功当失败处理
if curl -fL --retry 2 -m 1800 -o "$ZIP" "${m}${GODOT_URL}" && unzip -tq "$ZIP" >/dev/null 2>&1; then
_ok=1; break
fi
log "该源失败,换下一源"
done
[ "$_ok" = 1 ] || die "全部下载源失败——手动下载 ${GODOT_URL} 后重跑并加 --godot-zip <路径>;或用 tools/ci/chunk_dl.py 分片拉取"
fi
TMPD="$(mktemp -d)"
unzip -tq "$ZIP" >/dev/null # 双保险:本地复用的 zip 同样验结构
unzip -q -o "$ZIP" -d "$TMPD"
mkdir -p /usr/local/lib
rm -rf "/usr/local/lib/Godot_ci.app"
cp -R "${TMPD}/Godot.app" "/usr/local/lib/Godot_ci.app"
xattr -dr com.apple.quarantine "/usr/local/lib/Godot_ci.app" 2>/dev/null || true
cat > /usr/local/bin/godot <<'EOF'
#!/bin/bash
exec /usr/local/lib/Godot_ci.app/Contents/MacOS/Godot "$@"
EOF
chmod 0755 /usr/local/bin/godot
rm -rf "$TMPD"
log "Godot 已装:$(/usr/local/bin/godot --version 2>/dev/null | head -1)"
fi
# ── 4. gdstyle v0.4.0 darwin → /usr/local/bin/gdstyle(S13 静态扫描闸件,版本钉死)──
if /usr/local/bin/gdstyle --version >/dev/null 2>&1; then
log "gdstyle 已在位,跳过"
else
TGZ="$(mktemp "${TMPDIR:-/tmp}/gdstyle_XXXXXX")" # macOS BSD mktemp 禁 X 后带后缀
SRC_TGZ="/Users/${INVOKER}/godot-linux/gdstyle-x86_64-apple-darwin.tar.gz"
if [ -s "$SRC_TGZ" ] && tar -tzf "$SRC_TGZ" >/dev/null 2>&1; then
TGZ="$SRC_TGZ"; log "复用已下载 gdstyle 包: $TGZ"
else
_ok=0
for m in "${MIRRORS[@]}"; do
log "下载 gdstyle v${GDSTYLE_VER} darwin(${m:+经 $m})"
if curl -fL --retry 2 -m 600 -o "$TGZ" "${m}${GDSTYLE_URL}" && tar -tzf "$TGZ" >/dev/null 2>&1; then _ok=1; break; fi
done
[ "$_ok" = 1 ] || die "gdstyle 下载失败——手动下载 ${GDSTYLE_URL} 解压放 /usr/local/bin/gdstyle 后重跑"
fi
TMPD="$(mktemp -d)"
tar -xzf "$TGZ" -C "$TMPD"
BIN="$(find "$TMPD" -type f -name 'gdstyle*' | head -1)"
[ -n "$BIN" ] || die "压缩包内未找到 gdstyle 二进制"
install -m 0755 "$BIN" /usr/local/bin/gdstyle
xattr -dr com.apple.quarantine /usr/local/bin/gdstyle 2>/dev/null || true
rm -rf "$TMPD"
log "gdstyle 已装:/usr/local/bin/gdstyle"
fi
# ── 5. Pillow(渲染闸依赖)——系统级安装(/Library/Python,全账户可导入),
# 不用 --user:ci-runner 家目录时序敏感,系统级绕开全部家目录问题 ──
if python3 -c "import PIL" >/dev/null 2>&1; then
log "Pillow 已在位(系统级),跳过"
else
log "系统级安装 Pillow(阿里源)"
python3 -m pip install pillow \
-i https://mirrors.aliyun.com/pypi/simple/ --quiet \
|| die "Pillow 系统级安装失败"
fi
# ── 6. runner 程序与注册文件就位(复制既有注册身份,免重新注册)──
mkdir -p "${CI_HOME}/gitea-runner" "${CI_HOME}/Library/LaunchAgents"
cp -f "${RUNNER_SRC}/act_runner" "${CI_HOME}/gitea-runner/act_runner"
cp -f "${RUNNER_SRC}/.runner" "${CI_HOME}/gitea-runner/.runner"
cp -f "${RUNNER_SRC}/config.yaml" "${CI_HOME}/gitea-runner/config.yaml" 2>/dev/null || true
python3 - "${CI_HOME}/gitea-runner/.runner" <<'PYEOF'
import json, sys
p = sys.argv[1]
d = json.load(open(p))
d["labels"] = ["ci-mac:host"]
json.dump(d, open(p, "w"))
print("labels ->", d["labels"])
PYEOF
sed -i '' 's/^ level: debug/ level: info/' "${CI_HOME}/gitea-runner/config.yaml" 2>/dev/null || true
chown -R "${CI_USER}:staff" "${CI_HOME}/gitea-runner" "${CI_HOME}/Library/LaunchAgents"
# ── 7. LaunchAgent(用户级常驻,挂在 ci-runner 图形会话内——渲染闸窗口的来源)──
PLIST="${CI_HOME}/Library/LaunchAgents/com.gitea.act-runner.plist"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.gitea.act-runner</string>
<key>ProgramArguments</key>
<array>
<string>${CI_HOME}/gitea-runner/act_runner</string>
<string>daemon</string>
</array>
<key>WorkingDirectory</key><string>${CI_HOME}/gitea-runner</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>${CI_HOME}/gitea-runner/daemon.log</string>
<key>StandardErrorPath</key><string>${CI_HOME}/gitea-runner/daemon.log</string>
</dict>
</plist>
EOF
chown "${CI_USER}:staff" "$PLIST"
_uid="$(id -u "$CI_USER")"
if launchctl asuser "$_uid" launchctl bootstrap "gui/$_uid" "$PLIST" 2>/dev/null; then
log "LaunchAgent 已在 ${CI_USER} 会话内启动"
_started=1
else
_started=0
log "当前无 ${CI_USER} 图形会话——LaunchAgent 已就位,重启后随自动登录自动拉起"
fi
# ── 8. 退役 Docker 版 runner(如存在)──
if command -v docker >/dev/null 2>&1; then
docker rm -f gitea-runner >/dev/null 2>&1 && log "Docker 版 runner 容器已退役" || log "无 Docker 版 runner 容器(或 docker 未运行)"
fi
# ── 9. 验证与收尾提示 ──
if [ "$_started" = 1 ]; then
sleep 5
grep -q "declare successfully" "${CI_HOME}/gitea-runner/daemon.log" 2>/dev/null \
&& log "runner 已向 Gitea 声明成功" || log "runner 已启动,声明确认见 ${CI_HOME}/gitea-runner/daemon.log"
else
log "⚠ 重启 Mac 后自动登录生效、runner 随会话拉起——重启后验证第①条"
fi
log "全部完成。验证清单:"
log " ① Gitea 网页 → 仓库设置 → Actions → Runners 应显示 mac-ci 在线(Idle,标签 ci-mac)"
log " ② sudo -u ${CI_USER} /usr/local/bin/godot --headless --version"
log " ③ sudo -u ${CI_USER} python3 -c 'import PIL'"
log " ④ 日常使用:登录窗/快速用户切换到本人账户(ci-runner 会话在后台保持)"
log " ⑤ 端到端:下一次合回 master 的推送自动触发跑批"
+97
View File
@@ -0,0 +1,97 @@
#!/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
+49 -118
View File
@@ -1,126 +1,57 @@
#!/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)。
# 回执等待件——合回后等 CI 回执的后台轮询器(2026-11-02 用户裁;规则面=process-conventions §1 第 3 条「回执等待纪律」)。
# 用法: tools/ci_watch.sh <合并提交hash> [超时秒,默认 900]
# 调用面: 合回落地先向用户呈报本批总结,随即以 run_in_background 后台任务运行本件;命令结束时
# harness 唤醒会话——绿→删区收口;红→呈报红闸名单走修复批;超时→呈报「回执未落」(CI 可能崩溃)。
# 「本次回执已落」认领判据(三条全满足,防认错轮):
# ① 回执可整读——CI 写回执非原子,半截/读空静默重试;
# ② 回执 commit 为传入提交或其后代——另一会话合回推进尖端触发补跑覆盖时按后代认领(coalesce 语义见 ci_runner.sh);
# ③ 回执 timestamp 晚于合并提交时刻——排除合并前盘上的旧回执(旧尖端是合并提交的祖先,只查②会误领旧回执)。
# 退出码: 0=绿 1=红 2=超时/回执未落 3=参数错误
set -u
# 主工作区定位:CI 只在主工作区跑(post-merge 钩子限主区),从任务 worktree 内调用也要读主区回执——
# 经 git 公共目录(.git)的上级定位主仓根,主区调用与 worktree 调用同一读数
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}"
STATUS="$REPO/game/tests/.gates/ci-status.json"
BASE="${1:-}"
TIMEOUT="${2:-900}"
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; }
[ -n "$BASE" ] || { echo "用法: ci_watch.sh <合并提交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
BASE_CT="$(git -C "$REPO" log -1 --format=%ct "$BASE")"
_deadline=$(( $(date +%s) + TIMEOUT ))
while :; do
_read="$(python -I -c "
import json, sys, datetime
try:
d = json.load(open(sys.argv[1], encoding='utf-8'))
ts = int(datetime.datetime.fromisoformat(d['timestamp']).timestamp())
print(d.get('commit', ''), d.get('verdict', ''), ts)
except Exception:
pass
" "$STATUS")"
_com=""; _verdict=""; _ts=""
if [ -n "$_read" ]; then
set -- $_read
[ $# -eq 3 ] && { _com="$1"; _verdict="$2"; _ts="$3"; }
fi
if [ -n "$_com" ] && [ -n "$_verdict" ] && [ -n "$_ts" ] \
&& [ "$_ts" -gt "$BASE_CT" ] \
&& git -C "$REPO" merge-base --is-ancestor "$BASE" "$_com" 2>/dev/null; then
python -I -c "
import json, sys
d = json.load(open(sys.argv[1], encoding='utf-8'))
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)
" "$STATUS"
[ "$_verdict" = "green" ] && exit 0
exit 1
fi
_now=$(date +%s); _left=$((_deadline - _now))
[ "$_left" -le 0 ] && { echo "超时(${TIMEOUT}s)回执未落——CI 可能崩溃或未触发,查 game/tests/.gates/ 的 ci.lock 与 ci.log"; exit 2; }
sleep $(( _left < INTERVAL ? _left : INTERVAL ))
done
-97
View File
@@ -1,97 +0,0 @@
#!/usr/bin/env bash
# 闸件跨平台件——平台探测/路径形式/带超时执行/Godot 孤儿清杀,Git Bash 与 macOS/Linux 双栖。
# 单点定义:run_all_gates.sh / probe_gate.sh / render_gate.sh / golden_baseline_refresh.sh 共用(source 引入)。
# 纪律:闸脚本禁裸调 coreutils timeout 与 Windows 专用件(taskkill/PowerShell/ps -W/tasklist/cygpath)——
# 一律走本件封装;macOS 为 BSD 工具链(无 coreutils timeout,sed/stat 语义不同),裸调必断
# (2026-11-03 远端 CI 迁移裁决:runner=Mac 本机 host 模式,禁再增 Windows 专用直调)。
# 平台判定以 uname 为准(Git Bash=MINGW*,macOS=Darwin,Linux=Linux)。
gate_is_win() {
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*|Windows_NT) return 0 ;;
*) return 1 ;;
esac
}
# Godot --path 接受的项目路径形式:Windows 需 cygpath -m 混合斜杠,其他平台原样。
gate_game_path() {
if gate_is_win; then cygpath -m "$1"; else printf '%s' "$1"; fi
}
# 杀整棵进程树(<pid>)——递归杀子进程后杀本进程。macOS/Linux 走 pgrep -P;
# Windows 侧 MSYS kill 对其可见进程有效,带 wrapper 树的场景仍由调用方原 taskkill /T 逻辑负责。
gate_kill_tree() {
local p c
kill -9 "$1" 2>/dev/null
for c in $(pgrep -P "$1" 2>/dev/null); do gate_kill_tree "$c"; done
}
# 带超时执行(coreutils timeout 的纯 bash 替代,双平台同语义):
# gate_run_timeout <秒> <日志路径> <命令...>
# 命令输出重定向到日志;退出码=命令退出码,超时=124(与 coreutils 语义一致,判据与既有闸一致)。
# 实现=后台启动+守卫落标志文件(probe_gate run_suite 同款手法:taskkill 强杀后 wait 报 0 的
# 假绿以标志文件纠偏);超时先杀整树再回 124。
gate_run_timeout() {
local t="$1" log="$2"; shift 2
local kf
kf="$(mktemp "${TMPDIR:-/tmp}/gatetime_XXXXXX")"
"$@" >"$log" 2>&1 &
local pid=$!
( sleep "$t" 2>/dev/null
touch "$kf"
gate_kill_tree "$pid"
true ) &
local guard=$!
wait "$pid"; local st=$?
kill "$guard" 2>/dev/null; wait "$guard" 2>/dev/null
if [ -f "$kf" ]; then
gate_kill_tree "$pid"
st=124
fi
rm -f "$kf"
return "$st"
}
# 列命令行含 <game路径> 的 Godot 进程 PID(孤儿清杀与批末残留清点共用)。
# Windows:PowerShell WMI 按进程名+命令行匹配(console wrapper+真引擎都算);
# macOS/Linux:ps 全表按「可执行含 godot+命令行含本 game 路径」过滤(ps 两平台同形)。
gate_orphans() {
local gp="$1"
if gate_is_win; then
local tmpd tmpf
tmpd="$(mktemp -d "${TMPDIR:-/tmp}/gateorph_XXXXXX")" # macOS BSD mktemp 禁 X 后带后缀(本分支 Windows 专用,模板统一防复用)
tmpf="$tmpd/k.ps1"
local gp_win
gp_win="$(cygpath -m "$gp")"
printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$gp_win" > "$tmpf"
powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$tmpf")" 2>/dev/null | tr -d '\r'
rm -rf "$tmpd"
else
ps -axo pid=,command= 2>/dev/null | awk -v g="$gp" 'tolower($0) ~ /godot/ && index($0, g) {print $1}'
fi
}
gate_kill_orphans() { # gate_kill_orphans <game路径>
local p
for p in $(gate_orphans "$1"); do
if gate_is_win; then taskkill //F //PID "$p" >/dev/null 2>&1
else kill -9 "$p" 2>/dev/null; fi
done
}
# 全局 Godot 进程计数(其他会话的仅报数不算账——批末残留清点的对照面)。
gate_godot_count_all() {
if gate_is_win; then tasklist 2>/dev/null | grep -ci '^Godot' || true
else ps -axo comm= 2>/dev/null | grep -ci 'godot' || true
fi
}
# golden 基线平台子目录名——渲染基线按平台各存一份(Windows 显卡字形/抗锯齿与 macOS CoreText
# 差异使跨平台逐像素比对必失配,2026-11-03 用户裁:按平台各存基线,平台内部照常严格比对)。
gate_platform_id() {
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*|Windows_NT) echo "windows" ;;
Darwin) echo "mac" ;;
*) echo "linux" ;;
esac
}
+1 -4
View File
@@ -48,10 +48,7 @@ CONFIG = ROOT / "tools" / "gdstyle.toml"
BASELINE = ROOT / "tools" / "gdstyle_baseline.txt"
SCAN_DIRS = ["game/core", "game/data", "game/presentation"]
PINNED_VERSION = "0.4.0"
# 二进制定位:GDSTYLE_BIN 环境变量优先;Windows 本机固定路径保底,macOS/Linux 走 /usr/local/bin
# (mac_setup.sh 自举件装钉定版 v0.4.0 darwin 包至此,远端 CI 与本机同判据)。
DEFAULT_BIN = ("D:/code/gdstyle/gdstyle.exe" if sys.platform == "win32"
else "/usr/local/bin/gdstyle")
DEFAULT_BIN = "D:/code/gdstyle/gdstyle.exe"
MAX_REPORT = 50
MAX_VANISHED_REPORT = 20
SHRINK_GUARD = 0.9 # 新键数 < 旧键数 * 本系数=骤降,refresh 拒写须 --force
Executable → Regular
+13 -15
View File
@@ -15,7 +15,6 @@
set -u
G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
REPO="$(cd "$(dirname "$0")/.." && pwd)"
. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行)
GAME=""
MODE="board"
for a in "$@"; do
@@ -25,8 +24,7 @@ for a in "$@"; do
esac
done
GAME="${GAME:-$REPO/game}"
GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样)
PLAT="$(gate_platform_id)" # 基线落点=本平台子目录(game/tests/golden/<windows|mac|linux>/)
GAME_WIN="$(cygpath -m "$GAME")"
TMP="$(mktemp -d /tmp/goldenrefresh_XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
@@ -34,20 +32,20 @@ if [ "$MODE" = "menu" ]; then
for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/ui/button_stone.png; do
if [ ! -f "$GAME/$a" ]; then
echo "[golden_refresh] 拒跑 — 菜单素材缺位: ${a}(S3 停机点口径:基线禁锚回退态,先补素材+导入边车)"
echo "[golden_refresh] 拒跑 — 菜单素材缺位: $a(S3 停机点口径:基线禁锚回退态,先补素材+导入边车)"
exit 1
fi
done
gate_run_timeout 240 "$TMP/run.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/shot_probe.tscn -- "$TMP/shots"
timeout 240 "$G" --path "$GAME_WIN" res://probe/shot_probe.tscn -- "$TMP/shots" >"$TMP/run.log" 2>&1
st=$?
grep -E "\[shot_probe\]" "$TMP/run.log" | tail -3
if [ "$st" -ne 0 ] || [ ! -s "$TMP/shots/shot_menu.png" ]; then
echo "[golden_refresh] FAIL — shot_probe 退出码=${st}(菜单截图缺失/探针自检红,见上)"
echo "[golden_refresh] FAIL — shot_probe 退出码=$st(菜单截图缺失/探针自检红,见上)"
exit 1
fi
mkdir -p "$REPO/game/tests/golden/$PLAT"
cp "$TMP/shots/shot_menu.png" "$REPO/game/tests/golden/$PLAT/menu_boot.png" || exit 1
echo "[golden_refresh] 完成 — 菜单全屏基线已代入 game/tests/golden/$PLAT/menu_boot.png(提交说明须写明触发原因)"
mkdir -p "$REPO/game/tests/golden"
cp "$TMP/shots/shot_menu.png" "$REPO/game/tests/golden/menu_boot.png" || exit 1
echo "[golden_refresh] 完成 — 菜单全屏基线已代入 game/tests/golden/menu_boot.png(提交说明须写明触发原因)"
exit 0
fi
@@ -58,21 +56,21 @@ for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \
assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do
if [ ! -f "$GAME/$a" ]; then
echo "[golden_refresh] 拒跑 — 素材缺位: ${a}(S3 停机点口径:基线禁锚字形回退态,先补素材+导入边车)"
echo "[golden_refresh] 拒跑 — 素材缺位: $a(S3 停机点口径:基线禁锚字形回退态,先补素材+导入边车)"
exit 1
fi
done
gate_run_timeout 180 "$TMP/run.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/board_shot_probe.tscn -- "$TMP/shots"
timeout 180 "$G" --path "$GAME_WIN" res://probe/board_shot_probe.tscn -- "$TMP/shots" >"$TMP/run.log" 2>&1
st=$?
grep -E "\[board_shot\]" "$TMP/run.log" | tail -6
if [ "$st" -ne 0 ] || [ ! -s "$TMP/shots/board_meta.json" ]; then
echo "[golden_refresh] FAIL — board_shot_probe 退出码=${st}(meta/截图缺失见上)"
echo "[golden_refresh] FAIL — board_shot_probe 退出码=$st(meta/截图缺失见上)"
exit 1
fi
python tools/golden_board_crop.py "$TMP/shots/board_meta.json" "$TMP/shots" "$TMP/crop" || exit 1
mkdir -p "$REPO/game/tests/golden/$PLAT"
mkdir -p "$REPO/game/tests/golden"
for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png; do
cp "$TMP/crop/$b" "$REPO/game/tests/golden/$PLAT/$b" || exit 1
cp "$TMP/crop/$b" "$REPO/game/tests/golden/$b" || exit 1
done
echo "[golden_refresh] 完成 — 三张基线已换代入 game/tests/golden/$PLAT/(提交说明须写明触发原因)"
echo "[golden_refresh] 完成 — 三张基线已换代入 game/tests/golden/(提交说明须写明触发原因)"
-13
View File
@@ -19,14 +19,6 @@ from PIL import Image, ImageChops
TOLERANCE = 16 # 灰度差容差(单通道)
FAIL_RATIO = 0.02 # changed 占比失败线
# 基线平台子目录:<黄金图根目录>/<windows|mac|linux>——渲染基线按平台各存(gate_platform_id 同口径),
# 平台内部照常逐像素严格比对;跨平台不互比(字形/抗锯齿机制差异,2026-11-03 用户裁)。
_PLAT = {"win32": "windows", "darwin": "mac"}.get(sys.platform, "linux")
def plat_missing(base: Path) -> bool:
return not (base / _PLAT).is_dir()
def compare(base: Path, curr: Path, name: str, failures: list) -> None:
pb, pc = base / name, curr / name
@@ -56,11 +48,6 @@ def main() -> int:
print("用法: python tools/golden_compare.py <黄金图目录> <新截图目录> [屏清单...]")
return 1
base, curr = Path(sys.argv[1]), Path(sys.argv[2])
base = base / _PLAT
if not base.is_dir():
print("[golden] FAIL — 本平台(%s)基线目录未建:%s" % (_PLAT, base))
print(" 处置:在该平台跑 tools/golden_baseline_refresh.sh 重制基线并提交(基线换代权限 Q6)")
return 1
shots = sys.argv[3:] or ["shot_menu.png", "shot_camp.png", "shot_level.png", "shot_battle.png"]
failures = []
for name in shots:
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# 红态追绿钩子(本地 CI 触发钩子,2026-10-30 用户裁)——仅当最近一次 CI 判红时,本提交触发
# 后台复验(tools/ci_runner.sh commit,判定在跑批件内);绿态与无状态零触发——
# 小批直提不重复跑全量,同一份字节禁止第二次人手跑。主工作区限定。
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)"
( nohup bash "$REPO/tools/ci_runner.sh" commit >/dev/null 2>&1 & )
exit 0
+5 -20
View File
@@ -1,27 +1,12 @@
#!/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(防静默,下游开工读态即见)。
# 任务 worktree 内的合并不触发(CI 只认 master);CI 红了不回滚合并——修复批合回后本钩子再推再验(追绿同链)。
# 合回验证点(本地 CI 触发钩子,2026-10-30 用户裁)——主工作区 git merge 落地后无条件触发
# 全量域闸后台跑批(tools/ci_runner.sh merge,约 4 分钟,不阻塞合并动作本身)。
# 任务 worktree 内的合并不触发(CI 只认 master;worktree 批要提前验证就手动跑 ci_runner.sh)。
# CI 红了不回滚合并——红项记入 game/tests/.gates/ci-status.json,修复提交经 post-commit 追绿。
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)"
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
exit 0
fi
( nohup bash "$REPO/tools/ci_watch.sh" remote "$TIP" >/dev/null 2>&1 & )
( nohup bash "$REPO/tools/ci_runner.sh" merge >/dev/null 2>&1 & )
exit 0
Executable → Regular
+11 -20
View File
@@ -17,12 +17,10 @@
# 四件带断言探针;默认段断言面零删减零改动(--full 只增不改)。
# 渲染面归 render_gate.sh 正主(窗口化:shot_probe 七屏+fx_shot/board_shot 取证+棋盘 golden 逐像素)。
# 用法: tools/probe_gate.sh [game目录] [--click] [--full] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree)
# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell(Windows 侧);macOS/Linux 走 tools/gate_platform.sh
# 共享跨平台件(Godot 路径两平台都可用 GODOT_BIN 环境变量覆盖)。
# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell;Godot 路径可用环境变量 GODOT_BIN 覆盖。
set -u
G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
REPO="$(cd "$(dirname "$0")/.." && pwd)"
. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行/孤儿清杀)
GAME=""; WITH_CLICK=0; WITH_FULL=0
for a in "$@"; do
case "$a" in
@@ -32,7 +30,7 @@ for a in "$@"; do
esac
done
GAME="${GAME:-$REPO/game}"
GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样)
GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式
TMP="$(mktemp -d /tmp/probegate_XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
_rc=0
@@ -57,7 +55,7 @@ verdict() { # verdict <探针名> <日志路径> <退出码> [签名标记]
echo "[probe_gate] FAIL — $name 超时挂死(exit=$st),孤儿已清"; return 1
fi
if [ "$st" -ne 0 ]; then
echo "[probe_gate] FAIL — $name 退出码=${st}(退出码为唯一权威判据,PASS 串不抵)"; return 1
echo "[probe_gate] FAIL — $name 退出码=$st(退出码为唯一权威判据,PASS 串不抵)"; return 1
fi
if [ -n "$PY" ] && ! "$PY" "$REPO/tools/error_scan.py" "$log" >"$TMP/err.txt" 2>&1; then
echo "[probe_gate] FAIL — $name 脚本错误扫描命中:"
@@ -65,16 +63,17 @@ verdict() { # verdict <探针名> <日志路径> <退出码> [签名标记]
return 1
fi
if ! grep -q "$mark" "$log"; then
echo "[probe_gate] FAIL — $name 未见签名标记「${mark}」(exit=${st}——退出码 0 但零输出亦判红)"; return 1
echo "[probe_gate] FAIL — $name 未见签名标记「$mark」(exit=$st——退出码 0 但零输出亦判红)"; return 1
fi
return 0
}
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(实现单点在 gate_platform.sh,双平台)
gate_orphans "$GAME_WIN"
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算)
printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1"
powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r'
}
kill_orphans() {
gate_kill_orphans "$GAME_WIN"
local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done
}
# 每段跑批统一走本函数:后台启动记 MSYS PID→WINPID,守卫进程到点先 taskkill /T 整树回收
@@ -82,14 +81,6 @@ kill_orphans() {
# 2026-10-25 在册挂死根源),后 kill -9 收 MSYS 侧。正常退出不动进程树;段后 kill_orphans 照跑兜底。
run_suite() { # run_suite <timeout秒> <日志> <场景 res:// 路径>
local t="$1" log="$2" scene="$3"
if ! gate_is_win; then
# macOS/Linux:无 console wrapper(Godot 单进程),走共享带超时执行+孤儿清杀兜底。
local st
gate_run_timeout "$t" "$log" "$G" --headless --path "$GAME_WIN" "$scene"
st=$?
gate_kill_orphans "$GAME_WIN"
return "$st"
fi
local kf="$TMP/killed.$RANDOM"
rm -f "$kf"
"$G" --headless --path "$GAME_WIN" "$scene" >"$log" 2>&1 &
@@ -117,7 +108,7 @@ run_suite() { # run_suite <timeout秒> <日志> <场景 res:// 路径>
# 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付)
if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then
echo "[probe_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)"
gate_run_timeout 300 "$TMP/refresh.log" "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1
timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1
kill_orphans
fi
@@ -192,8 +183,8 @@ fi
echo "======== 残留检查(批末闸) ========"
left="$(orphans | wc -l | tr -d ' ')"
total="$(gate_godot_count_all)"
echo "本闸残留 Godot=${left}(必须0) 全局 Godot 进程=${total}(其他会话的仅报数不算账)"
total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)"
echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)"
if [ "$left" -ne 0 ]; then echo "[probe_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi
if [ "$_rc" -eq 0 ]; then echo "[probe_gate] 全绿"; else echo "[probe_gate] 存在 FAIL,见上"; fi
exit "$_rc"
Executable → Regular
+20 -21
View File
@@ -27,23 +27,22 @@
# 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径);禁改 --headless 跑本闸
# (headless 无渲染面,探针内 fail-fast 护栏会红——那是护栏生效不是闸红)。
# 用法: tools/render_gate.sh [game目录] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree)
# 依赖: tools/gate_platform.sh(双平台封装)+ python(PIL);Windows 侧另用 taskkill/PowerShell,macOS/Linux 走共享件;Godot 路径可用环境变量 GODOT_BIN 覆盖。
# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell + python(PIL);Godot 路径可用环境变量 GODOT_BIN 覆盖。
set -u
G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
REPO="$(cd "$(dirname "$0")/.." && pwd)"
. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行/孤儿清杀)
GAME="${1:-$REPO/game}"
GAME_WIN="$(gate_game_path "$GAME")" # Godot --path 接受形式(Windows 混合斜杠,其他平台原样)
GOLDEN="$REPO/game/tests/golden/$(gate_platform_id)" # 本平台基线目录(按平台各存,比较器同口径)
GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式
TMP="$(mktemp -d /tmp/rendergate_XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
_rc=0
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(实现单点在 gate_platform.sh,双平台)
gate_orphans "$GAME_WIN"
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算)
printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1"
powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r'
}
kill_orphans() {
gate_kill_orphans "$GAME_WIN"
local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done
}
scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不可读(error_scan exit 2)同判红
@@ -58,7 +57,7 @@ scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不
# 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付)
if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then
echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)"
gate_run_timeout 300 "$TMP/refresh.log" "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1
timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1
kill_orphans
fi
@@ -67,13 +66,13 @@ if [ "$pre" -gt 0 ]; then echo "[render_gate] 预清本闸残留 Godot×$pre"; k
echo "======== shot_probe 七屏截图 → shot_checker A-D ========"
SHOT="$TMP/shots"
gate_run_timeout 180 "$TMP/shot.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/shot_probe.tscn -- "$SHOT"
timeout 180 "$G" --path "$GAME_WIN" res://probe/shot_probe.tscn -- "$SHOT" >"$TMP/shot.log" 2>&1
st=$?; kill_orphans
grep -E "\[shot_probe\]" "$TMP/shot.log" | tail -12
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[render_gate] FAIL — shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif [ "$st" -ne 0 ] || ! grep -q "问题=0" "$TMP/shot.log"; then
echo "[render_gate] FAIL — shot_probe 未干净完成(exit=${st},缺失屏/空文件/路径断裂见上)"; _rc=1
echo "[render_gate] FAIL — shot_probe 未干净完成(exit=$st,缺失屏/空文件/路径断裂见上)"; _rc=1
fi
if [ "$_rc" -eq 0 ]; then
python tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /'
@@ -84,13 +83,13 @@ scan_log "shot_probe" "$TMP/shot.log"
echo "======== fx_shot_probe 三时刻取证(Q7) ========"
FX="$TMP/fxshots"
gate_run_timeout 180 "$TMP/fx.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/fx_shot_probe.tscn -- "$FX"
timeout 180 "$G" --path "$GAME_WIN" res://probe/fx_shot_probe.tscn -- "$FX" >"$TMP/fx.log" 2>&1
st=$?; kill_orphans
grep -E "\[fx_shot_probe\]" "$TMP/fx.log" | tail -4
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[render_gate] FAIL — fx_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif [ "$st" -ne 0 ]; then
echo "[render_gate] FAIL — fx_shot_probe 退出码=${st}(取证失败=闸红)"; _rc=1
echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
fi
scan_log "fx_shot_probe" "$TMP/fx.log"
fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7)
@@ -109,13 +108,13 @@ fx_evidence "fx_final.png" 1
echo "======== board_shot_probe L1-L3 取证(Q7) ========"
BD="$TMP/boardshots"
gate_run_timeout 180 "$TMP/bd.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/board_shot_probe.tscn -- "$BD"
timeout 180 "$G" --path "$GAME_WIN" res://probe/board_shot_probe.tscn -- "$BD" >"$TMP/bd.log" 2>&1
st=$?; kill_orphans
grep -E "\[board_shot\]" "$TMP/bd.log" | tail -6
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
echo "[render_gate] FAIL — board_shot_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
elif [ "$st" -ne 0 ]; then
echo "[render_gate] FAIL — board_shot_probe 退出码=${st}(取证失败=闸红)"; _rc=1
echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
fi
scan_log "board_shot_probe" "$TMP/bd.log"
for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do
@@ -123,8 +122,8 @@ for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png
done
echo "======== 棋盘 golden 逐像素(T2——裁剪→对比,容差 provisional) ========"
if [ ! -d "$GOLDEN" ]; then
echo "[render_gate] FAIL — 本平台($(gate_platform_id))基线目录缺失:${GOLDEN}(基线按平台各存、在库=验收断言②)"; _rc=1
if [ ! -d "$REPO/game/tests/golden" ]; then
echo "[render_gate] FAIL — game/tests/golden/ 基线目录缺失(基线在库=验收断言②)"; _rc=1
elif [ ! -s "$BD/board_meta.json" ]; then
echo "[render_gate] SKIP — board_meta.json 缺失(取证段已红),golden 对比不判"
else
@@ -137,7 +136,7 @@ else
assets/art/board/terrain_wall_n_dungeon.png assets/art/board/terrain_floor_dungeon_1.png \
assets/art/board/terrain_wall_n_underdark.png assets/art/board/terrain_floor_underdark_1.png \
assets/art/board/terrain_wall_n_deep_underdark.png assets/art/board/terrain_floor_deep_underdark_1.png; do
[ -f "$GAME/$a" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: ${a}(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; }
[ -f "$GAME/$a" ] || { echo "[render_gate] FAIL — 棋盘素材缺位: $a(Q5:基线锚素材齐盘态,缺盘不得出验收结论)"; _asset_miss=1; }
done
if [ "$_asset_miss" -eq 0 ]; then
CROP="$TMP/crop"
@@ -155,8 +154,8 @@ else
fi
echo "======== 菜单 golden 逐像素(迭代 28 T4——启动态全屏基线)========"
if [ ! -s "$GOLDEN/menu_boot.png" ]; then
echo "[render_gate] FAIL — $GOLDEN/menu_boot.png 基线缺失(菜单 golden 未建——本平台跑 tools/golden_baseline_refresh.sh --menu)"; _rc=1
if [ ! -s "$REPO/game/tests/golden/menu_boot.png" ]; then
echo "[render_gate] FAIL — game/tests/golden/menu_boot.png 基线缺失(菜单 golden 未建——tools/golden_baseline_refresh.sh --menu)"; _rc=1
elif [ ! -s "$SHOT/shot_menu.png" ]; then
echo "[render_gate] SKIP — shot_menu.png 缺失(shot_probe 段已红),菜单 golden 不判"
elif [ ! -f "$GAME/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" ] || [ ! -f "$GAME/assets/art/ui/button_stone.png" ]; then
@@ -173,8 +172,8 @@ fi
echo "======== 残留检查(批末闸) ========"
left="$(orphans | wc -l | tr -d ' ')"
total="$(gate_godot_count_all)"
echo "本闸残留 Godot=${left}(必须0) 全局 Godot 进程=${total}(其他会话的仅报数不算账)"
total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)"
echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)"
if [ "$left" -ne 0 ]; then echo "[render_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi
if [ "$_rc" -eq 0 ]; then echo "[render_gate] 全绿"; else echo "[render_gate] 存在 FAIL,见上"; fi
exit "$_rc"
Executable → Regular
+7 -10
View File
@@ -4,8 +4,8 @@
# 漏跑 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/全量档)只服务开发期按域跑批。
# 本地 CI(tools/ci_runner.sh,2026-10-30 用户裁)读回执写 ci-status.json——强制验证点=合回 master
# 后 post-merge 钩子触发的全量跑批;本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。
# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节),CI 判定照常落账留痕。
#
# 用法:tools/run_all_gates.sh [--code|--docs|--quick]
@@ -44,7 +44,6 @@ GAME="$REPO/game"
STAMP_DIR="$GAME/tests/.gates"
RECEIPT="$STAMP_DIR/receipt.json"
BASELINE="$STAMP_DIR/baseline.json"
. "$REPO/tools/gate_platform.sh" # 跨平台件(平台探测/路径形式/带超时执行)
QUICK=0
DOMAIN="full"
@@ -94,10 +93,8 @@ fi
# ── python 解释器解析:优先带 PIL 的 gamedev venv(渲染闸的 shot_checker/golden_compare 依赖),
# 回落 PATH 上的 python。渲染闸的三个脚本要 PIL,用错解释器会假红。
PY=""
for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "$(command -v python 2>/dev/null)" "python3" "python"; do
for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "$(command -v python 2>/dev/null)" "python"; do
[ -z "$cand" ] && continue
# 存在性守卫先行:Windows venv 路径在非 Windows 平台不存在,误选会让 PY 指向幽灵路径整批崩。
command -v "$cand" >/dev/null 2>&1 || [ -x "$cand" ] || continue
if "$cand" -c "import PIL" >/dev/null 2>&1; then PY="$cand"; break; fi
[ -z "$PY" ] && PY="$cand"
done
@@ -285,7 +282,7 @@ if in_domain code; then
echo "[gates] 关联档选闸·申报:未找到在册需求卡片(diff 与 roadmap 进行中行均无),申报集为空——仅按 diff 推导"
fi
if [ "$SCOPE" = related ]; then
echo "[gates] 关联档选闸:02 选套件跑(--only=${ONLY_TERMS})"
echo "[gates] 关联档选闸:02 选套件跑(--only=$ONLY_TERMS)"
fi
fi
fi
@@ -341,7 +338,7 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分
local _shards="${GATE_SHARDS:-4}"
echo ""
echo "════════════════════════════════════════════"
echo " ${name}(--script 模式,退出码为唯一判据)"
echo " $name(--script 模式,退出码为唯一判据)"
if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then
echo " 关联档选跑:--only=$ONLY_TERMS"
fi
@@ -351,7 +348,7 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分
echo "════════════════════════════════════════════"
local rc=0
if [ "$_shards" -le 1 ]; then
local -a _cmd=(--headless --path "$(gate_game_path "$GAME")" --script res://tests/run_all.gd)
local -a _cmd=(--headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd)
[ ${#_user_args[@]} -gt 0 ] && _cmd+=(-- "${_user_args[@]}")
( cd "$REPO" && "$GODOT" "${_cmd[@]}" ) \
>"$STAMP_DIR/unit.log" 2>&1
@@ -363,7 +360,7 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分
local -a _pids=()
local k
for k in $(seq 1 "$_shards"); do
( cd "$REPO" && "$GODOT" --headless --path "$(gate_game_path "$GAME")" --script res://tests/run_all.gd \
( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd \
-- "${_user_args[@]}" "--shard=$k" "--shard-count=$_shards" ) \
>"$STAMP_DIR/unit_shard_$k.log" 2>&1 &
_pids+=($!)
+1 -3
View File
@@ -18,15 +18,13 @@ r"""run_smoke.py — 冒烟测试运行器(脚本错误外部门禁)
③ 输出无「SCRIPT ERROR」「Parse Error」标记行(标记清单供 tools/error_scan.py 复用,禁另写一份)。
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# 引擎定位:GODOT_BIN 环境变量优先(远端 CI 与非 Windows 平台的注入口),Windows 本机固定路径保底。
DEFAULT_GODOT = os.environ.get("GODOT_BIN") or r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
DEFAULT_SLICE = ROOT / "game"
DEFAULT_SCENE = "res://tests/smoke_boot.tscn"
SUMMARY_RE = re.compile(r"\[smoke\] 摘要:(\d+) 项检查,(\d+) 项失败")
+1 -3
View File
@@ -19,15 +19,13 @@ r"""run_system_test.py — 系统测试运行器(脚本错误外部门禁)
③ 输出无「SCRIPT ERROR」「Parse Error」标记行(标记清单经 tools/error_scan.py 复用,禁另写一份)。
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# 引擎定位:GODOT_BIN 环境变量优先(远端 CI 与非 Windows 平台的注入口),Windows 本机固定路径保底。
DEFAULT_GODOT = os.environ.get("GODOT_BIN") or r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
DEFAULT_SLICE = ROOT / "game"
DEFAULT_SCRIPT = "res://tests/system_test.gd" # 系统测试宿主
SUMMARY_RE = re.compile(r"(?:冒烟完成|\[system_test\] 摘要):(\d+) 项检查,(\d+) 项失败")
Executable → Regular
View File
File mode changed.
Executable → Regular
View File
File mode changed.