部署脚本:本地构建 standalone + 远端 up_next_h5_prod.sh 热替换

- deploy_prod.sh: npm build + bundle + bundle:linux → tar.gz + SHA-256 → scp root@172.104.166.87:/server/web_hgb/h5 → 远端 up_next_h5_prod.sh
- deploy/up_next_h5_prod.sh: SHA-256 校验、tar 路径/类型白名单、保留 production.local.env 与 .next/cache/seo、supervisorctl 启停 web_hgb_h5、curl 本机 PORT 健康探测

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 20:36:00 +08:00
co-authored by Claude Opus 4.7
parent 38e7edc756
commit bc1a369c5f
2 changed files with 138 additions and 94 deletions
+43 -94
View File
@@ -1,95 +1,44 @@
#!/bin/bash
# 熟果短剧正式服(wdb_sgdj):构建 → Linux standalone + MD5 → 可选触发 rsupdate.sh。
# 对齐 jha-1048-hddj-web;H5 监听 21038,包目录 /home/server/web_sgdj/h5。
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
cd "$SCRIPT_DIR"
source "$SCRIPT_DIR/scripts/deploy-common.sh"
JUMP_HOST="${DEPLOY_JUMP_HOST:-server@34.92.204.175}"
# 压缩包上传完成后,在同一目录执行服务器现有的 rsupdate.sh。
JUMP_DIR="${DEPLOY_JUMP_DIR:-/home/server/web_sgdj/h5}"
# valid_path 限制目录为安全字符;命令文本与手动执行一致,兼容 SSH 命令白名单。
REMOTE_UPDATE_COMMAND="cd ${JUMP_DIR%/} && bash rsupdate.sh"
SCP_PORT="${DEPLOY_SCP_PORT:-22889}"
SSH_PORT="${DEPLOY_SSH_PORT:-22889}"
TARGET_ARCH="${DEPLOY_TARGET_ARCH:-x64}"
TARGET_LIBC="${DEPLOY_TARGET_LIBC:-glibc}"
PACKAGE_NAME="${DEPLOY_PACKAGE_NAME:-web_sgdj_h5.tar.gz}"
MD5_NAME="${PACKAGE_NAME%.tar.gz}.md5"
RELEASE="$SCRIPT_DIR/dist/production-linux-$TARGET_ARCH-$TARGET_LIBC"
PACKAGE_ONLY=0
for argument in "$@"; do
case "$argument" in
--dry-run) DEPLOY_DRY_RUN=1 ;;
--package-only) PACKAGE_ONLY=1 ;;
-h|--help)
printf '用法:%s [--dry-run] [--package-only]\n' "$0"
printf '仅打包:--package-only。仅预览:--dry-run(不构建、不联网)。\n'
printf '上传目标:%s:%s/;上传成功后在该目录执行 bash rsupdate.sh。\n' "$JUMP_HOST" "${JUMP_DIR%/}"
printf '可覆盖 DEPLOY_JUMP_HOST、DEPLOY_JUMP_DIR、DEPLOY_SCP_PORT(%s)、DEPLOY_SSH_PORT(%s)、DEPLOY_PACKAGE_NAME(%s)、DEPLOY_TARGET_ARCH(x64/arm64)、DEPLOY_TARGET_LIBC(glibc/musl)。\n' "$SCP_PORT" "$SSH_PORT" "$PACKAGE_NAME"
exit 0 ;;
*) fail "不支持的参数 $argument" ;;
esac
done
case "$TARGET_ARCH" in x64|arm64) ;; *) fail 'DEPLOY_TARGET_ARCH 仅支持 x64 或 arm64' ;; esac
case "$TARGET_LIBC" in glibc|musl) ;; *) fail 'DEPLOY_TARGET_LIBC 仅支持 glibc 或 musl' ;; esac
[[ "$PACKAGE_NAME" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.-]*\.tar\.gz$ ]] || fail 'DEPLOY_PACKAGE_NAME 必须是 .tar.gz 文件名'
if [ "$PACKAGE_ONLY" = 0 ] && [ "${DEPLOY_DRY_RUN:-0}" != 1 ]; then
valid_host "$JUMP_HOST" DEPLOY_JUMP_HOST; valid_path "$JUMP_DIR" DEPLOY_JUMP_DIR
valid_port "$SCP_PORT" DEPLOY_SCP_PORT; valid_port "$SSH_PORT" DEPLOY_SSH_PORT
need ssh; need scp; need tee
fi
need node; need pnpm
node -e 'const [a,b]=process.versions.node.split(".").map(Number);if(a<20||a===20&&b<9)process.exit(1)' || fail '需要 Node.js >=20.9,推荐 22'
[ -d node_modules/next ] && [ -d node_modules/dotenv ] || fail '请先执行 pnpm install'
[ -s config/production.env ] && [ -s pnpm-lock.yaml ] || fail '缺少 config/production.env 或 pnpm-lock.yaml'
if [ "${DEPLOY_DRY_RUN:-0}" = 1 ]; then
printf 'DRY RUN:正式环境;Linux %s/%s;不构建、不连接服务器。\n' "$TARGET_ARCH" "$TARGET_LIBC"; public_profile production
printf ' pnpm run build → pnpm run bundle → pnpm run bundle:linux -- %s %s\n' "$TARGET_ARCH" "$TARGET_LIBC"
printf ' 保留 standalone 裁剪依赖,仅安装 lockfile 中目标 Linux 的 Sharp/libvips。\n'
printf ' 生成 %s + %s(纯 MD5 hash)。\n' "$PACKAGE_NAME" "$MD5_NAME"
if [ "$PACKAGE_ONLY" = 1 ]; then printf ' 仅生成本地包。\n'
else
printf ' scp -P %s %s %s %s:%s/\n' "$SCP_PORT" "$PACKAGE_NAME" "$MD5_NAME" "$JUMP_HOST" "${JUMP_DIR%/}"
printf ' ssh -p %s %s "%s"\n' "$SSH_PORT" "$JUMP_HOST" "$REMOTE_UPDATE_COMMAND"
fi
exit 0
fi
need tar
command -v md5sum >/dev/null || command -v md5 >/dev/null || fail '缺少 md5sum 或 macOS md5'
lock_build
step '构建正式环境 standalone'
pnpm run build
step '整理正式环境 standalone 和目标 Linux 原生依赖'
pnpm run bundle
pnpm run bundle:linux -- "$TARGET_ARCH" "$TARGET_LIBC"
for file in start.mjs server.js .next/BUILD_ID config/production.env runtime-target.json ecosystem.config.cjs; do
[ -s "$RELEASE/$file" ] || fail "正式包缺少 $file"
done
for directory in node_modules .next/static public; do [ -d "$RELEASE/$directory" ] || fail "正式包缺少 $directory"; done
step "生成 $PACKAGE_NAME"
TEMP_PACKAGE=$(mktemp "$SCRIPT_DIR/dist/.production-archive.XXXXXX")
if ! COPYFILE_DISABLE=1 COPY_EXTENDED_ATTRIBUTES_DISABLE=1 tar --no-xattrs \
--exclude='._*' --exclude='.DS_Store' --exclude='.env*' --exclude='*.local.env' --exclude='*.log' \
-czf "$TEMP_PACKAGE" -C "$RELEASE" .; then
rm -f "$TEMP_PACKAGE"; fail '压缩失败'
fi
tar -tzf "$TEMP_PACKAGE" >/dev/null
mv -f "$TEMP_PACKAGE" "$PACKAGE_NAME"
chmod 600 "$PACKAGE_NAME"
if command -v md5sum >/dev/null; then md5sum "$PACKAGE_NAME" | awk '{print $1}' > "$MD5_NAME"
else md5 -q "$PACKAGE_NAME" > "$MD5_NAME"; fi
printf '正式包:%s/%s;校验:%s/%s\n' "$SCRIPT_DIR" "$PACKAGE_NAME" "$SCRIPT_DIR" "$MD5_NAME"
if [ "$PACKAGE_ONLY" = 1 ]; then exit 0; fi
step "上传到 $JUMP_HOST:$JUMP_DIR/"
scp -P "$SCP_PORT" "$PACKAGE_NAME" "$MD5_NAME" "$JUMP_HOST:${JUMP_DIR%/}/"
step '执行正式服 rsupdate.sh'
printf '远端命令:%s\n' "$REMOTE_UPDATE_COMMAND"
RUN_LOG=$(mktemp "$SCRIPT_DIR/dist/production-rsupdate.XXXXXX")
if ! ssh -p "$SSH_PORT" "$JUMP_HOST" "$REMOTE_UPDATE_COMMAND" 2>&1 | tee "$RUN_LOG"; then fail "rsupdate.sh 执行失败,日志:$RUN_LOG"; fi
if ! node - "$RUN_LOG" <<'NODE'
const output=require('node:fs').readFileSync(process.argv[2],'utf8').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g,'');
if(/\b(?:BACKOFF|FATAL|ERROR)\b|\bspawn\s+error\b/i.test(output))process.exit(1);
NODE
then fail "远端输出包含失败状态,日志:$RUN_LOG"; fi
printf '正式服 rsupdate.sh 执行完成;请核对远端服务状态。日志:%s\n' "$RUN_LOG"
# 生产部署:本地构建 Next.js standalone → tar.gz → scp → 远端 up_next_h5_prod.sh
# 目标:/server/web_hgb/h5(参考 /server/web_hgcg 的发布流程)
set -Eeuo pipefail
DIR=$(cd "$(dirname "$0")" && pwd)
SSH_TARGET="${DEPLOY_SSH_TARGET:-root@172.104.166.87}"
REMOTE_DIR="${DEPLOY_REMOTE_DIR:-/server/web_hgb/h5}"
PACKAGE_NAME="${DEPLOY_PACKAGE_NAME:-web_hgb_h5.tar.gz}"
PROGRAM="${DEPLOY_PROGRAM:-web_hgb_h5}"
REMOTE_SCRIPT="up_next_h5_prod.sh"
fail() { printf '错误:%s\n' "$*" >&2; exit 1; }
for tool in node npm tar shasum ssh scp; do command -v "$tool" >/dev/null || fail "缺少命令 $tool"; done
node -e 'const [a,b]=process.versions.node.split(".").map(Number);if(a<20||a===20&&b<9)process.exit(1)' || fail '需要 Node.js >=20.9'
[ -f "$DIR/deploy/$REMOTE_SCRIPT" ] || fail "缺少 deploy/$REMOTE_SCRIPT"
STAGE="$DIR/dist/.prod-stage"
rm -rf "$STAGE" "$DIR/dist/$PACKAGE_NAME" "$DIR/dist/$PACKAGE_NAME.sha256"
mkdir -p "$STAGE"
echo "[1/5] 构建 production..."
(cd "$DIR" && npm run build)
echo "[2/5] 打包 standalone(含 Linux x64/glibc 原生包)..."
(cd "$DIR" && npm run bundle)
(cd "$DIR" && npm run bundle:linux)
LINUX_DIST="$DIR/dist/production-linux-x64-glibc"
[ -d "$LINUX_DIST" ] || fail "构建产物 $LINUX_DIST 不存在"
echo "[3/5] 生成 tar.gz 和 SHA-256..."
(cd "$LINUX_DIST" && tar --no-xattrs --exclude='.DS_Store' --exclude='node_modules/@img/sharp-darwin-arm64' --exclude='node_modules/@img/sharp-libvips-darwin-arm64' -czf "$STAGE/$PACKAGE_NAME" --owner=0 --group=0 .)
(cd "$STAGE" && shasum -a 256 "$PACKAGE_NAME" > "$PACKAGE_NAME.sha256")
ls -lh "$STAGE/$PACKAGE_NAME" "$STAGE/$PACKAGE_NAME.sha256"
echo "[4/5] 上传到 $SSH_TARGET:$REMOTE_DIR ..."
ssh "$SSH_TARGET" "mkdir -p '$REMOTE_DIR'"
scp "$DIR/deploy/$REMOTE_SCRIPT" "$SSH_TARGET:$REMOTE_DIR/$REMOTE_SCRIPT"
scp "$STAGE/$PACKAGE_NAME" "$STAGE/$PACKAGE_NAME.sha256" "$SSH_TARGET:$REMOTE_DIR/"
echo "[5/5] 远端执行解包和热替换..."
ssh "$SSH_TARGET" "chmod +x $REMOTE_DIR/$REMOTE_SCRIPT && DEPLOY_PROGRAM=$PROGRAM DEPLOY_PACKAGE_NAME=$PACKAGE_NAME bash $REMOTE_DIR/$REMOTE_SCRIPT"
echo DEPLOY_OK