Files
web_hgdja_h5/deploy/up_next_h5_prod.sh
T
2026-11-07 20:11:51 +07:00

106 lines
5.7 KiB
Bash
Raw 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_hga 生产服 tar.gz 更新入口;需预先配置 Supervisor 程序 web_hga_h5。
set -Eeuo pipefail
DIR=$(cd "$(dirname "$0")" && pwd)
PROGRAM="${DEPLOY_PROGRAM:-web_hga_h5}"
PACKAGE_NAME="${DEPLOY_PACKAGE_NAME:-web_hga_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"
# Freeze the uploaded pair before validation/extraction; concurrent uploads cannot replace this copy.
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"
# Reject symlinks and special entries before extraction, including hard links to outside paths.
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")
# mktemp -d 建的目录是 700,换上线后非 root 的 Supervisor 进程进不去会 spawn error;改成常规的 755。
chmod 755 "$STAGING"
tar -xzf "$UPLOAD/$PACKAGE_NAME" -C "$STAGING" --no-same-owner --no-same-permissions
[ -z "$(find "$STAGING" -type l -print -quit)" ] || fail '生产包不允许符号链接'
# standalone 打包:start.mjs + server.js + .next + public + config/production.env + node_modules/dotenv
for file in start.mjs server.js .next/BUILD_ID config/production.env package.json; do
[ -s "$STAGING/$file" ] || fail "包内缺少 $file"
done
[ -d "$STAGING/.next/static" ] && [ -d "$STAGING/public" ] || fail '包内缺少静态资源'
[ -d "$STAGING/node_modules/dotenv" ] || fail '包内缺少 node_modules/dotenv'
# 保留服务端 production.local.env(若存在且不是软链接)
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
# 保留运行期 SEO 缓存(若有)
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
# 读取包内 production.env 的 PORT,用于下面的健康检查
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'
# Stop only after validation has succeeded.
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:-无}"