fix(deploy): 修复 NODE_ENV 污染导致管理端构建失败(tsc: not found)

用户实测:部署时构建管理端报 `sh: 1: tsc: not found`,且 npm 只装了 23 个包。

根因:脚本开头用 `set -a; . deploy/.env; set +a` 载入 .env,其中的
NODE_ENV=production 被导出到脚本自身环境;npm 见到它便会跳过
devDependencies,于是 typescript / vite / nest 等构建工具全部缺失。
本地实测完全复现:
  NODE_ENV=production npm ci          → added 23 package,无 tsc
  env -u NODE_ENV npm ci --include=dev → added 53 package,tsc/vite 齐备

修复:
- 新增 npm_install_with_dev():统一用 `env -u NODE_ENV npm ... --include=dev`
- 管理端构建改用它;**API 构建回退路径存在同一隐患,一并修复**
- 管理端改为在 $APP_DIR/web-server/admin 内构建(不再污染源码树、
  也不要求源码目录可写),构建完删除该处 node_modules 释放磁盘
- 构建失败时给出可操作提示(改用发布包)

另修复一处静默的信息泄露:
- 复制源码树时未排除 .env,本地开发用的 web-server/.env(含真实数据库
  口令)会被复制进 /opt/opendance/web-server/.env;deploy/.env 才是权威配置,
  多一个 dotenv 配置源只会造成排障困惑。已把 .env / .env.local 加入排除,
  并验证 .env.example 不受影响(209 → 208 个文件)

实测:
  旧行为 tsc 缺失、构建报 tsc: not found(与用户报错一致)
  新行为 tsc/vite 齐备,vite build 成功产出 dist/index.html + 3 个 assets
  复制排除:.env 不复制、.env.example 保留、node_modules 无泄漏
