Files
asd-backend/deploy.sh

347 lines
9.3 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 跳过部署后测试
# ===========================================
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