Files
opendance-windows/build/accept-release.sh
T
zijunchiang 20c6682202 test(release): 逐文件核对「发布包 == 打标签的树」(T-WIN-025)
「发布包 == 打标签的树」此前只抽查几个文件,必然会漏。新增
build/check-tracked-release.py(纯标准库,直接读 LZMA zip):对包内**每一个**文件
按路径决定基准 —— OpenDance.exe / OpenDance.pck 比工作树 build/windows/ 的导出结果,
其余文件比 git show <tag>:<路径>;内容不一致 / 包内多余 / 构建产物缺失都会列出并退出 1。
已接入 build/accept-release.sh(第 0 步)与 build/package-release.sh 的提示。

实测(已发布的 v0.2.5 资产重新下载后):
  包内 13 个文件:源码/文档 11(与 tag v0.2.5 比)+ 构建产物 2(与工作树导出比)
  ✓ 逐文件一致:0 处不一致 / 0 处多余 / 0 处缺失
反向验证:把包内 README.txt 追加一行后用 7z 重打成 LZMA zip →
  ✗ 内容不一致 1 处(README.txt),退出码 1。

踩坑:脚本第一版把 BUILD_OUTPUTS 的键一律当目录前缀(rel[len(prefix):]),
而本仓产物是单文件,拼出 build/windows/OpenDance.exe/ 导致误报「工作树缺失」;
现在区分「以 / 结尾的目录前缀」与「精确文件名」两种键。
2026-09-18 03:10:15 +08:00

