Some checks failed
Build and Deploy / deploy (push) Has been cancelled
由于 `-DskipTests` 仅跳过测试执行但不会跳过测试编译,在无网络环境中可能导致测试编译依赖无法解析而失败。改为 `-Dmaven.test.skip=true` 完全跳过测试阶段,确保在离线或受限环境下的构建稳定性。
474 lines
16 KiB
Bash
474 lines
16 KiB
Bash
#!/bin/bash
|
||
# ============================================
|
||
# 芋道 (Yudao) 生产部署脚本 - 运维级
|
||
# ============================================
|
||
# 特性:
|
||
# - 优雅启停 (SIGTERM → 等待 → SIGKILL 兜底)
|
||
# - 健康检查 + 自动回滚
|
||
# - 版本备份 + 保留最近 5 个版本
|
||
# - 并发锁,防止同时执行
|
||
# - 前置检查 (磁盘/内存/端口/编译产物)
|
||
# - 结构化日志
|
||
# ============================================
|
||
set -o pipefail
|
||
|
||
# ==================== 配置 ====================
|
||
readonly PROJECT_DIR="/www/wwwroot/sionrui"
|
||
readonly BACKEND_DIR="/www/wwwroot/yudao-server"
|
||
readonly FRONTEND_DIR="/www/wwwroot/muyetools.cn"
|
||
readonly ADMIN_DIR="/www/wwwroot/8.155.172.147"
|
||
|
||
readonly BACKEND_JAR="sion-rui.jar"
|
||
readonly BACKEND_PORT=9900
|
||
readonly BACKUP_KEEP=5 # 保留最近 N 个版本
|
||
readonly SHUTDOWN_GRACE_SEC=30 # 优雅关闭等待上限
|
||
readonly HEALTH_CHECK_RETRIES=30 # 健康检查重试次数 (30 × 2s = 60s)
|
||
readonly HEALTH_CHECK_INTERVAL=2 # 健康检查间隔(秒)
|
||
|
||
readonly DEPLOY_LOCK="/tmp/yudao_deploy.lock"
|
||
readonly BUILD_LOG="/tmp/yudao_build_$(date +%Y%m%d_%H%M%S).log"
|
||
readonly DEPLOY_LOG="/tmp/yudao_deploy.log"
|
||
readonly BACKUP_DIR="$BACKEND_DIR/backups"
|
||
|
||
# Java 环境
|
||
export JAVA_HOME="/www/server/java/jdk-17.0.8"
|
||
export PATH="$JAVA_HOME/bin:$PATH"
|
||
export MAVEN_OPTS="-Xms512m -Xmx1024m"
|
||
export NODE_OPTIONS="--max-old-space-size=2048"
|
||
|
||
# Maven
|
||
MVN_EXEC=$(which mvn 2>/dev/null || find /www/server -name "mvn" -type f 2>/dev/null | head -1 || echo "mvn")
|
||
MAVEN_REPO="$PROJECT_DIR/.m2_repo"
|
||
|
||
# ==================== 工具函数 ====================
|
||
log() { echo "[$(date '+%H:%M:%S')] $1" | tee -a "$DEPLOY_LOG"; }
|
||
success() { echo "[$(date '+%H:%M:%S')] ✅ $1" | tee -a "$DEPLOY_LOG"; }
|
||
warn() { echo "[$(date '+%H:%M:%S')] ⚠️ $1" | tee -a "$DEPLOY_LOG"; }
|
||
error() { echo "[$(date '+%H:%M:%S')] ❌ $1" | tee -a "$DEPLOY_LOG"; }
|
||
die() { error "$1"; release_lock; exit 1; }
|
||
|
||
# ==================== 并发锁 ====================
|
||
acquire_lock() {
|
||
if [ -f "$DEPLOY_LOCK" ]; then
|
||
local pid
|
||
pid=$(cat "$DEPLOY_LOCK" 2>/dev/null)
|
||
if ps -p "$pid" > /dev/null 2>&1; then
|
||
die "部署脚本已在运行中 (PID=$pid),请等待完成或手动删除 $DEPLOY_LOCK"
|
||
fi
|
||
warn "发现残留锁文件(进程 $pid 已不存在),清理后继续"
|
||
rm -f "$DEPLOY_LOCK"
|
||
fi
|
||
echo $$ > "$DEPLOY_LOCK"
|
||
}
|
||
|
||
release_lock() {
|
||
rm -f "$DEPLOY_LOCK"
|
||
}
|
||
|
||
# 确保退出时释放锁
|
||
trap release_lock EXIT INT TERM
|
||
|
||
# ==================== 前置检查 ====================
|
||
pre_flight() {
|
||
log "=== 前置检查 ==="
|
||
|
||
# 磁盘空间 (至少 2GB)
|
||
local avail
|
||
avail=$(df -m "$BACKEND_DIR" | awk 'NR==2 {print $4}')
|
||
if [ "${avail:-0}" -lt 2048 ]; then
|
||
die "磁盘空间不足:${BACKEND_DIR} 可用 ${avail}MB,需要至少 2048MB"
|
||
fi
|
||
log " 磁盘可用: ${avail}MB ✓"
|
||
|
||
# 内存 (至少 512MB 空闲)
|
||
local mem_free
|
||
mem_free=$(free -m | awk '/Mem:/ {print $7}')
|
||
if [ "${mem_free:-0}" -lt 512 ]; then
|
||
warn "可用内存偏低:${mem_free}MB,构建可能失败"
|
||
else
|
||
log " 可用内存: ${mem_free}MB ✓"
|
||
fi
|
||
|
||
# JDK
|
||
if [ ! -x "$JAVA_HOME/bin/java" ]; then
|
||
die "JDK 未找到:$JAVA_HOME/bin/java"
|
||
fi
|
||
log " JDK: $($JAVA_HOME/bin/java -version 2>&1 | head -1) ✓"
|
||
|
||
# 项目目录
|
||
if [ ! -d "$PROJECT_DIR/.git" ]; then
|
||
die "项目目录不存在或不是 Git 仓库:$PROJECT_DIR"
|
||
fi
|
||
log " 项目目录: $PROJECT_DIR ✓"
|
||
|
||
# 端口 (是否被非本应用的进程占用)
|
||
local port_pid
|
||
port_pid=$(ss -tlnp 2>/dev/null | grep ":$BACKEND_PORT " | grep -oP 'pid=\K\d+' || true)
|
||
if [ -n "$port_pid" ]; then
|
||
local port_cmd
|
||
port_cmd=$(ps -p "$port_pid" -o comm= 2>/dev/null || echo "unknown")
|
||
if ! echo "$port_cmd" | grep -q "java"; then
|
||
die "端口 $BACKEND_PORT 被非 Java 进程占用 (PID=$port_pid, cmd=$port_cmd)"
|
||
fi
|
||
log " 端口 $BACKEND_PORT: 被当前后端占用 (PID=$port_pid),将在停止阶段处理"
|
||
else
|
||
log " 端口 $BACKEND_PORT: 空闲 ✓"
|
||
fi
|
||
|
||
success "前置检查通过"
|
||
}
|
||
|
||
# ==================== 停止旧服务 ====================
|
||
stop_service() {
|
||
log "=== 停止旧服务 ==="
|
||
|
||
local old_pid
|
||
old_pid=$(ps -ef | grep "$BACKEND_JAR" | grep -v grep | awk '{print $2}')
|
||
|
||
if [ -z "$old_pid" ]; then
|
||
log " 未发现运行中的旧进程"
|
||
return 0
|
||
fi
|
||
|
||
# 记录旧进程信息用于回滚
|
||
local old_start
|
||
old_start=$(ps -p "$old_pid" -o lstart= 2>/dev/null || echo "unknown")
|
||
log " 旧进程: PID=$old_pid, 启动时间=$old_start"
|
||
|
||
# Step 1: SIGTERM 优雅关闭
|
||
log " 发送 SIGTERM ..."
|
||
kill -15 "$old_pid" 2>/dev/null || true
|
||
|
||
local waited=0
|
||
while [ $waited -lt $SHUTDOWN_GRACE_SEC ]; do
|
||
if ! ps -p "$old_pid" > /dev/null 2>&1; then
|
||
success "旧进程优雅退出 (PID=$old_pid, 耗时 ${waited}s)"
|
||
break
|
||
fi
|
||
sleep 1
|
||
waited=$((waited + 1))
|
||
# 每 10 秒汇报一次状态
|
||
if [ $((waited % 10)) -eq 0 ] && [ $waited -gt 0 ]; then
|
||
log " 等待中... (${waited}s/${SHUTDOWN_GRACE_SEC}s)"
|
||
fi
|
||
done
|
||
|
||
# Step 2: 仍未退出 → SIGKILL
|
||
if ps -p "$old_pid" > /dev/null 2>&1; then
|
||
warn "优雅关闭超时 (${SHUTDOWN_GRACE_SEC}s),执行 kill -9"
|
||
kill -9 "$old_pid" 2>/dev/null || true
|
||
sleep 3
|
||
if ps -p "$old_pid" > /dev/null 2>&1; then
|
||
die "无法杀死进程 PID=$old_pid,请手动处理"
|
||
fi
|
||
log " 进程已强制终止"
|
||
fi
|
||
|
||
# Step 3: 确认端口释放
|
||
local retries=10
|
||
while [ $retries -gt 0 ]; do
|
||
if ! ss -tlnp 2>/dev/null | grep -q ":$BACKEND_PORT "; then
|
||
log " 端口 $BACKEND_PORT 已释放 ✓"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
retries=$((retries - 1))
|
||
done
|
||
|
||
# 最后手段:fuser 释放端口
|
||
warn "端口 $BACKEND_PORT 未自动释放,使用 fuser 强制释放"
|
||
fuser -k "${BACKEND_PORT}/tcp" 2>/dev/null || true
|
||
sleep 2
|
||
}
|
||
|
||
# ==================== 版本备份 ====================
|
||
backup_current() {
|
||
if [ -f "$BACKEND_DIR/$BACKEND_JAR" ]; then
|
||
mkdir -p "$BACKUP_DIR"
|
||
local backup_name="${BACKEND_JAR%.jar}_$(date +%Y%m%d_%H%M%S).jar"
|
||
cp "$BACKEND_DIR/$BACKEND_JAR" "$BACKUP_DIR/$backpack_name"
|
||
log " 已备份: $backup_name"
|
||
|
||
# 清理旧备份,只保留最近 N 个
|
||
local count
|
||
count=$(ls -1t "$BACKUP_DIR"/*.jar 2>/dev/null | wc -l)
|
||
if [ "$count" -gt "$BACKUP_KEEP" ]; then
|
||
ls -1t "$BACKUP_DIR"/*.jar | tail -n +$((BACKUP_KEEP + 1)) | xargs rm -f
|
||
log " 清理旧备份,保留最近 $BACKUP_KEEP 个"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ==================== 回滚 ====================
|
||
rollback() {
|
||
error "=== 健康检查失败,开始回滚 ==="
|
||
|
||
# 停止新进程
|
||
local new_pid="$1"
|
||
if [ -n "$new_pid" ] && ps -p "$new_pid" > /dev/null 2>&1; then
|
||
log " 停止新进程 PID=$new_pid"
|
||
kill -9 "$new_pid" 2>/dev/null || true
|
||
sleep 2
|
||
fi
|
||
|
||
# 恢复最近的备份
|
||
local latest_backup
|
||
latest_backup=$(ls -1t "$BACKUP_DIR"/*.jar 2>/dev/null | head -1)
|
||
if [ -n "$latest_backup" ]; then
|
||
log " 恢复备份: $latest_backup"
|
||
cp -f "$latest_backup" "$BACKEND_DIR/$BACKEND_JAR"
|
||
else
|
||
error "没有可用的备份文件!"
|
||
return 1
|
||
fi
|
||
|
||
# 启动旧版本
|
||
log " 启动旧版本..."
|
||
cd "$BACKEND_DIR"
|
||
nohup "$JAVA_HOME/bin/java" -Xms512m -Xmx1024m \
|
||
-jar "$BACKEND_JAR" --server.port="$BACKEND_PORT" \
|
||
> ./logs/console.log 2>&1 &
|
||
|
||
local rollback_pid=$!
|
||
log " 回滚进程 PID=$rollback_pid"
|
||
|
||
# 等待旧版本启动
|
||
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
|
||
if curl -s -o /dev/null -w "%{http_code}" \
|
||
"http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then
|
||
success "回滚成功 (PID=$rollback_pid)"
|
||
return 0
|
||
fi
|
||
if ! ps -p "$rollback_pid" > /dev/null 2>&1; then
|
||
break
|
||
fi
|
||
sleep $HEALTH_CHECK_INTERVAL
|
||
done
|
||
|
||
error "回滚失败!请手动检查!"
|
||
return 1
|
||
}
|
||
|
||
# ==================== 启动新服务 ====================
|
||
start_service() {
|
||
log "=== 启动新服务 ==="
|
||
|
||
cd "$BACKEND_DIR"
|
||
|
||
# 确保日志目录
|
||
mkdir -p "$BACKEND_DIR/logs"
|
||
|
||
nohup "$JAVA_HOME/bin/java" \
|
||
-Xms512m -Xmx1024m \
|
||
-jar "$BACKEND_JAR" --server.port="$BACKEND_PORT" \
|
||
> ./logs/console.log 2>&1 &
|
||
|
||
local new_pid=$!
|
||
log " 新进程 PID=$new_pid"
|
||
|
||
# 健康检查
|
||
log " 等待健康检查..."
|
||
local started=false
|
||
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
|
||
if ! ps -p "$new_pid" > /dev/null 2>&1; then
|
||
error "新进程已退出!最近日志:"
|
||
tail -30 "$BACKEND_DIR/logs/console.log" | while read -r line; do
|
||
error " $line"
|
||
done
|
||
rollback "$new_pid"
|
||
die "启动失败,已回滚到上一版本"
|
||
fi
|
||
|
||
if curl -s -o /dev/null -w "%{http_code}" \
|
||
"http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then
|
||
success "后端启动成功 (PID=$new_pid, 耗时 $((i * HEALTH_CHECK_INTERVAL))s)"
|
||
started=true
|
||
break
|
||
fi
|
||
|
||
# 每 5 次汇报进度
|
||
if [ $((i % 5)) -eq 0 ]; then
|
||
log " 等待中... ($((i * HEALTH_CHECK_INTERVAL))s / $((HEALTH_CHECK_RETRIES * HEALTH_CHECK_INTERVAL))s)"
|
||
fi
|
||
sleep $HEALTH_CHECK_INTERVAL
|
||
done
|
||
|
||
if [ "$started" != "true" ]; then
|
||
error "健康检查超时"
|
||
rollback "$new_pid"
|
||
die "启动超时,已回滚到上一版本"
|
||
fi
|
||
}
|
||
|
||
# ==================== 后端部署 ====================
|
||
deploy_backend() {
|
||
log ""
|
||
log "╔══════════════════════════════════════════╗"
|
||
log "║ 后端部署 ║"
|
||
log "╚══════════════════════════════════════════╝"
|
||
|
||
# 0. 前置检查
|
||
pre_flight
|
||
|
||
# 1. 拉取代码 & 构建
|
||
log "=== 构建 ==="
|
||
cd "$PROJECT_DIR"
|
||
|
||
log " 拉取最新代码..."
|
||
git fetch origin 2>&1 | tee -a "$BUILD_LOG"
|
||
local old_commit new_commit
|
||
old_commit=$(git rev-parse --short HEAD)
|
||
git reset --hard origin/main 2>&1 | tee -a "$BUILD_LOG"
|
||
new_commit=$(git rev-parse --short HEAD)
|
||
|
||
if [ "$old_commit" != "$new_commit" ]; then
|
||
log " 代码更新: ${old_commit} → ${new_commit}"
|
||
else
|
||
log " 代码无变化 (${old_commit})"
|
||
fi
|
||
|
||
log " Maven 构建..."
|
||
mkdir -p "$MAVEN_REPO" && chmod -R 777 "$MAVEN_REPO" 2>/dev/null || true
|
||
if ! $MVN_EXEC clean package -Dmaven.test.skip=true -pl yudao-server -am -T 1C \
|
||
-Dmaven.repo.local="$MAVEN_REPO" >> "$BUILD_LOG" 2>&1; then
|
||
error "Maven 构建失败!查看:tail -100 $BUILD_LOG"
|
||
die "构建失败"
|
||
fi
|
||
success "Maven 构建完成"
|
||
|
||
# 2. 检查产物
|
||
local source_jar="$PROJECT_DIR/yudao-server/target/$BACKEND_JAR"
|
||
if [ ! -f "$source_jar" ]; then
|
||
die "编译产物不存在:$source_jar"
|
||
fi
|
||
log " 产物大小: $(du -h "$source_jar" | cut -f1)"
|
||
|
||
# 3. 备份当前版本
|
||
backup_current
|
||
|
||
# 4. 停止旧服务
|
||
stop_service
|
||
|
||
# 5. 部署新 JAR
|
||
log "=== 部署 ==="
|
||
rm -f "$BACKEND_DIR/$BACKEND_JAR"
|
||
cp -f "$source_jar" "$BACKEND_DIR/"
|
||
log " JAR 已复制到 $BACKEND_DIR/"
|
||
|
||
# 6. 启动
|
||
start_service
|
||
|
||
log ""
|
||
success "后端部署完成 ($old_commit → $new_commit)"
|
||
}
|
||
|
||
# ==================== 前端部署 ====================
|
||
deploy_frontend() {
|
||
log ""
|
||
log "╔══════════════════════════════════════════╗"
|
||
log "║ 用户端前端部署 ║"
|
||
log "╚══════════════════════════════════════════╝"
|
||
|
||
log "=== 构建 ==="
|
||
cd "$PROJECT_DIR/frontend"
|
||
|
||
log " pnpm install..."
|
||
pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败"
|
||
|
||
log " pnpm build:gold..."
|
||
pnpm build:gold >> "$BUILD_LOG" 2>&1 || die "pnpm build:gold 失败"
|
||
|
||
log "=== 部署 ==="
|
||
rm -rf "${FRONTEND_DIR:?}"/*
|
||
cp -r "$PROJECT_DIR/frontend/app/web-gold/dist/"* "$FRONTEND_DIR/"
|
||
success "用户端前端部署完成"
|
||
}
|
||
|
||
deploy_admin() {
|
||
log ""
|
||
log "╔══════════════════════════════════════════╗"
|
||
log "║ 管理后台前端部署 ║"
|
||
log "╚══════════════════════════════════════════╝"
|
||
|
||
log "=== 构建 ==="
|
||
cd "$PROJECT_DIR/yudao-ui-admin-vue3"
|
||
|
||
log " pnpm install..."
|
||
pnpm install >> "$BUILD_LOG" 2>&1 || die "pnpm install 失败"
|
||
|
||
log " pnpm build:prod..."
|
||
pnpm build:prod >> "$BUILD_LOG" 2>&1 || die "pnpm build:prod 失败"
|
||
|
||
log "=== 部署 ==="
|
||
rm -rf "${ADMIN_DIR:?}"/*
|
||
cp -r "$PROJECT_DIR/yudao-ui-admin-vue3/dist-prod/"* "$ADMIN_DIR/"
|
||
success "管理后台部署完成"
|
||
}
|
||
|
||
# ==================== 部署后摘要 ====================
|
||
print_summary() {
|
||
log ""
|
||
log "╔══════════════════════════════════════════╗"
|
||
log "║ 部署摘要 ║"
|
||
log "╚══════════════════════════════════════════╝"
|
||
log " 构建日志: $BUILD_LOG"
|
||
log " 部署日志: $DEPLOY_LOG"
|
||
log " 版本备份: $BACKUP_DIR/ ($(ls -1 "$BACKUP_DIR"/*.jar 2>/dev/null | wc -l) 个)"
|
||
log " 控制台日志: $BACKEND_DIR/logs/console.log"
|
||
|
||
# 快速验证
|
||
if curl -s -o /dev/null -w "%{http_code}" \
|
||
"http://127.0.0.1:${BACKEND_PORT}/actuator/health" 2>/dev/null | grep -q "200"; then
|
||
success "健康检查: UP"
|
||
else
|
||
warn "健康检查: DOWN (可能仍在启动中)"
|
||
fi
|
||
|
||
# 磁盘
|
||
local disk_use
|
||
disk_use=$(df -h "$BACKEND_DIR" | awk 'NR==2 {print $5}')
|
||
log " 磁盘使用: ${disk_use}"
|
||
|
||
log ""
|
||
}
|
||
|
||
# ==================== 主流程 ====================
|
||
main() {
|
||
acquire_lock
|
||
|
||
# 清理残留 Maven 进程
|
||
pkill -f "maven" 2>/dev/null || true
|
||
|
||
echo "" >> "$DEPLOY_LOG"
|
||
log "═══════════════════════════════════════════"
|
||
log " 部署开始 (模式: ${1:-full})"
|
||
log "═══════════════════════════════════════════"
|
||
|
||
case "${1:-full}" in
|
||
backend-only)
|
||
deploy_backend
|
||
;;
|
||
frontend-only)
|
||
deploy_frontend
|
||
;;
|
||
admin-only)
|
||
deploy_admin
|
||
;;
|
||
backend|full|"")
|
||
deploy_backend
|
||
deploy_frontend
|
||
deploy_admin
|
||
;;
|
||
*)
|
||
echo "用法: $0 {backend-only|frontend-only|admin-only|full}"
|
||
release_lock
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
print_summary
|
||
log "🏁 部署流水线全部执行完毕 ($(date '+%H:%M:%S'))"
|
||
}
|
||
|
||
main "$@"
|