Files
asd-backend/deploy.sh

494 lines
13 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.
#!/bin/bash
# ===========================================
# Rust Backend Deployment Script
# ===========================================
# Usage: ./deploy.sh [development|production] [options]
#
# Options:
# --dry-run 预览模式,不执行实际操作
# --yes, -y 跳过确认提示
# --skip-tests 跳过部署后测试
# --rollback 回滚到上一个备份版本
# --backup-list 列出可用备份
# ===========================================
set -euo pipefail
PROJECT_NAME="rust-backend"
REMOTE_USER="root"
REMOTE_HOST="1panel-server"
APP_ENV="${1:-production}"
# 默认选项
DRY_RUN=false
SKIP_CONFIRM=false
SKIP_TESTS=false
ROLLBACK=false
LIST_BACKUPS=false
# 解析选项
shift 2>/dev/null || true
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
;;
--yes|-y)
SKIP_CONFIRM=true
;;
--skip-tests)
SKIP_TESTS=true
;;
--rollback)
ROLLBACK=true
;;
--backup-list)
LIST_BACKUPS=true
;;
--help|-h)
echo "用法: $0 [development|production] [options]"
echo ""
echo "选项:"
echo " --dry-run 预览模式,不执行实际操作"
echo " --yes, -y 跳过确认提示"
echo " --skip-tests 跳过部署后测试"
echo " --rollback 回滚到上一个备份版本"
echo " --backup-list 列出可用备份"
exit 0
;;
*)
echo "未知选项: $1"
exit 1
;;
esac
shift
done
# 根据环境选择远程目录和服务
case "${APP_ENV}" in
development)
REMOTE_DIR="/root/rust/rust_backend_dev"
SERVICE_NAME="rust-backend-dev.service"
TEST_DOMAIN="https://xmclassmate.top/dev"
;;
production)
REMOTE_DIR="/root/rust/rust_backend"
SERVICE_NAME="rust-backend.service"
TEST_DOMAIN="https://xmclassmate.top"
;;
*)
echo "无效的环境: ${APP_ENV}"
echo "支持的環境: development, production"
exit 1
;;
esac
# 彩色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
log_dry() { echo -e "${YELLOW}[DRY-RUN]${NC} $1"; }
# 获取 Git 版本信息
get_git_info() {
local git_branch
local git_commit
local git_version
git_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
git_commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
git_version="${git_branch}@${git_commit}"
echo "$git_version"
}
# 部署日志路径
get_deploy_log() {
echo "${REMOTE_DIR}/deploy.log"
}
# 记录部署日志
log_to_file() {
if [ "$DRY_RUN" = true ]; then
return 0
fi
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local log_entry="[${timestamp}] ${APP_ENV} ${GIT_VERSION} $1"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "echo '${log_entry}' >> $(get_deploy_log)" 2>/dev/null || true
}
# Webhook 通知
send_webhook() {
local status="$1"
local message="$2"
if [ -z "${WEBHOOK_URL:-}" ]; then
return 0
fi
local payload
payload=$(cat <<EOF
{
"app": "rust-backend",
"env": "${APP_ENV}",
"status": "${status}",
"version": "${GIT_VERSION}",
"message": "${message}",
"timestamp": "$(date -Iseconds)"
}
EOF
)
if [ "$DRY_RUN" = true ]; then
log_dry "curl -X POST '${WEBHOOK_URL}' -d '${payload}'"
return 0
fi
curl -s -X POST "${WEBHOOK_URL}" \
-H "Content-Type: application/json" \
-d "${payload}" >/dev/null 2>&1 || true
}
# 列出备份
list_backups() {
log_step "可用备份列表..."
echo ""
if [ "$DRY_RUN" = true ]; then
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'ls -la ${REMOTE_DIR}/${PROJECT_NAME}.backup.*'"
return 0
fi
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls -la ${REMOTE_DIR}/${PROJECT_NAME}.backup.*" 2>/dev/null || echo "无备份文件"
echo ""
}
# 回滚
rollback() {
log_step "查找最新备份..."
local latest_backup
if [ "$DRY_RUN" = true ]; then
latest_backup="${REMOTE_DIR}/${PROJECT_NAME}.backup.20240101_120000"
log_dry "latest_backup=${latest_backup}"
else
latest_backup=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \
"ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null | head -1" || echo "")
if [ -z "$latest_backup" ]; then
log_error "没有找到可用的备份"
return 1
fi
fi
echo ""
echo -e "${YELLOW}========== 回滚确认 ==========${NC}"
echo -e " 将回滚到: ${latest_backup}"
echo -e " 服务: ${SERVICE_NAME}"
echo -e "================================${NC}"
echo ""
if [ "$SKIP_CONFIRM" = false ]; then
read -p "确认回滚? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_warn "回滚已取消"
return 0
fi
fi
log_step "停止服务..."
ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl stop ${SERVICE_NAME}" 2>/dev/null || log_warn "停止服务失败,继续尝试..."
log_step "恢复备份..."
if [ "$DRY_RUN" = true ]; then
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'cp ${latest_backup} ${REMOTE_DIR}/${PROJECT_NAME}'"
else
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cp ${latest_backup} ${REMOTE_DIR}/${PROJECT_NAME}"
fi
log_step "启动服务..."
if [ "$DRY_RUN" = true ]; then
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'systemctl start ${SERVICE_NAME}'"
else
ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl start ${SERVICE_NAME}"
fi
log_info "回滚完成"
log_to_file "ROLLBACK to $(basename $latest_backup)"
send_webhook "rollback" "回滚完成: $(basename $latest_backup)"
}
# 检查依赖
check_dependencies() {
log_step "检查依赖..."
local deps=("cargo" "rsync" "ssh")
for cmd in "${deps[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
log_error "依赖命令 '$cmd' 未找到,请先安装。"
fi
done
log_info "依赖检查通过"
}
# 获取远程服务器 SSH ControlMaster socket 路径
get_ssh_socket() {
echo "/tmp/ssh-master-${REMOTE_HOST}-$$"
}
# 建立 SSH 连接(复用)
ssh_connect() {
if [ "$DRY_RUN" = true ]; then
log_dry "ssh -o ControlMaster=auto -o ControlPath=$(get_ssh_socket) -o ControlPersist=5 ${REMOTE_USER}@${REMOTE_HOST}"
return 0
fi
local socket
socket=$(get_ssh_socket)
# 检查是否已有连接
if ! ssh -o "ControlPath=${socket}" -o "ConnectionAttempts=3" -o "ConnectTimeout=10" \
-o "ControlMaster=no" -o "ControlPersist=no" \
"${REMOTE_USER}@${REMOTE_HOST}" "echo 'connection test'" &>/dev/null; then
log_info "建立 SSH 连接..."
ssh -o ControlMaster=auto -o ControlPath="${socket}" -o ControlPersist=600 \
-o ConnectionAttempts=3 -o ConnectTimeout=10 \
-N "${REMOTE_USER}@${REMOTE_HOST}" &
sleep 2
fi
}
# 关闭 SSH 连接
ssh_disconnect() {
if [ "$DRY_RUN" = true ]; then
return 0
fi
local socket
socket=$(get_ssh_socket)
ssh -o "ControlPath=${socket}" -O exit "${REMOTE_USER}@${REMOTE_HOST}" 2>/dev/null || true
}
# 编译项目
build_project() {
log_step "编译项目 (release模式)..."
if [ "$DRY_RUN" = true ]; then
log_dry "APP_ENV=\"${APP_ENV}\" cargo build --release"
return 0
fi
if APP_ENV="${APP_ENV}" cargo build --release; then
log_info "编译成功"
else
log_error "编译失败!"
fi
}
# 备份旧版本
backup_old_version() {
log_step "备份旧版本..."
local binary_path="./target/release/${PROJECT_NAME}"
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
local backup_path="${REMOTE_DIR}/${PROJECT_NAME}.backup.${timestamp}"
if [ "$DRY_RUN" = true ]; then
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'if [ -f ${REMOTE_DIR}/${PROJECT_NAME} ]; then cp ${REMOTE_DIR}/${PROJECT_NAME} ${backup_path}; fi'"
return 0
fi
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
if [ -f '${REMOTE_DIR}/${PROJECT_NAME}' ]; then
cp '${REMOTE_DIR}/${PROJECT_NAME}' '${backup_path}'
echo '已备份到: ${backup_path}'
else
echo '无旧版本需要备份'
fi
"
log_info "备份完成"
}
# 上传二进制文件
upload_binary() {
log_step "上传二进制文件..."
local binary_path="./target/release/${PROJECT_NAME}"
if [ ! -f "$binary_path" ]; then
log_error "编译产物未找到: $binary_path"
fi
if [ "$DRY_RUN" = true ]; then
log_dry "rsync -avzP '$binary_path' '${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/'"
return 0
fi
rsync -avzP --progress "$binary_path" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
log_info "上传完成"
}
# 上传配置文件(只上传 .toml 文件)
upload_config() {
log_step "上传配置文件..."
if [ "$DRY_RUN" = true ]; then
log_dry "rsync -avzP --include='*.toml' --exclude='*' ./config/ '${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/'"
return 0
fi
rsync -avzP --include='*.toml' --exclude='*' ./config/ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/"
log_info "配置文件上传完成"
}
# 上传测试脚本
upload_test_script() {
log_step "上传测试脚本..."
if [ "$DRY_RUN" = true ]; then
log_dry "rsync -avzP ./test_deployment.sh '${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/'"
return 0
fi
rsync -avzP --progress ./test_deployment.sh "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "chmod +x ${REMOTE_DIR}/test_deployment.sh"
log_info "测试脚本上传完成"
}
# 重启远程服务
restart_service() {
log_step "重启服务 '${SERVICE_NAME}'..."
if [ "$DRY_RUN" = true ]; then
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'systemctl restart ${SERVICE_NAME}'"
return 0
fi
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl restart ${SERVICE_NAME}"; then
log_info "服务重启成功"
else
log_error "服务重启失败!请检查服务状态和日志。"
fi
# 等待服务启动
sleep 3
}
# 运行部署后测试
run_tests() {
log_step "执行部署后测试..."
if [ "$DRY_RUN" = true ]; then
log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'TEST_DOMAIN=\"${TEST_DOMAIN}\" ${REMOTE_DIR}/test_deployment.sh'"
return 0
fi
if [ "$SKIP_TESTS" = true ]; then
log_warn "跳过测试(--skip-tests"
return 0
fi
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "TEST_DOMAIN=\"${TEST_DOMAIN}\" ${REMOTE_DIR}/test_deployment.sh"; then
log_info "测试通过"
else
log_error "测试失败!请检查服务状态。"
fi
}
# 确认部署
confirm_deploy() {
if [ "$SKIP_CONFIRM" = true ]; then
return 0
fi
echo ""
echo -e "${YELLOW}========== 部署确认 ==========${NC}"
echo -e " 环境: ${APP_ENV}"
echo -e " 远程目录: ${REMOTE_DIR}"
echo -e " 服务: ${SERVICE_NAME}"
echo -e " Git: ${GIT_VERSION}"
echo -e " 测试域名: ${TEST_DOMAIN}"
echo -e "================================${NC}"
echo ""
read -p "确认部署? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_warn "部署已取消"
exit 0
fi
}
# 显示部署信息
show_deploy_info() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} Rust Backend 部署脚本${NC}"
echo -e "${BLUE}========================================${NC}"
echo -e " 项目: ${PROJECT_NAME}"
echo -e " 环境: ${APP_ENV}"
echo -e " 远程主机: ${REMOTE_USER}@${REMOTE_HOST}"
echo -e " 远程目录: ${REMOTE_DIR}"
echo -e " 服务: ${SERVICE_NAME}"
echo -e " Git 版本: ${GIT_VERSION}"
if [ "$DRY_RUN" = true ]; then
echo -e " 模式: ${YELLOW}预览模式 (DRY-RUN)${NC}"
fi
echo -e "${BLUE}========================================${NC}"
echo ""
}
# 清理函数
cleanup() {
ssh_disconnect
}
trap cleanup EXIT
# 主函数
main() {
GIT_VERSION=$(get_git_info)
if [ "$LIST_BACKUPS" = true ]; then
ssh_connect
list_backups
exit 0
fi
if [ "$ROLLBACK" = true ]; then
ssh_connect
rollback
exit 0
fi
show_deploy_info
confirm_deploy
check_dependencies
ssh_connect
build_project
backup_old_version
upload_binary
upload_config
upload_test_script
restart_service
run_tests
log_to_file "DEPLOY success"
send_webhook "success" "部署完成"
echo ""
log_info "========== 部署完成! =========="
echo -e " Git: ${GIT_VERSION}"
echo -e " 环境: ${APP_ENV}"
echo ""
}
main