216 lines
9.4 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
# ============================================================
# OpenDance Studio Windows 客户端 —— **发布包级端到端验收**(T-WIN-018)
#
# 为什么需要它:`build/verify.sh` 跑的是**仓库源码**(`godot --path .`),
# 而交付给用户的是 zip 里的 `OpenDance.pck` + `OpenDance.exe`。
# 差别全在「导出」这一步 —— 导出预设漏资源、pck 没重新生成、版本号没同步、
# 契约快照没更新,都只在包里才暴露。本项目已经因此翻过一次车:
# v0.2.1 的包解开后 exe 属性页显示 0.1.0(配置文件却全一致)。
#
# 本脚本对一个 zip(或已解压目录)跑完:
# 1. sha256 校验(有 SHA256SUMS 时);
# 2. 解压 + 文件清单完整性(exe / pck / README / THIRD_PARTY / contracts);
# 3. exe 的 **PE 版本资源** = 本次发布版本(build/check-pe-version.sh);
# 4. 用 Godot 直接加载**包内 pck** 跑 180 帧,断言无 SCRIPT ERROR;
# 5. 包内运行时探针(build/probe-release.gd):版本号、AboutScreen 显示值、
# 主场景、autoload、关键资源可加载、**pck 内契约表与包内 contracts/VERSION 一致**。
#
# 需要与本项目同版本的 Godot 4.7.x(导出模板要求的版本)。
#
# 用法:
# GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.2.zip
# GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.2
# GODOT_BIN=... bash build/accept-release.sh <zip> 0.2.2 # 显式指定期望版本
#
# 可选环境变量:
# WORK_DIR 解压目录(默认 mktemp -d)
# BOOT_FRAMES 启动检查帧数(默认 180)
# ============================================================
set -uo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
TARGET="${1:-}"
YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)"
EXPECTED="${2:-$YAML_VERSION}"
WORK_DIR="${WORK_DIR:-$(mktemp -d)}"
BOOT_FRAMES="${BOOT_FRAMES:-180}"
FAILED=0
log() { printf '\033[1;34m[accept]\033[0m %s\n' "$*"; }
fail() { printf '\033[1;31m[accept] ✗ %s\033[0m\n' "$*" >&2; FAILED=1; }
if [ -z "$TARGET" ] || [ ! -e "$TARGET" ]; then
echo "用法:GODOT_BIN=/path/to/godot bash build/accept-release.sh <发布包 zip 或已解压目录> [期望版本]" >&2
exit 2
fi
if [ -z "$EXPECTED" ]; then
echo "✗ 无法确定期望版本(project.yaml 里没有 version,也没有显式传入)" >&2
exit 2
fi
log "目标:$TARGET"
log "期望版本:$EXPECTED"
log "工作目录:$WORK_DIR"
mkdir -p "$WORK_DIR"
# ---- 0. 逐文件核对:发布包 == 打标签的树(T-WIN-025)----
# 「发布包里的东西必须等于打标签的那棵树」此前只靠抽查几个文件,必然会漏。
# 本步骤对包内**每一个**文件核对:源码/文档比 tag,构建产物(exe/pck)比工作树导出结果。
CHECKER="$PROJECT_DIR/build/check-tracked-release.py"
if [ -f "$CHECKER" ] && command -v git >/dev/null 2>&1 && [ -f "$TARGET" ]; then
TAG="v$(basename "$TARGET" .zip | sed 's/^opendance-windows-v//')"
if git -C "$PROJECT_DIR" rev-parse --verify "${TAG}^{commit}" >/dev/null 2>&1; then
log "逐文件核对:发布包 == tag $TAG"
python3 "$CHECKER" "$TARGET" --tag "$TAG" || fail "发布包与 tag $TAG 不一致"
else
log "跳过逐文件核对(仓库里没有 tag $TAG;打标签后可手工跑 $CHECKER)"
fi
else
log "跳过逐文件核对(缺 checker / git / 目标是已解压目录)"
fi
# ---- 1. sha256 + 解压 ----
if [ -f "$TARGET" ]; then
# SHA256SUMS 里是**裸文件名**,所以校验时要把 cwd 切到包所在目录;
# 但 sums 文件本身必须用绝对路径 —— 否则 cd 之后相对路径会被二次拼接成
# dist-release/dist-release/SHA256SUMS(这个 bug 在本脚本首跑时就命中了)。
TARGET_DIR_ABS="$(cd "$(dirname "$TARGET")" && pwd)"
SUMS="$TARGET_DIR_ABS/SHA256SUMS"
if [ -f "$SUMS" ]; then
log "sha256 校验($SUMS):"
if ( cd "$TARGET_DIR_ABS" && sha256sum -c "$SUMS" --ignore-missing ); then
:
else
fail "sha256 校验失败"
fi
else
log "未找到 $SUMS,跳过 sha256 校验"
fi
log "解压…"
case "$TARGET" in
*.zip)
if unzip -qq -o "$TARGET" -d "$WORK_DIR" 2>/dev/null; then
log " 用 unzip 解压成功(deflate 包)"
elif command -v 7z >/dev/null 2>&1 && 7z x -y -o"$WORK_DIR" "$TARGET" >/dev/null; then
log " 用 7z 解压成功(LZMA 包 —— 注意 Windows 10 资源管理器不支持它)"
else
fail "解压失败(既不是可 unzip 的 deflate 包,也没有 7z 处理 LZMA)"
exit 1
fi
;;
*)
fail "只支持 .zip 或已解压目录"
exit 2
;;
esac
PACKAGE_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
else
PACKAGE_DIR="$TARGET"
fi
[ -n "$PACKAGE_DIR" ] && [ -d "$PACKAGE_DIR" ] || { fail "找不到解压后的包目录"; exit 1; }
log "包目录:$PACKAGE_DIR"
# ---- 2. 文件清单 ----
log "文件清单:"
for required in OpenDance.exe OpenDance.pck README.txt THIRD_PARTY.md contracts/VERSION; do
if [ -f "$PACKAGE_DIR/$required" ]; then
printf ' ✓ %s\n' "$required"
else
fail "缺少 $required"
fi
done
# exe 与 pck 必须同名同目录,否则 Godot 找不到资源
if [ "$(basename "$PACKAGE_DIR/OpenDance.exe" .exe)" != "$(basename "$PACKAGE_DIR/OpenDance.pck" .pck)" ]; then
fail "exe 与 pck 主文件名不一致"
fi
# 不允许出现历史分卷残留
if find "$PACKAGE_DIR" -maxdepth 1 -name "*.part[0-9]*" | grep -q .; then
fail "包内出现 .partN 分卷残留(发布规则要求单个完整包)"
fi
# ---- 3. exe 的 PE 版本资源 ----
log "产物 exe 的 PE 版本资源:"
if [ -f "$PACKAGE_DIR/OpenDance.exe" ]; then
bash "$PROJECT_DIR/build/check-pe-version.sh" "$PACKAGE_DIR/OpenDance.exe" "$EXPECTED" \
|| fail "exe 的 PE 版本资源不是 v$EXPECTED(Windows 属性页会显示错版本)"
fi
# ---- 4/5. 用 Godot 加载包内 pck ----
GODOT_BIN="${GODOT_BIN:-}"
if [ -z "$GODOT_BIN" ]; then
for candidate in godot godot4 Godot; do
if command -v "$candidate" >/dev/null 2>&1; then GODOT_BIN="$candidate"; break; fi
done
fi
if [ -z "$GODOT_BIN" ]; then
fail "未找到 Godot(本端验收需要与本项目同版本的 Godot 4.7.x;用 GODOT_BIN=<路径> 指定)"
echo "(其余检查已完成,见上)" >&2
exit 1
fi
log "Godot:$GODOT_BIN($("$GODOT_BIN" --version 2>/dev/null | head -1))"
PCK="$PACKAGE_DIR/OpenDance.pck"
if [ -f "$PCK" ]; then
log "包内 pck 启动检查($BOOT_FRAMES 帧,无头)…"
BOOT_LOG="$WORK_DIR/pck-boot.log"
timeout 300 "$GODOT_BIN" --headless --main-pack "$PCK" --quit-after "$BOOT_FRAMES" >"$BOOT_LOG" 2>&1
BOOT_STATUS=$?
if [ "$BOOT_STATUS" -ne 0 ]; then
fail "包内 pck 启动退出码 $BOOT_STATUS"
tail -20 "$BOOT_LOG" | sed 's/^/ /'
elif grep -qE "SCRIPT ERROR|Parse Error|Compile Error" "$BOOT_LOG"; then
fail "包内 pck 启动出现脚本错误"
grep -E "SCRIPT ERROR|Parse Error|Compile Error" "$BOOT_LOG" | head -10 | sed 's/^/ /'
else
echo " ✓ 包内 pck 启动 $BOOT_FRAMES 帧无错误(退出码 0)"
fi
# 产物内不得混入开发/验收工具(T-WIN-023):export_filter="all_resources" 只看
# exclude_filter,加了新的工具目录却忘了排除,就会把探针(乃至**故意不合规**的
# 坏服务端夹具)与向量文件打进 pck —— 实测曾占 pck 的 54%(315,464 B → 145,400 B)。
log "产物内开发残留检查(build/ tests/ _scratch/ *.md)…"
LIST_JSON="$WORK_DIR/pck-entries.json"
if timeout 120 "$GODOT_BIN" --headless --main-pack "$PCK" \
--script "$PROJECT_DIR/build/integration/list-pck.gd" \
-- --json --out "$LIST_JSON" >"$WORK_DIR/pck-list.log" 2>&1; then
node -e '
const fs = require("node:fs");
const r = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
console.log(` ✓ pck 共 ${r.count} 项,无开发残留(build/ tests/ _scratch/ *.md)`);
' "$LIST_JSON" || fail "pck 残留清单解析失败"
else
fail "产物里混入了开发/验收残留(见 $WORK_DIR/pck-list.log)"
grep -E "✗|·" "$WORK_DIR/pck-list.log" | head -10 | sed 's/^/ /'
fi
log "包内运行时探针(版本号 / 界面显示值 / 契约表 / 资源可加载)…"
PROBE="$PROJECT_DIR/build/probe-release.gd"
PROBE_LOG="$WORK_DIR/probe.log"
# 探针自己断言并 quit(0/1),因此以**退出码**为准;日志单独落盘再打印,
# 避免把「grep 的退出码」误当成「探针的退出码」。
if timeout 300 "$GODOT_BIN" --headless --main-pack "$PCK" --script "$PROBE" \
-- --expect-version "$EXPECTED" --snapshot "$PACKAGE_DIR/contracts/VERSION" \
>"$PROBE_LOG" 2>&1; then
PROBE_OK=1
else
PROBE_OK=0
fi
grep -vE "^Godot Engine|^$" "$PROBE_LOG" | sed 's/^/ /'
[ "$PROBE_OK" = 1 ] || fail "包内运行时探针未通过"
fi
echo ""
if [ "$FAILED" -ne 0 ]; then
echo "============================================================"
echo " 发布包级验收失败(工作目录:$WORK_DIR)"
echo "============================================================"
exit 1
fi
echo "============================================================"
echo " 发布包级验收全部通过(工作目录:$WORK_DIR)"
echo "============================================================"