#!/bin/bash # =========================================== # Rust Backend Deployment Script # =========================================== # Usage: ./deploy.sh [development|production] [options] # # Options: # --dry-run 预览模式,不执行实际操作 # --yes, -y 跳过确认提示 # --skip-tests 跳过部署后测试 # =========================================== 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 # 解析选项 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 ;; --help|-h) echo "用法: $0 [development|production] [options]" echo "" echo "选项:" echo " --dry-run 预览模式,不执行实际操作" echo " --yes, -y 跳过确认提示" echo " --skip-tests 跳过部署后测试" 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" } # 检查依赖 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) 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 echo "" log_info "========== 部署完成! ==========" echo -e " Git: ${GIT_VERSION}" echo -e " 环境: ${APP_ENV}" echo "" } main