两个都是**只有真正启动服务才会暴露**的缺陷,v0.7.0 / v0.7.1 的产物受影响。 1. 【致命】CatalogModule 漏 import ScenesModule CatalogService 因为 catalog/export 要下发 scenes[] 而注入 ScenesService,但 CatalogModule 的 imports 只列了 SongsModule / MotionsModule / ModelsModule。 Nest 的 DI 按模块解析,于是 node dist/main.js 在 bootstrap 阶段直接抛 UnknownDependenciesException: Nest can't resolve dependencies of the CatalogService (... ? ...) —— 服务完全起不来(装得上、起不来)。 为什么三层检查全绿:nest build/tsc 看不出运行时 DI;258 项单元测试直接 new 服务或测纯函数,从不编译 AppModule;打包脚本只校验文件清单与压缩包完整性。 修复:CatalogModule.imports 补上 ScenesModule。 2. /health 的 version 在原生部署下永远是 0.1.0 原实现 process.env.npm_package_version ?? '0.1.0',而该变量只在 npm 脚本下 存在;正式部署是原生服务(ExecStart=node dist/main.js),npm 不参与 -> 恒为 undefined -> 回退到硬编码 0.1.0,与管理端左下角展示的 vX.Y.Z 冲突, 违反文档 11.2 第 4 条「可核对:Web 端至少与 GET /health 的 version 一致」。 修复:新增 src/config/app-version.ts 作为运行时唯一解析点 (APP_VERSION -> 运行入口旁/工作目录的 package.json(带包名校验) -> npm_package_version -> 显式 0.0.0),/health 改用 resolveAppVersion(), main.ts 启动首行也打印版本号。新增 12 项测试,其中一项显式模拟无 npm 环境变量的原生部署。 防线(都挂进了 verify 与打包脚本) - scripts/check-bootstrap.mjs:以正式部署方式(node dist/main.js、无 npm 环境 变量、不需要数据库)拉起真实产物,断言①无依赖解析类错误②启动日志的应用版本 等于 package.json ③已推进到数据库连接阶段。已做反向验证:删掉 ScenesModule 后该检查立刻失败。 - scripts/check-version.mjs(并入 contracts:check):校验 web-server/package.json = admin/package.json = project.yaml,正则禁止 health.controller.ts 再出现硬编码版本回退,管理端已构建时断言产物 JS 含 vX.Y.Z。 - npm run verify 现在包含 bootstrap:check;package-release.sh 在构建后、打包前 各跑一次(构建管理端后再跑 version:check,因为此时才需要校验产物内版本)。 文档:README.md / README.txt 增加 v0.7.2 修订说明; tasks/011-version-display.md 与 tasks/015-scene-management.md 补「缺陷修复记录」 (含为什么当时没被发现的三层检查对照表)。 验证:npm run verify 通过(契约六项一致、35 条路由、258 项测试、nest build、 启动冒烟);产物 opendance-web-v0.7.2.tar.gz sha256 f04ecc85d17517ec00754238616eb1237cdb2fe17440af6dce731827c0922b26。
207 lines
9.2 KiB
Bash
Executable File
207 lines
9.2 KiB
Bash
Executable File
#!/bin/bash
|
||
# ============================================================
|
||
# 打包发布产物:opendance-web-v0.7.0.tar.gz
|
||
#
|
||
# 产物结构(Linux 业务服务部署,不使用 Docker、不部署反向代理):
|
||
# web-server/ NestJS 源码 + 构建产物
|
||
# admin/ React 管理端构建产物
|
||
# deploy/ install.sh / ctl.sh / uninstall.sh / .env.example
|
||
# env-lib.sh (.env 语法预检) / assimp-lib.sh (Assimp 定位安装)
|
||
# init-env.sh (.env 引导)
|
||
# systemd|openrc|initd / proxy(反代说明文档,仅文档不部署)
|
||
# contracts/ 当前版本契约快照
|
||
# THIRD_PARTY.md 开源项目声明
|
||
# README.md 使用说明(Markdown, 仓库首页展示的就是它)
|
||
# README.txt 使用说明(纯文本同内容版本)
|
||
#
|
||
# 用法:./scripts/package-release.sh [版本号]
|
||
# ============================================================
|
||
set -euo pipefail
|
||
|
||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||
cd "$REPO_ROOT"
|
||
|
||
# 版本号唯一来源:web-server/package.json 的 version(与 GET /health 的 version 同源,
|
||
# 也是管理端左下角展示的那个值)。不传参数时自动读取,并与 project.yaml 交叉校验。
|
||
PKG_VERSION="$(node -p "require('./web-server/package.json').version" 2>/dev/null || true)"
|
||
YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)"
|
||
VERSION="${1:-$PKG_VERSION}"
|
||
VERSION="${VERSION:-0.7.1}"
|
||
if [ -n "$PKG_VERSION" ] && [ "$VERSION" != "$PKG_VERSION" ]; then
|
||
echo "[error] 版本号不一致:参数=$VERSION,web-server/package.json=$PKG_VERSION" >&2
|
||
exit 1
|
||
fi
|
||
if [ -n "$YAML_VERSION" ] && [ "$VERSION" != "$YAML_VERSION" ]; then
|
||
echo "[error] 版本号不一致:参数=$VERSION,project.yaml=$YAML_VERSION" >&2
|
||
exit 1
|
||
fi
|
||
NAME="opendance-web-v${VERSION}"
|
||
OUT_DIR="$REPO_ROOT/dist-release"
|
||
STAGE="$OUT_DIR/$NAME"
|
||
|
||
log() { printf '\033[1;34m[package]\033[0m %s\n' "$*"; }
|
||
die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
# ---------- 1. 契约一致性 ----------
|
||
log "校验契约版本一致性..."
|
||
( cd web-server && node scripts/check-contracts.mjs )
|
||
|
||
# ---------- 2. 后端构建 ----------
|
||
log "构建后端..."
|
||
( cd web-server && npm run build )
|
||
# 构建后**启动冒烟**:产物能不能真正 boot 起来(模块 DI 图可解析 + 版本号正确)。
|
||
# v0.7.0/v0.7.1 就是漏了这一步才把「装得上、起不来」的产物发了出去。
|
||
( cd web-server && npm run bootstrap:check )
|
||
|
||
# ---------- 3. 管理端构建 ----------
|
||
if [ -d web-server/admin/node_modules ]; then
|
||
log "构建管理端..."
|
||
( cd web-server/admin && npm run build )
|
||
# 构建后再次校验版本一致性:此时管理端产物已存在,会额外断言产物 JS 内含 vX.Y.Z
|
||
( cd web-server && npm run version:check )
|
||
else
|
||
log "警告:web-server/admin/node_modules 不存在,跳过管理端构建(产物将缺少 admin/)"
|
||
fi
|
||
|
||
# ---------- 4. 组织产物目录 ----------
|
||
log "整理产物目录 $STAGE ..."
|
||
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 '/tmp/' \
|
||
--exclude '/.env' \
|
||
--exclude '/.env.local' \
|
||
--exclude '/admin/' \
|
||
web-server/ "$STAGE/web-server/"
|
||
|
||
# 管理端构建产物(文档要求 admin/ 与 web-server/ 平级)
|
||
if [ -d web-server/admin/dist ]; then
|
||
mkdir -p "$STAGE/admin"
|
||
cp -r web-server/admin/dist/. "$STAGE/admin/"
|
||
else
|
||
log "警告:未找到 web-server/admin/dist,产物不含 admin/"
|
||
fi
|
||
|
||
cp -r deploy "$STAGE/deploy"
|
||
# 部署配置里绝不能带真实 .env(含数据库口令)
|
||
rm -f "$STAGE/deploy/.env"
|
||
rm -rf "$STAGE/deploy"/systemd/*.bak "$STAGE/deploy"/openrc/*.bak "$STAGE/deploy"/initd/*.bak
|
||
cp -r contracts "$STAGE/contracts"
|
||
cp THIRD_PARTY.md README.txt README.md project.yaml DSH.md "$STAGE/"
|
||
cp -r tasks "$STAGE/tasks"
|
||
|
||
# ---------- 5. 打包 ----------
|
||
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/check-access.sh" \
|
||
"opendance-web-v${VERSION}/web-server/dist/preprocess/vmd-parser.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/preprocess/mmd-pose.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/preprocess/mmd-skeleton.js" \
|
||
"opendance-web-v${VERSION}/web-server/scripts/inspect-vmd.mjs" \
|
||
"opendance-web-v${VERSION}/web-server/scripts/import-default-model.mjs" \
|
||
"opendance-web-v${VERSION}/web-server/scripts/reanalyze-models.mjs" \
|
||
"opendance-web-v${VERSION}/web-server/dist/common/zip-reader.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/models/model-convert.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/models/glb-textures.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/migrations/1789450000000-AddModelConversionFields.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/migrations/1789460000000-AddModelStats.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/models/model-inspector.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/models/model-thumbnail.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/models/vrm-writer.js" \
|
||
"opendance-web-v${VERSION}/web-server/dist/models/gltf-packer.js" \
|
||
"opendance-web-v${VERSION}/deploy/init-env.sh" \
|
||
"opendance-web-v${VERSION}/deploy/env-lib.sh" \
|
||
"opendance-web-v${VERSION}/deploy/assimp-lib.sh" \
|
||
"opendance-web-v${VERSION}/deploy/proxy/README.md" \
|
||
"opendance-web-v${VERSION}/deploy/proxy/nginx.conf.example" \
|
||
"opendance-web-v${VERSION}/deploy/proxy/web.config.example" \
|
||
"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" \
|
||
"opendance-web-v${VERSION}/README.md" ; 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
|
||
|
||
# ---- SHA256SUMS(裸文件名,下载目录内 sha256sum -c 直接可用) ----
|
||
( cd "$OUT_DIR" && sha256sum "$NAME.tar.gz" > SHA256SUMS )
|
||
log "完成:$OUT_DIR/$NAME.tar.gz"
|
||
log "已生成 $OUT_DIR/SHA256SUMS"
|
||
if ( cd "$OUT_DIR" && sha256sum -c SHA256SUMS --status ); then
|
||
log "sha256 自校验:OK"
|
||
else
|
||
die "sha256 自校验失败"
|
||
fi
|
||
echo
|
||
echo "产物内容:"
|
||
# 先把清单读入变量,避免 tar 因管道提前关闭(SIGPIPE)而误报 "write error"
|
||
MANIFEST=$(tar -tzf "$OUT_DIR/$NAME.tar.gz")
|
||
echo "$MANIFEST" | sed -n '1,20p'
|
||
echo " ...(共 $(echo "$MANIFEST" | wc -l) 项,$(du -h "$OUT_DIR/$NAME.tar.gz" | cut -f1))"
|
||
echo
|
||
echo "部署方式:解压后进入 deploy/,复制 .env.example 为 .env 并填写数据库信息,执行 ./install.sh"
|