This commit is contained in:
2026-09-15 00:44:54 +08:00
parent c092b833fb
commit f6fe9af0a2
+33 -13
View File
@@ -52,6 +52,19 @@ as_service_user() {
fi fi
} }
# 安装**含 devDependencies** 的依赖。
# 必须显式处理:脚本开头用 `set -a; . deploy/.env` 载入了 .env,其中的
# NODE_ENV=production 会被导出到本脚本环境;npm 见到它会自动跳过
# devDependencies,于是 tsc / vite / nest 等构建工具全部缺失
# (用户实测报错:`sh: 1: tsc: not found`,且只装了 23 个包)。
npm_install_with_dev() {
if [ -f package-lock.json ]; then
as_service_user env -u NODE_ENV npm ci --include=dev --no-audit --no-fund --loglevel=error
else
as_service_user env -u NODE_ENV npm install --include=dev --no-audit --no-fund --loglevel=error
fi
}
# HTTP GET,输出响应体。curl → wget → node(node 必有,且 22+ 自带 fetch) # HTTP GET,输出响应体。curl → wget → node(node 必有,且 22+ 自带 fetch)
http_get() { http_get() {
if command -v curl >/dev/null 2>&1; then if command -v curl >/dev/null 2>&1; then
@@ -138,8 +151,11 @@ esac
# 说明:在 git 检出目录直接执行时,web-server/ 里可能已有 200 MB+ 的 # 说明:在 git 检出目录直接执行时,web-server/ 里可能已有 200 MB+ 的
# node_modules(数万个小文件)。原实现用 cp -r 全量复制,会长时间无输出, # node_modules(数万个小文件)。原实现用 cp -r 全量复制,会长时间无输出,
# 看起来像卡死。这里统一排除,运行期依赖稍后由 npm ci 重新安装。 # 看起来像卡死。这里统一排除,运行期依赖稍后由 npm ci 重新安装。
# .env / .env.local 一并排除:deploy/.env 才是权威配置,源码树里的本地 .env
# 若被复制进来,dotenv 会把它当成第二配置源,造成排障困惑。
COPY_EXCLUDES=(--exclude=node_modules --exclude=.git --exclude=storage COPY_EXCLUDES=(--exclude=node_modules --exclude=.git --exclude=storage
--exclude=dist-release --exclude=coverage --exclude=.gradle) --exclude=dist-release --exclude=coverage --exclude=.gradle
--exclude=.env --exclude=.env.local)
copy_tree() { copy_tree() {
local src="$1" dst="$2" local src="$1" dst="$2"
@@ -152,7 +168,7 @@ copy_tree() {
# 嵌套的 "./admin/node_modules"(实测会漏掉 admin 下的 122MB 依赖)。 # 嵌套的 "./admin/node_modules"(实测会漏掉 admin 下的 122MB 依赖)。
# 因此每个名字都要同时给出顶层与任意层级的模式。 # 因此每个名字都要同时给出顶层与任意层级的模式。
local tar_excludes=() local tar_excludes=()
for pattern in node_modules .git storage dist-release coverage .gradle; do for pattern in node_modules .git storage dist-release coverage .gradle .env .env.local; do
tar_excludes+=(--exclude="./$pattern" --exclude="*/$pattern") tar_excludes+=(--exclude="./$pattern" --exclude="*/$pattern")
done done
( cd "$src" && tar "${tar_excludes[@]}" -cf - . ) | ( cd "$dst" && tar -xf - ) ( cd "$src" && tar "${tar_excludes[@]}" -cf - . ) | ( cd "$dst" && tar -xf - )
@@ -169,22 +185,26 @@ for item in web-server contracts; do
done done
# ---- 管理端产物:两种目录布局都要支持 ---- # ---- 管理端产物:两种目录布局都要支持 ----
# 发布包: <root>/admin/ (已构建) # 发布包: <root>/admin/ (已构建,直接用)
# 源码检出:<root>/web-server/admin/dist/ (需已构建,缺失则现场构建) # 源码检出:<root>/web-server/admin/dist/ (已构建,直接用)
# 否则在 $APP_DIR/web-server/admin 现场构建
# (在安装目录内构建,不污染源码树,也不要求源码目录可写)
ADMIN_SRC="" ADMIN_SRC=""
if [ -d "$SRC_DIR/admin" ]; then if [ -d "$SRC_DIR/admin" ]; then
ADMIN_SRC="$SRC_DIR/admin" ADMIN_SRC="$SRC_DIR/admin"
elif [ -d "$SRC_DIR/web-server/admin/dist" ]; then elif [ -d "$SRC_DIR/web-server/admin/dist" ]; then
ADMIN_SRC="$SRC_DIR/web-server/admin/dist" ADMIN_SRC="$SRC_DIR/web-server/admin/dist"
elif [ -d "$SRC_DIR/web-server/admin" ]; then elif [ -d "$APP_DIR/web-server/admin" ]; then
warn "未找到管理端构建产物,正在构建 web-server/admin(需要 devDependencies)" warn "未找到管理端构建产物,正在 ${APP_DIR}/web-server/admin 构建(需要 devDependencies)"
if [ -f "$SRC_DIR/web-server/admin/package-lock.json" ]; then if ( cd "$APP_DIR/web-server/admin" && npm_install_with_dev \
( cd "$SRC_DIR/web-server/admin" && npm ci --no-audit --no-fund --loglevel=error ) && as_service_user env -u NODE_ENV npm run build ); then
ADMIN_SRC="$APP_DIR/web-server/admin/dist"
# 构建完即可丢弃管理端的 node_modules,避免长期占用磁盘
rm -rf "$APP_DIR/web-server/admin/node_modules"
else else
( cd "$SRC_DIR/web-server/admin" && npm install --no-audit --no-fund --loglevel=error ) die "管理端构建失败。可改用发布包(内含已构建的 admin/):
./scripts/package-release.sh 然后在目标机解压部署"
fi fi
( cd "$SRC_DIR/web-server/admin" && npm run build )
ADMIN_SRC="$SRC_DIR/web-server/admin/dist"
fi fi
[ -n "$ADMIN_SRC" ] || die "找不到管理端:既没有 admin/ 目录,也没有 web-server/admin/" [ -n "$ADMIN_SRC" ] || die "找不到管理端:既没有 admin/ 目录,也没有 web-server/admin/"
@@ -215,8 +235,8 @@ fi
if [ ! -f dist/main.js ]; then if [ ! -f dist/main.js ]; then
warn "未找到构建产物 dist/main.js,改为在服务器上构建(需要 devDependencies)" warn "未找到构建产物 dist/main.js,改为在服务器上构建(需要 devDependencies)"
as_service_user npm install --no-audit --no-fund npm_install_with_dev
as_service_user npm run build as_service_user env -u NODE_ENV npm run build
fi fi
[ -f dist/main.js ] || die "构建产物缺失:$APP_DIR/web-server/dist/main.js" [ -f dist/main.js ] || die "构建产物缺失:$APP_DIR/web-server/dist/main.js"
[ -f "$APP_DIR/admin/index.html" ] || die "管理端构建产物缺失:$APP_DIR/admin/index.html" [ -f "$APP_DIR/admin/index.html" ] || die "管理端构建产物缺失:$APP_DIR/admin/index.html"