#!/bin/bash # ============================================================ # 打包发布产物:opendance-web-v0.4.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" VERSION="${1:-0.4.0}" 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 ) # ---------- 3. 管理端构建 ---------- if [ -d web-server/admin/node_modules ]; then log "构建管理端..." ( cd web-server/admin && npm run build ) 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 log "完成:$OUT_DIR/$NAME.tar.gz" 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"