Files
shanxia2.0/生产环境部署/deploy-release.sh
T
2026-08-08 22:19:09 +08:00

236 lines
7.6 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# ============================================
# shanxia2.0 生产一键发版(前端 → 后端)
#
# 用法(在仓库任意目录执行均可):
# ./生产环境部署/deploy-release.sh
# ./生产环境部署/deploy-release.sh --skip-build # 跳过本地 pnpm build,使用已有 dist
#
# 凭据:仓库根目录「生产机器账号密码」
# 生产路径:
# 前端 /iflytek/shanxia2.0/web/dist (保留远程 dist/src,禁止删除)
# 后端 /iflytek/shanxia2.0/backend/app
# 备份 /iflytek/shanxia2.0/bak/
# ============================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CRED_FILE="${CRED_FILE:-$REPO_ROOT/生产机器账号密码}"
REMOTE_BASE="/iflytek/shanxia2.0"
REMOTE_WEB="$REMOTE_BASE/web"
REMOTE_DIST="$REMOTE_WEB/dist"
REMOTE_BACKEND="$REMOTE_BASE/backend"
REMOTE_BAK="$REMOTE_BASE/bak"
LOCAL_WEB_ADMIN="$REPO_ROOT/web-admin"
LOCAL_DIST="$LOCAL_WEB_ADMIN/dist"
LOCAL_APP="$REPO_ROOT/backend/app"
SSH_USER="root"
SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30)
SKIP_BUILD=0
for arg in "$@"; do
case "$arg" in
--skip-build) SKIP_BUILD=1 ;;
-h|--help)
sed -n '2,16p' "$0" | sed 's/^# \?//'
exit 0
;;
*)
echo "未知参数: $arg(支持 --skip-build)" >&2
exit 1
;;
esac
done
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die() {
log_err "$@"
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1。$2"
}
# ---------- 前置检查 ----------
require_cmd ssh "macOS 系统自带即可。"
require_cmd rsync "macOS 系统自带即可。"
require_cmd sshpass "请执行: brew install hudochenkov/sshpass/sshpass"
[[ -f "$CRED_FILE" ]] || die "凭据文件不存在: $CRED_FILE"
[[ -d "$LOCAL_APP" ]] || die "本地后端不存在: $LOCAL_APP"
# ---------- 解析凭据 ----------
# 支持:
# ip: 1.2.3.4
# root账号:xxx / root密码:xxx / 密码:xxx
parse_credentials() {
local ip="" pass=""
local line key val
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" || "$line" == \#* ]] && continue
# 勿用 [::] 字符类:bash 会把全角冒号与 : 当成范围,误匹配中文
if [[ "$line" == *:* ]]; then
key="${line%%:*}"
val="${line#*:}"
elif [[ "$line" == *:* ]]; then
key="${line%%:*}"
val="${line#*:}"
else
continue
fi
key="${key//[[:space:]]/}"
val="${val#"${val%%[![:space:]]*}"}"
val="${val%"${val##*[![:space:]]}"}"
case "$key" in
ip|IP|host|Host|HOST) ip="$val" ;;
root账号|root密码|密码|password|Password|PASSWORD|passwd) pass="$val" ;;
esac
done < "$CRED_FILE"
[[ -n "$ip" ]] || die "凭据文件未解析到 IP(期望行如: ip: 121.x.x.x)"
[[ -n "$pass" ]] || die "凭据文件未解析到密码(期望行如: root账号:***)"
REMOTE_HOST="$ip"
REMOTE_PASS="$pass"
}
parse_credentials
log_info "目标主机: ${SSH_USER}@${REMOTE_HOST}"
export SSHPASS="$REMOTE_PASS"
trap 'unset SSHPASS REMOTE_PASS 2>/dev/null || true' EXIT
remote_ssh() {
sshpass -e ssh "${SSH_OPTS[@]}" "${SSH_USER}@${REMOTE_HOST}" "$@"
}
remote_rsync() {
# 用法: remote_rsync [rsync-args...] src dest
sshpass -e rsync -e "ssh ${SSH_OPTS[*]}" "$@"
}
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
GIT_SHA="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
log_info "发版时间戳: $TIMESTAMP commit: $GIT_SHA"
# ---------- 1. 本地构建前端 ----------
if [[ "$SKIP_BUILD" -eq 1 ]]; then
log_warn "已跳过 pnpm build,使用现有 dist"
else
require_cmd pnpm "请安装 pnpm: npm i -g pnpm"
[[ -f "$LOCAL_WEB_ADMIN/.env.production" ]] || log_warn "未找到 web-admin/.env.production,请确认生产构建配置正确"
log_info "开始构建前端: cd web-admin && pnpm build"
(
cd "$LOCAL_WEB_ADMIN"
pnpm build
)
fi
[[ -d "$LOCAL_DIST" ]] || die "前端产物不存在: $LOCAL_DIST(请先构建或去掉 --skip-build)"
[[ -f "$LOCAL_DIST/index.html" ]] || die "前端产物异常: 缺少 $LOCAL_DIST/index.html"
# ---------- 2. 备份远程 web ----------
log_info "备份远程 web → ${REMOTE_BAK}/web-${TIMESTAMP}"
remote_ssh "mkdir -p '${REMOTE_BAK}' && \
if [[ -d '${REMOTE_WEB}' ]]; then \
cp -a '${REMOTE_WEB}' '${REMOTE_BAK}/web-${TIMESTAMP}'; \
else \
echo '远程 web 目录不存在,跳过备份' >&2; \
exit 1; \
fi"
# ---------- 3. 同步前端 dist(保留远程 dist/src)----------
log_info "同步前端到 ${REMOTE_DIST}(rsync --delete,排除 src/)"
remote_ssh "mkdir -p '${REMOTE_DIST}'"
# --exclude 'src/':不同步也不删除远程 src
# --delete:删除远程 dist 中本地已不存在的构建文件(不含被 exclude 的 src)
remote_rsync -az --delete --exclude 'src/' --exclude '.DS_Store' \
"${LOCAL_DIST}/" \
"${SSH_USER}@${REMOTE_HOST}:${REMOTE_DIST}/"
remote_ssh "[[ -d '${REMOTE_DIST}/src' ]] && echo 'OK: dist/src 仍存在' || echo 'NOTE: 远程 dist/src 当前不存在(若业务依赖请检查)'"
log_info "前端同步完成"
# ---------- 4. 备份远程 backend ----------
log_info "备份远程 backend → ${REMOTE_BAK}/backend-${TIMESTAMP}"
remote_ssh "mkdir -p '${REMOTE_BAK}' && \
if [[ -d '${REMOTE_BACKEND}' ]]; then \
cp -a '${REMOTE_BACKEND}' '${REMOTE_BAK}/backend-${TIMESTAMP}'; \
else \
echo '远程 backend 目录不存在,跳过备份' >&2; \
exit 1; \
fi"
# ---------- 5. 替换 backend/app,并同步依赖清单 ----------
log_info "替换远程 ${REMOTE_BACKEND}/app"
remote_ssh "rm -rf '${REMOTE_BACKEND}/app'"
remote_rsync -az --delete \
--exclude '__pycache__/' \
--exclude '*.pyc' \
--exclude '.DS_Store' \
"${LOCAL_APP}/" \
"${SSH_USER}@${REMOTE_HOST}:${REMOTE_BACKEND}/app/"
if [[ -f "$REPO_ROOT/backend/requirements.txt" ]]; then
log_info "同步 requirements.txt 并安装依赖"
remote_rsync -az \
"$REPO_ROOT/backend/requirements.txt" \
"${SSH_USER}@${REMOTE_HOST}:${REMOTE_BACKEND}/requirements.txt"
remote_ssh "cd '${REMOTE_BACKEND}' && \
./venv/bin/pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple"
fi
log_info "后端 app 同步完成"
# ---------- 6. 重启后端 ----------
log_info "重启后端服务 (PORT=9111 WORKERS=1)"
remote_ssh "cd '${REMOTE_BACKEND}' && \
chmod +x ./start-prod.sh && \
PORT=9111 WORKERS=1 ENV_FILE=./.env.production ./start-prod.sh restart"
# ---------- 7. 健康检查(重启后进程可能尚未监听,重试数秒)----------
log_info "健康检查 /health"
HEALTH_OUT=""
for _i in 1 2 3 4 5 6 7 8 9 10; do
HEALTH_OUT="$(remote_ssh "curl -fsS --max-time 5 http://127.0.0.1:9111/health" 2>/dev/null || true)"
if [[ "$HEALTH_OUT" == *healthy* ]]; then
break
fi
sleep 2
done
if [[ "$HEALTH_OUT" == *healthy* ]]; then
log_info "健康检查通过: $HEALTH_OUT"
else
log_err "健康检查未通过,远程输出: ${HEALTH_OUT:-<empty>}"
log_warn "可手动排查: ssh ${SSH_USER}@${REMOTE_HOST} 'cd ${REMOTE_BACKEND} && ./start-prod.sh status|logs'"
log_warn "备份位置: ${REMOTE_BAK}/web-${TIMESTAMP} ${REMOTE_BAK}/backend-${TIMESTAMP}"
exit 1
fi
unset SSHPASS REMOTE_PASS
echo
log_info "发版成功"
echo " commit: $GIT_SHA"
echo " 前端备份: ${REMOTE_BAK}/web-${TIMESTAMP}"
echo " 后端备份: ${REMOTE_BAK}/backend-${TIMESTAMP}"
echo " 管理端: http://${REMOTE_HOST}:9911"
echo