From caca7672cbaee5f62f1dcdb7ac8a46f9cdcd6735 Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Tue, 15 Sep 2026 00:54:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(release):=20=E4=BF=AE=E5=A4=8D=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=8C=85=E7=BC=BA=E5=A4=B1=20storage=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8C=E5=B9=B6=E5=8A=A0=E5=85=A5=E4=BA=A7=E7=89=A9?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E6=80=A7=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户实测:用发布包部署后启动报 MODULE_NOT_FOUND at dist/models/models.module.js:13 require('../storage/storage.module') 根因:package-release.sh 的 rsync 用了 --exclude 'storage'。该模式匹配 **任意层级**的同名目录,把源码目录 src/storage/ 与编译产物 dist/storage/ 一并排除——发布包里完全没有 storage 模块,服务必然起不来。 这与第一阶段 .gitignore 里 'storage/' 误伤 src/storage/ 是同一类错误, 本项目第二次踩到。 修复: - 排除模式全部锚定到 web-server 根:'/storage/'、'/data/'、'/tmp/'、 '/.env'、'/admin/',不再匹配嵌套同名目录 - 新增产物完整性校验(打包后自动执行,失败即删除压缩包): src 下每个 .ts 必须有对应 dist/*.js;关键文件清单齐全; 不得混入 node_modules / 真实 .env / 运行时存储 / keystore - ctl.sh 补上缺失的 warn 函数(line 120: warn: command not found) 实测: - 对照实验:排除模式改回 'storage' 后校验准确报出缺少 dist/storage/storage.module.js 并删除压缩包,退出码 1 - 新包端到端:解压 → npm ci --omit=dev(213 包)→ 启动 → /health 200、 MariaDB 已连接、迁移 0、catalog/songs/models/motions 全部 200 --- deploy/ctl.sh | 5 ++- scripts/package-release.sh | 77 +++++++++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/deploy/ctl.sh b/deploy/ctl.sh index bb2f72b..9d9fa03 100755 --- a/deploy/ctl.sh +++ b/deploy/ctl.sh @@ -20,8 +20,9 @@ RUN_DIR="${RUN_DIR:-/run}" PID_FILE="${PID_FILE:-$RUN_DIR/opendance-api.pid}" MAIN_JS="$APP_DIR/web-server/dist/main.js" -die() { printf '\033[1;31m[ctl]\033[0m %s\n' "$*" >&2; exit 1; } -log() { printf '\033[1;34m[ctl]\033[0m %s\n' "$*"; } +die() { printf '\033[1;31m[ctl]\033[0m %s\n' "$*" >&2; exit 1; } +log() { printf '\033[1;34m[ctl]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[ctl]\033[0m %s\n' "$*"; } [ "$(id -u)" -eq 0 ] || die "需要 root 权限" diff --git a/scripts/package-release.sh b/scripts/package-release.sh index 5067f5a..303cb8c 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -47,13 +47,19 @@ rm -rf "$STAGE" mkdir -p "$STAGE" # 后端源码 + 构建产物(排除依赖、测试、存储、密钥) +# 排除模式**必须锚定到 web-server 根目录**(前导 /)。 +# 否则 rsync 的 'storage' 会匹配任意层级的同名目录,把源码目录 +# src/storage/ 与编译产物 dist/storage/ 一并删掉——发布包会缺少整个 +# storage 模块,服务启动即 MODULE_NOT_FOUND。这是本项目第二次踩到 +# 「storage 误伤源码目录」这个坑(第一次是 .gitignore)。 rsync -a --delete \ --exclude 'node_modules' \ - --exclude 'storage' \ - --exclude 'data' \ - --exclude '.env' \ - --exclude '.env.local' \ - --exclude 'admin' \ + --exclude '/storage/' \ + --exclude '/data/' \ + --exclude '/tmp/' \ + --exclude '/.env' \ + --exclude '/.env.local' \ + --exclude '/admin/' \ web-server/ "$STAGE/web-server/" # 管理端构建产物(文档要求 admin/ 与 web-server/ 平级) @@ -77,6 +83,67 @@ cp -r tasks "$STAGE/tasks" log "生成压缩包..." ( cd "$OUT_DIR" && tar -czf "$NAME.tar.gz" "$NAME" ) +# ---------- 6. 产物完整性校验 ---------- +# 打包脚本本身的错误(如排除模式写错)会静默产出残缺的发布包, +# 直到目标机启动时才暴露。这里做一次硬校验:src 下每个 .ts 都必须在 +# 包内有对应的 dist/*.js,且关键文件齐全。 +log "校验产物完整性…" +MANIFEST_TMP=$(tar -tzf "$OUT_DIR/$NAME.tar.gz") + +fail=0 + +# 6.1 src → dist 一一对应(能抓到整目录被误排除这类问题) +# 注意 nest build 的 sourceRoot 是 src,产物会**去掉 src/ 前缀**: +# web-server/src/storage/storage.service.ts → web-server/dist/storage/storage.service.js +while IFS= read -r tsfile; do + rel="${tsfile#src/}" + jsfile="opendance-web-v${VERSION}/web-server/dist/${rel%.ts}.js" + if ! grep -qx "$jsfile" <<< "$MANIFEST_TMP"; then + echo " ✗ 缺少编译产物:$jsfile(源:$tsfile)" >&2 + fail=1 + fi +done < <(cd web-server && find src -name '*.ts' ! -name '*.spec.ts') +[ "$fail" -eq 0 ] && echo " ✓ src 下所有 .ts 均有对应 dist/*.js" + +# 6.2 关键文件 +for required in \ + "opendance-web-v${VERSION}/web-server/dist/main.js" \ + "opendance-web-v${VERSION}/web-server/src/storage/storage.service.ts" \ + "opendance-web-v${VERSION}/admin/index.html" \ + "opendance-web-v${VERSION}/deploy/install.sh" \ + "opendance-web-v${VERSION}/deploy/ctl.sh" \ + "opendance-web-v${VERSION}/deploy/nginx/opendance.conf" \ + "opendance-web-v${VERSION}/deploy/systemd/opendance-api.service" \ + "opendance-web-v${VERSION}/contracts/VERSION" \ + "opendance-web-v${VERSION}/THIRD_PARTY.md" \ + "opendance-web-v${VERSION}/README.txt" ; do + if ! grep -qx "$required" <<< "$MANIFEST_TMP"; then + echo " ✗ 缺少关键文件:$required" >&2 + fail=1 + fi +done + +# 6.3 不得包含的东西(模式必须精确,否则会误伤 src/storage、.env.example) +PREFIX="opendance-web-v${VERSION}" +check_forbidden() { + local label="$1" pattern="$2" + if grep -qE "$pattern" <<< "$MANIFEST_TMP"; then + echo " ✗ 产物中混入了不应包含的内容($label)" >&2 + grep -E "$pattern" <<< "$MANIFEST_TMP" | head -3 | sed 's/^/ /' >&2 + fail=1 + fi +} +check_forbidden "node_modules" "node_modules" +check_forbidden "真实 .env" "^${PREFIX}/(deploy|web-server)/\.env$" +check_forbidden "运行时存储目录" "^${PREFIX}/web-server/storage/" +check_forbidden "keystore" "\.jks$|keystore\.properties$" +[ "$fail" -eq 0 ] && echo " ✓ 无 node_modules / 真实 .env / 运行时存储 / keystore" + +if [ "$fail" -ne 0 ]; then + rm -f "$OUT_DIR/$NAME.tar.gz" + die "产物完整性校验失败,已删除有问题的压缩包" +fi + log "完成:$OUT_DIR/$NAME.tar.gz" echo echo "产物内容:"