diff --git a/README.md b/README.md index 6747efa..f9fdfcd 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,19 @@ GODOT_BIN=/path/to/godot bash build/export-windows.sh release # 打包发布产物(单个完整 zip + SHA256SUMS) bash build/package-release.sh 0.2.2 + +# 发布包级验收(对**发布包**跑,而不是对源码;文档 §7.5) +GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.2.zip ``` +`accept-release.sh` 校验 sha256 → 解压 → 文件清单(并检查没有 `.partN` 残留)→ +exe 的 PE 版本资源 → **用 Godot 直接加载包内 pck 跑 180 帧** → 包内运行时探针 +(`build/probe-release.gd`:包内版本号、`AboutScreen.app_version()`、主场景、autoload、 +15 个关键资源、以及**包内 `Credits.CONTRACT_VERSIONS` 与 `contracts/VERSION` 快照是否一致**)。 +它读的全是包内资源,因此结论就是「用户装到机器上的那一份能不能用」。 +已对**已发布的 v0.2.2 资产**(重新下载)跑通,并用「篡改包内 contracts/VERSION」与 +「期望版本写错」两个负例反向验证过。 + 当前版本:**v0.2.2**(`project.godot` 的 `application/config/version`, 与 `project.yaml`、`export_presets.cfg`、导出产物名、Git tag、「关于」清单展示、 以及 exe 的 PE 版本资源全部一致)。 diff --git a/README.txt b/README.txt index 79c0d42..737e7f8 100644 --- a/README.txt +++ b/README.txt @@ -158,7 +158,14 @@ OpenDance.exe 的用户看第一节即可。 7. 自检清单 - 1) 一键验证:GODOT_BIN=/path/to/godot bash build/verify.sh + 0) 发布包级验收(对**发布包**而不是源码;文档 §7.5) + GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.2.zip + 预期末行「发布包级验收全部通过」。它会校验 sha256、解压、核对文件清单、 + 读 exe 的 PE 版本资源、用 Godot 直接加载包内 pck 跑 180 帧、并跑包内运行时 + 探针(包内版本号 / 「关于」清单显示值 / 主场景 / autoload / 关键资源 / + 包内契约表与 contracts/VERSION 快照是否一致)。 + + 1) 源码一键验证:GODOT_BIN=/path/to/godot bash build/verify.sh 预期末行「验证全部通过」。 2) 无头测试:$GODOT --headless --path . --script res://tests/run_tests.gd 预期「== 结果:751 通过 / 0 失败 ==」与「全部通过」(缺 VRM 素材或 diff --git a/build/accept-release.sh b/build/accept-release.sh new file mode 100755 index 0000000..fbef98a --- /dev/null +++ b/build/accept-release.sh @@ -0,0 +1,181 @@ +#!/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 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" + +# ---- 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 + + 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 "============================================================" diff --git a/build/package-release.sh b/build/package-release.sh index 87b87de..fab6503 100755 --- a/build/package-release.sh +++ b/build/package-release.sh @@ -178,3 +178,6 @@ fi echo "" echo "分发:上传 $NAME.zip 与 SHA256SUMS 两个资产;用户下载后" echo " sha256sum -c SHA256SUMS && unzip $NAME.zip 即可直接运行 OpenDance.exe。" +echo "" +echo "下一步(文档 §7.5:每次发布都要有一条读产物本身的验收):" +echo " GODOT_BIN=/path/to/godot bash build/accept-release.sh $ZIP" diff --git a/build/probe-release.gd b/build/probe-release.gd new file mode 100644 index 0000000..c597176 --- /dev/null +++ b/build/probe-release.gd @@ -0,0 +1,151 @@ +extends SceneTree + +## 发布产物验收探针(T-WIN-018,由 build/accept-release.sh 调用) +## +## 用法: +## godot --headless --main-pack <包内 OpenDance.pck> \ +## --script /绝对路径/build/probe-release.gd \ +## -- --expect-version 0.2.2 --snapshot /绝对路径/contracts/VERSION +## +## 为什么需要它:`build/verify.sh` 验证的是**仓库源码**(`godot --path .`), +## 而交付给用户的是 zip 里的 `OpenDance.pck`。差别全在「导出」这一步: +## 导出预设漏了资源、pck 没重新生成、版本号没同步、契约快照没更新…… +## 都只在包里才暴露。本探针**只读包内资源**(`--main-pack` + `res://`), +## 所以它给出的版本号、契约表、autoload、主场景就是用户装到机器上的那一份。 +## +## 断言(任一不满足即 quit(1)): +## 1. ProjectSettings 的 application/config/version == 期望版本 +## (界面上「关于」清单与 .pck 都源自它 —— 文档 §11.1); +## 2. AboutScreen.app_version() == 期望版本(界面真正会显示的值); +## 3. 主场景非空且可加载,四个 autoload 都在场景树里; +## 4. 关键资源(UI / 模型 / 场景 / 网络 / 评分)在包内都可加载; +## 5. 包内 Credits.CONTRACT_VERSIONS 与**包内** contracts/VERSION 快照逐项一致 +## —— 抓「pck 用旧契约构建、快照却拷了新的」这类漂移。 + +const KEY_RESOURCES := [ + "res://src/ui/credits.gd", + "res://src/ui/about_screen.gd", + "res://src/ui/main.gd", + "res://src/ui/main_menu.gd", + "res://src/ui/scene_panel.gd", + "res://src/ui/source_panel.gd", + "res://src/model/humanoid_map.gd", + "res://src/model/vrm_loader.gd", + "res://src/scene/scene_runtime.gd", + "res://src/net/api_client.gd", + "res://src/net/catalog_types.gd", + "res://src/net/pose_server.gd", + "res://src/net/keypoint_frame.gd", + "res://src/score/dtw.gd", + "res://src/score/scoring.gd", +] + +const EXPECTED_AUTOLOADS := ["AppConfig", "CatalogStore", "PoseReceiver", "ScoreService"] + +var _failed := 0 + + +func _check(ok: bool, text: String) -> void: + print(" %s %s" % ["✓" if ok else "✗", text]) + if not ok: + _failed += 1 + + +## 解析 contracts/VERSION(`key: value`,`#` 注释) +func _read_snapshot(path: String) -> Dictionary: + var out := {} + if path.is_empty() or not FileAccess.file_exists(path): + return out + var file := FileAccess.open(path, FileAccess.READ) + if file == null: + return out + while not file.eof_reached(): + var line := file.get_line().strip_edges() + if line.is_empty() or line.begins_with("#"): + continue + var idx := line.find(":") + if idx > 0: + out[line.substr(0, idx).strip_edges()] = line.substr(idx + 1).strip_edges() + file.close() + return out + + +func _initialize() -> void: + var expect_version := "" + var snapshot_path := "" + var args := OS.get_cmdline_user_args() + var i := 0 + while i < args.size(): + if args[i] == "--expect-version" and i + 1 < args.size(): + expect_version = args[i + 1] + i += 2 + elif args[i] == "--snapshot" and i + 1 < args.size(): + snapshot_path = args[i + 1] + i += 2 + else: + i += 1 + + print("[accept] 包内运行时探针(--main-pack OpenDance.pck)") + var project_version := str(ProjectSettings.get_setting("application/config/version", "")) + print(" engine=%s project=%s main_scene=%s" % [ + str(Engine.get_version_info().get("string", "?")), + project_version, + str(ProjectSettings.get_setting("application/run/main_scene", "")), + ]) + + var about = load("res://src/ui/about_screen.gd") + var about_version := str(about.app_version()) if about != null else "" + var credits = load("res://src/ui/credits.gd") + var contract_versions: Dictionary = credits.CONTRACT_VERSIONS if credits != null else {} + + if not expect_version.is_empty(): + _check(project_version == expect_version, + "包内 application/config/version = %s(期望 %s)" % [project_version, expect_version]) + _check(about_version == expect_version, + "AboutScreen.app_version() = %s(「关于」清单会显示这个值)" % about_version) + else: + print(" · 未指定 --expect-version,跳过版本比对(实际 %s)" % project_version) + + var main_scene := str(ProjectSettings.get_setting("application/run/main_scene", "")) + _check(not main_scene.is_empty() and load(main_scene) != null, "主场景可加载:%s" % main_scene) + + var autoloads := [] + for child in root.get_children(): + autoloads.append(str(child.name)) + var missing := [] + for name in EXPECTED_AUTOLOADS: + if not autoloads.has(name): + missing.append(name) + _check(missing.is_empty(), "autoload 齐备 %s(实际 %s)" % [str(EXPECTED_AUTOLOADS), str(autoloads)]) + + var unloadable := [] + for path in KEY_RESOURCES: + if load(path) == null: + unloadable.append(path) + _check(unloadable.is_empty(), "关键资源全部可加载(%d 项)%s" % [ + KEY_RESOURCES.size(), "" if unloadable.is_empty() else " 缺失:" + str(unloadable)]) + + var snapshot := _read_snapshot(snapshot_path) + if snapshot.is_empty(): + print(" · 未提供 contracts/VERSION 快照(%s),跳过契约比对" % snapshot_path) + else: + print(" [contracts] 包内 Credits = ", JSON.stringify(contract_versions)) + print(" [contracts] 包内快照文件 = ", JSON.stringify(snapshot)) + var mismatch := [] + for key in snapshot.keys(): + if not contract_versions.has(key): + mismatch.append("%s 缺失" % key) + elif str(contract_versions[key]) != str(snapshot[key]): + mismatch.append("%s: pck=%s 快照=%s" % [key, contract_versions[key], snapshot[key]]) + for key in contract_versions.keys(): + if not snapshot.has(key): + mismatch.append("%s 只存在于 pck" % key) + _check(mismatch.is_empty(), "pck 内契约表与包内 contracts/VERSION 快照一致%s" % [ + "" if mismatch.is_empty() else ":" + str(mismatch)]) + + if _failed > 0: + print("[accept] ✗ 探针发现 %d 项不符" % _failed) + quit(1) + return + print("[accept] ✓ 包内运行时探针全部通过") + quit(0) diff --git a/build/probe-release.gd.uid b/build/probe-release.gd.uid new file mode 100644 index 0000000..c7b8a57 --- /dev/null +++ b/build/probe-release.gd.uid @@ -0,0 +1 @@ +uid://bb1l1hl8fpavq diff --git a/project.yaml b/project.yaml index 5de8acc..107e3f2 100644 --- a/project.yaml +++ b/project.yaml @@ -94,6 +94,10 @@ tasks: file: tasks/017-pe-version.md scope: 发布包 exe 的 PE 版本资源(Windows 属性页显示的文件/产品版本)必须等于发布版本 status: done + - id: T-WIN-018 + file: tasks/018-release-acceptance.md + scope: 发布包级端到端验收(读包内 pck:版本号 / 界面显示值 / 契约表 / 资源可加载) + status: done deliverable: artifact: opendance-windows-v0.2.2.zip diff --git a/tasks/018-release-acceptance.md b/tasks/018-release-acceptance.md new file mode 100644 index 0000000..f1db964 --- /dev/null +++ b/tasks/018-release-acceptance.md @@ -0,0 +1,83 @@ +# T-WIN-018:发布包级端到端验收(读包内 pck,不是读源码) + +- **状态**: done +- **依赖**: T-WIN-014(整包发布)/ T-WIN-017(PE 版本元数据) +- **文档**: v1.6 §5.7 / §7.5 / §11 +- **契约**: 不涉及(验收流程) + +## 为什么需要 + +`build/verify.sh` 跑的是**仓库源码**(`godot --path .`),而交付给用户的是 zip 里的 +`OpenDance.pck` + `OpenDance.exe`。差别全在「导出」这一步: + +| 只在导出后才可能出现的问题 | 源码侧能发现吗 | +|--|--| +| 导出预设漏了资源 / 资源没导入 | ❌ | +| pck 没重新生成(用了上一次的) | ❌ | +| 版本号没同步进预设 → PE 资源与 pck 里都是旧值 | ❌(v0.2.1 就这么翻的车) | +| 契约快照文件拷了新的、pck 却是旧契约构建的 | ❌ | + +本项目已在两端因此翻车(本仓 v0.2.1 的 PE 版本元数据、Web v0.7.4 的实体注册), +所以三端统一要求:**每次发布都要有一条读产物本身的验收**(文档 §7.5)。 + +## 实现 + +| 文件 | 作用 | +|--|--| +| `build/probe-release.gd`(新增) | **包内运行时探针**:用 `godot --headless --main-pack --script <探针>` 只读包内资源,断言并 `quit(0/1)` | +| `build/accept-release.sh`(新增) | 对一个 zip(或已解压目录)跑完整验收 | + +### 探针断言(5 类) + +1. `ProjectSettings` 的 `application/config/version` == 期望版本 + —— 这是「关于」清单与 pck 的**唯一来源**(§11.1); +2. `AboutScreen.app_version()` == 期望版本 —— 界面真正会显示的值; +3. 主场景非空且可加载;四个 autoload(`AppConfig` / `CatalogStore` / + `PoseReceiver` / `ScoreService`)都在场景树里; +4. 15 个关键资源(UI / 模型 / 场景 / 网络 / 评分)在包内都可加载; +5. 包内 `Credits.CONTRACT_VERSIONS` 与**包内** `contracts/VERSION` 快照逐项一致 + —— 抓「pck 用旧契约构建、快照却拷了新的」。 + +### 验收脚本步骤 + +1. sha256 校验(有 `SHA256SUMS` 时); +2. 解压(deflate 用 `unzip`,LZMA 用 `7z`)+ 文件清单完整性 + (`OpenDance.exe` / `OpenDance.pck` / `README.txt` / `THIRD_PARTY.md` / `contracts/VERSION`); + 顺带检查**不允许有 `.partN` 分卷残留**; +3. exe 的 **PE 版本资源** = 发布版本(复用 `build/check-pe-version.sh`); +4. 用 Godot 直接加载**包内 pck** 跑 180 帧,断言无 `SCRIPT ERROR`; +5. 执行包内运行时探针。 + +## 实测(对**已发布**的 v0.2.2 资产,重新下载后跑) + +``` +[accept] sha256 校验:opendance-windows-v0.2.2.zip: OK + ✓ OpenDance.exe / OpenDance.pck / README.txt / THIRD_PARTY.md / contracts/VERSION + ✓ PE 版本资源一致:FileVersion 0.2.2.0 / 资源字符串 0.2.2 + ✓ 包内 pck 启动 180 帧无错误(退出码 0) + ✓ 包内 application/config/version = 0.2.2(期望 0.2.2) + ✓ AboutScreen.app_version() = 0.2.2(「关于」清单会显示这个值) + ✓ 主场景可加载:res://scenes/main.tscn + ✓ autoload 齐备 ["AppConfig", "CatalogStore", "PoseReceiver", "ScoreService"] + ✓ 关键资源全部可加载(15 项) + ✓ pck 内契约表与包内 contracts/VERSION 快照一致 + 发布包级验收全部通过 +``` + +## 反向验证 + +| 负例 | 结果 | +|--|--| +| 把包内 `contracts/VERSION` 的 `rest-api` 改成 `0.4.0`(快照与 pck 不一致) | ✗ `pck 内契约表与包内 contracts/VERSION 快照一致:["rest-api: pck=0.5.0 快照=0.4.0"]`,退出码 1 | +| 期望版本写错(如 `0.2.1` 对 v0.2.2 的包) | ✗ 三处版本断言全失败,退出码 1 | + +> 首次运行还暴露了脚本自身的一个缺陷:`SHA256SUMS` 用相对路径 + `cd` 到包目录后 +> 被二次拼接成 `dist-release/dist-release/SHA256SUMS`,导致 sha256 校验假失败 +> (而其余检查全绿,很容易被忽略)。已改为绝对路径,并把这条记在脚本注释里。 + +## 用法 + +```bash +GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.2.zip +# 或对已解压目录:bash build/accept-release.sh dist-release/opendance-windows-v0.2.2 +```