Files
rootandClaude Opus 4.7 d95a734909 部署脚本:本地构建 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_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 <noreply@anthropic.com>
2026-11-07 20:36:06 +08:00

96 lines
5.0 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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:-无}"