#!/usr/bin/env bash # ============================================================ # OpenDance Studio Web 服务端 —— **发布产物级端到端验收** # # 为什么需要它:仓库里的 `npm run verify`(单元测试 + `check-bootstrap`)跑的是 # **源码与刚构建的 dist**;而真正交付给部署方的是 `dist-release/` 里的 tar.gz。 # 两者之间还隔着「解包 → npm install → 迁移 → 起进程」四步,历史上三次生产 # 事故(v0.7.0/v0.7.1 起不来、v0.7.3 场景上传 400、v0.7.4 `No metadata for "Scene"`) # 全都只在**产物真正跑起来**时才暴露。 # # 本脚本把那次验收完整自动化:解包发布包 → 装生产依赖 → 跑迁移 → 起服务 → # 跑发布包自带的 `test/smoke.mjs` → 停服务 → 汇报。跑完你就得到一份 # 「这个 tarball 能不能用」的结论,而不是「源码测试是绿的」。 # # 前置:需要一个可连的 **MySQL / MariaDB**(脚本不会安装数据库,与项目 # 「不部署 MySQL」的边界一致)。本机验证时可用纯用户态方案,例如: # npm i mysql-memory-server # 或任何现成实例 # # 用法: # DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=opendance DB_PASS=xxx DB_NAME=opendance_test \ # bash scripts/accept-release.sh dist-release/opendance-web-v0.7.4.tar.gz # # 可选环境变量: # PORT / ADMIN_PORT 服务端口(默认 34100 / 34101,避免撞上真实服务) # WORK_DIR 解包与存储目录(默认 mktemp -d) # ASSIMP_BIN Assimp 可执行文件;设为空则跳过需要它的用例 # SKIP_MIGRATE=1 跳过迁移(复用已有库) # KEEP_RUNNING=1 跑完不杀进程(便于手工继续查) # # 退出码:0 全通过 / 1 有失败 / 2 用法或前置问题。 # ============================================================ set -uo pipefail PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$PROJECT_DIR" TARBALL="${1:-}" if [ -z "$TARBALL" ]; then echo "用法:DB_HOST=... DB_USER=... DB_NAME=... bash scripts/accept-release.sh <发布包 tar.gz>" >&2 exit 2 fi if [ ! -f "$TARBALL" ]; then echo "✗ 找不到发布包:$TARBALL" >&2 exit 2 fi for name in DB_HOST DB_USER DB_NAME; do if [ -z "${!name:-}" ]; then echo "✗ 缺少环境变量 $name(需要现有 MySQL/MariaDB 连接信息)" >&2 exit 2 fi done PORT="${PORT:-34100}" ADMIN_PORT="${ADMIN_PORT:-34101}" WORK_DIR="${WORK_DIR:-$(mktemp -d)}" SKIP_MIGRATE="${SKIP_MIGRATE:-0}" KEEP_RUNNING="${KEEP_RUNNING:-0}" log() { printf '\033[1;34m[accept]\033[0m %s\n' "$*"; } fail() { printf '\033[1;31m[accept] ✗ %s\033[0m\n' "$*" >&2; FAILED=1; } FAILED=0 log "发布包:$TARBALL" log "工作目录:$WORK_DIR" mkdir -p "$WORK_DIR" # ---- 0. 逐文件核对:发布包 == 打标签的树(T-WEB-018)---- # 只在仓库内、且能解析到对应 tag 时执行;对用户拿到的 tarball 也能跑(--tag 需显式给)。 TRACKED_CHECKER="$PROJECT_DIR/scripts/check-tracked-release.py" if [ -f "$TRACKED_CHECKER" ] && command -v git >/dev/null 2>&1; then TAG="$(basename "$TARBALL" .tar.gz | sed 's/^opendance-web-//')" if git -C "$PROJECT_DIR" rev-parse --verify "${TAG}^{commit}" >/dev/null 2>&1; then log "逐文件核对:发布包 == tag $TAG(源码/文档比 tag,构建产物比工作树构建)" python3 "$TRACKED_CHECKER" "$TARBALL" --tag "$TAG" || fail "发布包与 tag $TAG 不一致" else log "跳过逐文件核对(仓库里没有 tag $TAG;可在仓库内用 --tag 手工核对)" fi else log "跳过逐文件核对(缺 $TRACKED_CHECKER 或 git)" fi # ---- 1. 解包 ---- log "解包…" tar -xzf "$TARBALL" -C "$WORK_DIR" || { echo "✗ 解包失败" >&2; exit 1; } PACKAGE_ROOT="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)" SERVER_DIR="$PACKAGE_ROOT/web-server" [ -d "$SERVER_DIR" ] || { echo "✗ 包里没有 web-server/ 目录" >&2; exit 1; } # ---- 2. 装生产依赖 ---- # 与部署脚本一致:只装 dependencies,不装 devDependencies。 log "安装生产依赖(npm install --omit=dev)…" ( cd "$SERVER_DIR" && npm install --omit=dev --no-audit --no-fund >"$WORK_DIR/npm-install.log" 2>&1 ) \ || { echo "✗ 依赖安装失败,日志:$WORK_DIR/npm-install.log" >&2; tail -20 "$WORK_DIR/npm-install.log"; exit 1; } # ---- 3. 环境变量 ---- cat >"$WORK_DIR/accept.env" <"$WORK_DIR/migrate.log" 2>&1 ) \ || { echo "✗ 迁移失败,日志:$WORK_DIR/migrate.log" >&2; tail -30 "$WORK_DIR/migrate.log"; exit 1; } log "迁移完成" fi # ---- 5. 起服务 ---- log "启动产物(node dist/main.js)…" ( cd "$SERVER_DIR" && nohup node dist/main.js >"$WORK_DIR/server.log" 2>&1 & echo $! >"$WORK_DIR/server.pid" ) SERVER_PID="$(cat "$WORK_DIR/server.pid")" cleanup() { if [ "$KEEP_RUNNING" != "1" ] && [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then kill "$SERVER_PID" 2>/dev/null || true sleep 2 kill -9 "$SERVER_PID" 2>/dev/null || true log "已停止服务(pid $SERVER_PID)" fi } trap cleanup EXIT API="http://127.0.0.1:$PORT" READY=0 for _ in $(seq 1 60); do if curl -sf -m 5 "$API/health" >"$WORK_DIR/health.json" 2>/dev/null; then READY=1; break; fi if ! kill -0 "$SERVER_PID" 2>/dev/null; then break; fi sleep 1 done if [ "$READY" != "1" ]; then fail "服务 60 秒内没有就绪;日志:$WORK_DIR/server.log" tail -30 "$WORK_DIR/server.log" exit 1 fi # ---- 6. 健康检查关键字段(④ 的生产根因就在这几项上)---- log "健康检查:" node -e ' const fs = require("node:fs"); const j = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const pkg = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); let bad = 0; const say = (ok, text) => { console.log(` ${ok ? "✓" : "✗"} ${text}`); if (!ok) bad = 1; }; say(j.version === pkg.version, `/health 的 version=${j.version} 与 package.json 的 ${pkg.version} 一致`); say(j.database.connected === true, `数据库已连接(${j.database.database},pending=${j.database.migrationsPending})`); say(j.database.migrationsPending === 0, "没有未执行的迁移"); say(j.scenes.available === true, `scenes.available=${j.scenes.available}(false 时 error=${JSON.stringify(j.scenes.error)})`); process.exit(bad); ' "$WORK_DIR/health.json" "$SERVER_DIR/package.json" || fail "健康检查关键字段不符合预期" # ---- 7. 产物级实体注册核对 ---- # 用**当前仓库**的检查脚本去核对包内产物:验收标准应当是最新的,而不是 # 「这个包自带的那份检查」(包里那份可能比现在旧,旧规则查不出新增的检查项)。 log "产物级实体注册核对:" CHECKER="$PROJECT_DIR/scripts/check-bootstrap.mjs" if [ -f "$CHECKER" ] && ! cmp -s "$CHECKER" "$SERVER_DIR/scripts/check-bootstrap.mjs"; then cp "$CHECKER" "$SERVER_DIR/scripts/check-bootstrap.mjs" log " 已用仓库当前的 check-bootstrap.mjs 覆盖包内版本(验收标准以最新为准)" fi if ( cd "$SERVER_DIR" && node scripts/check-bootstrap.mjs >"$WORK_DIR/bootstrap.log" 2>&1 ); then grep -E "^✓" "$WORK_DIR/bootstrap.log" | sed 's/^/ /' else fail "实体注册核对失败" sed 's/^/ /' "$WORK_DIR/bootstrap.log" | tail -20 fi # ---- 8. 发布包自带的端到端冒烟 ---- log "端到端冒烟(发布包自带的 test/smoke.mjs)…" if ( cd "$SERVER_DIR" && API_BASE="$API" node test/smoke.mjs >"$WORK_DIR/smoke.log" 2>&1 ); then grep -E "^== 结果" "$WORK_DIR/smoke.log" | sed 's/^/ /' grep -c "^ ✓" "$WORK_DIR/smoke.log" | sed 's/^/ 通过项:/' else fail "冒烟未全通过" grep -E "^ ✗|^== 结果" "$WORK_DIR/smoke.log" | sed 's/^/ /' fi # ---- 9. 契约快照与三端一致 ---- log "包内契约快照:" if [ -f "$PACKAGE_ROOT/contracts/VERSION" ]; then sed 's/^/ /' "$PACKAGE_ROOT/contracts/VERSION" else fail "包内缺少 contracts/VERSION" fi echo "" if [ "$FAILED" -ne 0 ]; then echo "============================================================" echo " 产物级验收失败(日志目录:$WORK_DIR)" echo "============================================================" exit 1 fi echo "============================================================" echo " 产物级验收全部通过(日志目录:$WORK_DIR)" echo "============================================================"