From d95a73490916ee511c9fe12508d9c1ff873687aa Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Nov 2026 20:36:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=EF=BC=9A?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=9E=84=E5=BB=BA=20standalone=20+=20?= =?UTF-8?q?=E8=BF=9C=E7=AB=AF=20up=5Fnext=5Fh5=5Fprod.sh=20=E7=83=AD?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy_prod.sh: npm build + bundle + bundle:linux → tar.gz + SHA-256 → scp root@172.104.166.87:/server/web_hgc/h5 → 远端 up_next_h5_prod.sh - deploy/up_next_h5_prod.sh: SHA-256 校验、tar 路径/类型白名单、保留 production.local.env 与 .next/cache/seo、supervisorctl 启停 web_hgc_h5、curl 本机 PORT 健康探测 Co-Authored-By: Claude Opus 4.7 --- deploy/up_next_h5_prod.sh | 95 +++++++++++++++++++++++++++ deploy_prod.sh | 133 ++++++++++++-------------------------- 2 files changed, 138 insertions(+), 90 deletions(-) create mode 100644 deploy/up_next_h5_prod.sh diff --git a/deploy/up_next_h5_prod.sh b/deploy/up_next_h5_prod.sh new file mode 100644 index 0000000..f675ee6 --- /dev/null +++ b/deploy/up_next_h5_prod.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# web_hgc 生产服 tar.gz 更新入口;需预先配置 Supervisor 程序 web_hgc_h5。 +set -Eeuo pipefail +DIR=$(cd "$(dirname "$0")" && pwd) +PROGRAM="${DEPLOY_PROGRAM:-web_hgc_h5}" +PACKAGE_NAME="${DEPLOY_PACKAGE_NAME:-web_hgc_h5.tar.gz}" +CHECKSUM_NAME="$PACKAGE_NAME.sha256" +[[ "$PROGRAM" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_-]*$ ]] || { echo 'DEPLOY_PROGRAM 格式无效' >&2; exit 1; } +[[ "$PACKAGE_NAME" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.-]*\.tar\.gz$ ]] || { echo 'DEPLOY_PACKAGE_NAME 必须是 .tar.gz 文件名' >&2; exit 1; } +CURRENT="$DIR/next" +LOCK="$DIR/.$PROGRAM.lock" +STAGING=''; UPLOAD=''; PREVIOUS=''; FAILED='' +MOVED_OLD=0; INSTALLED=0; STOPPED=0; WAS_RUNNING=0 +fail() { printf '错误:%s\n' "$*" >&2; exit 1; } +for tool in node sha256sum tar supervisorctl mktemp; 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' +mkdir "$LOCK" 2>/dev/null || fail "已有更新进程或残留锁:$LOCK" +finish() { + local result=$? + trap - EXIT INT TERM + if [ "$result" != 0 ] && { [ "$STOPPED" = 1 ] || [ "$INSTALLED" = 1 ] || [ "$MOVED_OLD" = 1 ]; }; then + printf '更新失败,恢复上一版本。\n' >&2 + supervisorctl stop "$PROGRAM" >/dev/null 2>&1 || true + if [ "$INSTALLED" = 1 ]; then + FAILED="$DIR/next.failed.$(date +%Y%m%d%H%M%S).$$" + mv "$CURRENT" "$FAILED" || { echo "无法保存失败版本:$CURRENT" >&2; result=1; } + fi + if [ "$MOVED_OLD" = 1 ]; then + mv "$PREVIOUS" "$CURRENT" || { echo "需要手动恢复:$PREVIOUS → $CURRENT" >&2; result=1; } + fi + if [ "$WAS_RUNNING" = 1 ]; then supervisorctl start "$PROGRAM" || result=1; fi + fi + if [ -n "$STAGING" ] && [ -d "$STAGING" ]; then rm -rf "$STAGING"; fi + if [ -n "$UPLOAD" ] && [ -d "$UPLOAD" ]; then rm -rf "$UPLOAD"; fi + rmdir "$LOCK" 2>/dev/null || true + exit "$result" +} +trap finish EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +[ ! -L "$CURRENT" ] && { [ ! -e "$CURRENT" ] || [ -d "$CURRENT" ]; } || fail 'next 必须是普通目录' +STATUS=$(supervisorctl status "$PROGRAM" 2>/dev/null || true) +[[ "$STATUS" == "$PROGRAM "* ]] && [[ "$STATUS" != *'ERROR'* ]] || fail "请先配置 Supervisor 程序 $PROGRAM,command 指向 node $CURRENT/start.mjs" +if [[ "$STATUS" == *'RUNNING'* ]]; then WAS_RUNNING=1; fi +[ -s "$DIR/$PACKAGE_NAME" ] && [ -s "$DIR/$CHECKSUM_NAME" ] || fail "缺少 $PACKAGE_NAME 或 SHA-256" +UPLOAD=$(mktemp -d "$DIR/.prod-upload.XXXXXX") +cp "$DIR/$PACKAGE_NAME" "$DIR/$CHECKSUM_NAME" "$UPLOAD/" +(cd "$UPLOAD" && sha256sum -c "$CHECKSUM_NAME") || fail 'SHA-256 校验失败' +LC_ALL=C tar -tzf "$UPLOAD/$PACKAGE_NAME" > "$UPLOAD/members" || fail 'tar.gz 压缩包损坏' +while IFS= read -r entry; do + entry="${entry#./}" + case "$entry" in + /*|../*|*/../*|*/..|*\\*) fail "非法 tar.gz 路径:$entry" ;; + esac +done < "$UPLOAD/members" +LC_ALL=C tar -tvzf "$UPLOAD/$PACKAGE_NAME" > "$UPLOAD/types" || fail '无法检查 tar.gz 文件类型' +while IFS= read -r entry; do + case "$entry" in + -?*|d?*) ;; + *) fail '生产包只允许普通文件和目录,不允许链接或特殊文件' ;; + esac +done < "$UPLOAD/types" +STAGING=$(mktemp -d "$DIR/.prod-release.XXXXXX") +tar -xzf "$UPLOAD/$PACKAGE_NAME" -C "$STAGING" --no-same-owner --no-same-permissions +[ -z "$(find "$STAGING" -type l -print -quit)" ] || fail '生产包不允许符号链接' +for file in start.mjs server.js .next/BUILD_ID config/production.env package.json package-lock.json scripts/next-env.mjs scripts/check-health.mjs; do + [ -s "$STAGING/$file" ] || fail "包内缺少 $file" +done +[ -d "$STAGING/.next/static" ] && [ -d "$STAGING/public" ] || fail '包内缺少静态资源' +[ -d "$STAGING/node_modules/dotenv" ] || fail '包内缺少 node_modules/dotenv' +if [ -f "$CURRENT/config/production.local.env" ] && [ ! -L "$CURRENT/config/production.local.env" ]; then + cp "$CURRENT/config/production.local.env" "$STAGING/config/production.local.env" + chmod 600 "$STAGING/config/production.local.env" +fi +if [ -d "$CURRENT/.next/cache/seo" ] && [ ! -L "$CURRENT/.next/cache/seo" ]; then + mkdir -p "$STAGING/.next/cache" + cp -R "$CURRENT/.next/cache/seo" "$STAGING/.next/cache/seo" +fi +APP_PORT=$(awk -F= '/^[[:space:]]*PORT[[:space:]]*=/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2);gsub(/^["'\'']|["'\'']$/,"",$2);print $2;exit}' "$STAGING/config/production.env") +[ -n "$APP_PORT" ] || fail '无法从 config/production.env 读取 PORT' +if [ "$WAS_RUNNING" = 1 ]; then supervisorctl stop "$PROGRAM"; STOPPED=1; fi +if [ -d "$CURRENT" ]; then + PREVIOUS="$DIR/next.prev.$(date +%Y%m%d%H%M%S).$$" + mv "$CURRENT" "$PREVIOUS"; MOVED_OLD=1 +fi +mv "$STAGING" "$CURRENT"; STAGING=''; INSTALLED=1 +supervisorctl start "$PROGRAM" +sleep 3 +for i in 1 2 3 4 5 6 7 8 9 10; do + if curl -fsS -o /dev/null --max-time 3 "http://127.0.0.1:$APP_PORT/"; then break; fi + sleep 1 + if [ "$i" = 10 ]; then fail "健康检查失败:http://127.0.0.1:$APP_PORT/"; fi +done +supervisorctl status "$PROGRAM" +printf '生产服更新完成:%s;上一版本:%s\n' "$CURRENT" "${PREVIOUS:-无}" diff --git a/deploy_prod.sh b/deploy_prod.sh index 8fd2791..4b83f8c 100755 --- a/deploy_prod.sh +++ b/deploy_prod.sh @@ -1,91 +1,44 @@ #!/bin/bash -# 禁果短剧正式服:构建 → 测试 → Linux standalone + MD5 → 可选触发 rsupdate.sh。 -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_jgdj/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_jgdj_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 -check_local production -if [ "${DEPLOY_DRY_RUN:-0}" = 1 ]; then - printf 'DRY RUN:正式环境;Linux %s/%s;不构建、不连接服务器。\n' "$TARGET_ARCH" "$TARGET_LIBC"; public_profile production - printf ' npm run check → npm run bundle → npm 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 '构建正式环境并运行回归测试' -npm run check -step '整理正式环境 standalone 和目标 Linux 原生依赖' -npm run bundle -npm run bundle:linux -- "$TARGET_ARCH" "$TARGET_LIBC" -for file in start.mjs server.js .next/BUILD_ID config/production.env runtime-target.json; 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_hgc/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_hgc/h5}" +PACKAGE_NAME="${DEPLOY_PACKAGE_NAME:-web_hgc_h5.tar.gz}" +PROGRAM="${DEPLOY_PROGRAM:-web_hgc_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