feat(deploy): 优化部署脚本,添加 dry-run、备份、SSH 复用

This commit is contained in:
2026-04-19 00:20:06 +08:00
parent 9d452c4e75
commit 44aa62da49
3 changed files with 324 additions and 64 deletions

View File

@@ -336,10 +336,26 @@ let new_expires = base_time + days(pkg_days);
```bash ```bash
./deploy.sh development # 部署到测试服务器 ./deploy.sh development # 部署到测试服务器
./deploy.sh production # 部署到生产服务器 ./deploy.sh production # 部署到生产服务器
``` ```
**deploy.sh 功能**:依赖检查 → 编译 → 上传二进制/配置 → 重启服务 → 部署后测试 **deploy.sh 选项:**
| 选项 | 说明 |
|------|------|
| `--dry-run` | 预览模式,不执行实际操作 |
| `--yes, -y` | 跳过确认提示 |
| `--skip-tests` | 跳过部署后测试 |
| `--help, -h` | 显示帮助信息 |
**示例:**
```bash
./deploy.sh production --dry-run # 预览生产部署
./deploy.sh production --yes # 无需确认直接部署
./deploy.sh development --skip-tests # 跳过测试
```
**deploy.sh 功能**:依赖检查 → 编译 → 备份旧版本 → 上传二进制/配置 → 重启服务 → 部署后测试
### 前端部署 ### 前端部署

311
deploy.sh
View File

@@ -1,5 +1,16 @@
#!/bin/bash #!/bin/bash
# ===========================================
# Rust Backend Deployment Script
# ===========================================
# Usage: ./deploy.sh [development|production] [options]
#
# Options:
# --dry-run 预览模式,不执行实际操作
# --yes, -y 跳过确认提示
# --skip-tests 跳过部署后测试
# ===========================================
set -euo pipefail set -euo pipefail
PROJECT_NAME="rust-backend" PROJECT_NAME="rust-backend"
@@ -7,15 +18,52 @@ REMOTE_USER="root"
REMOTE_HOST="1panel-server" REMOTE_HOST="1panel-server"
APP_ENV="${1:-production}" 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 case "${APP_ENV}" in
development) development)
REMOTE_DIR="/root/rust/rust_backend_dev" REMOTE_DIR="/root/rust/rust_backend_dev"
SERVICE_NAME="rust-backend-dev.service" SERVICE_NAME="rust-backend-dev.service"
TEST_DOMAIN="https://xmclassmate.top/dev"
;; ;;
production) production)
REMOTE_DIR="/root/rust/rust_backend" REMOTE_DIR="/root/rust/rust_backend"
SERVICE_NAME="rust-backend.service" SERVICE_NAME="rust-backend.service"
TEST_DOMAIN="https://xmclassmate.top"
;; ;;
*) *)
echo "无效的环境: ${APP_ENV}" echo "无效的环境: ${APP_ENV}"
@@ -24,88 +72,275 @@ case "${APP_ENV}" in
;; ;;
esac esac
log_info() { # 彩色输出
echo -e "\033[32m[INFO]\033[0m $1" RED='\033[0;31m'
} GREEN='\033[0;32m'
YELLOW='\033[1;33m'
log_error() { BLUE='\033[0;34m'
echo -e "\033[31m[ERROR]\033[0m $1" >&2 NC='\033[0m'
exit 1
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() { check_dependencies() {
log_step "检查依赖..."
local deps=("cargo" "rsync" "ssh") local deps=("cargo" "rsync" "ssh")
for cmd in "${deps[@]}"; do for cmd in "${deps[@]}"; do
if ! command -v "$cmd" &> /dev/null; then if ! command -v "$cmd" &> /dev/null; then
log_error "依赖命令 '$cmd' 未找到,请先安装。" log_error "依赖命令 '$cmd' 未找到,请先安装。"
fi fi
done done
log_info "所有依赖检查通过" log_info "依赖检查通过"
} }
build_project() { # 获取远程服务器 SSH ControlMaster socket 路径
log_info "开始编译项目 (release模式),环境=${APP_ENV}..." get_ssh_socket() {
if APP_ENV="${APP_ENV}" cargo build --release; then echo "/tmp/ssh-master-${REMOTE_HOST}-$$"
log_info "项目编译成功。" }
else
log_error "项目编译失败!" # 建立 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 fi
} }
upload_binary() { # 关闭 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 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 if [ ! -f "$binary_path" ]; then
log_error "编译产物未找到: $binary_path" log_error "编译产物未找到: $binary_path"
fi fi
log_info "正在上传二进制文件到 ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}..." 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}/" rsync -avzP --progress "$binary_path" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
log_info "文件上传成功。" log_info "上传完成"
} }
# 上传配置文件(只上传 .toml 文件)
upload_config() { upload_config() {
log_info "正在上传配置文件到 ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/..." log_step "上传配置文件..."
rsync -avzP --progress ./config/ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/" if [ "$DRY_RUN" = true ]; then
log_info "配置文件上传成功。" 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() { upload_test_script() {
log_info "正在上传测试脚本到 ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/..." 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}/" rsync -avzP --progress ./test_deployment.sh "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "chmod +x ${REMOTE_DIR}/test_deployment.sh" ssh "${REMOTE_USER}@${REMOTE_HOST}" "chmod +x ${REMOTE_DIR}/test_deployment.sh"
log_info "测试脚本上传成功。" log_info "测试脚本上传成"
} }
restart_remote_service() { # 重启远程服务
log_info "正在重启远程服务 '${SERVICE_NAME}'..." 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 if ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl restart ${SERVICE_NAME}"; then
log_info "远程服务重启成功" log_info "服务重启成功"
else else
log_error "远程服务重启失败!请检查服务状态和日志。" 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 fi
} }
run_deployment_tests() { # 确认部署
log_info "正在执行部署后测试..." confirm_deploy() {
if ssh "${REMOTE_USER}@${REMOTE_HOST}" "cd ${REMOTE_DIR} && ./test_deployment.sh"; then if [ "$SKIP_CONFIRM" = true ]; then
log_info "部署后测试通过。" return 0
else fi
log_error "部署后测试失败!请检查服务状态。"
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 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() { main() {
log_info "========== 开始部署 (环境: ${APP_ENV}) ==========" GIT_VERSION=$(get_git_info)
log_info "远程目录: ${REMOTE_DIR}"
log_info "服务名称: ${SERVICE_NAME}" show_deploy_info
confirm_deploy
check_dependencies check_dependencies
ssh_connect
build_project build_project
backup_old_version
upload_binary upload_binary
upload_config upload_config
upload_test_script upload_test_script
restart_remote_service restart_service
run_deployment_tests run_tests
echo ""
log_info "========== 部署完成! ==========" log_info "========== 部署完成! =========="
echo -e " Git: ${GIT_VERSION}"
echo -e " 环境: ${APP_ENV}"
echo ""
} }
main main

View File

@@ -2,8 +2,8 @@
set -euo pipefail set -euo pipefail
DEVELOPMENT_RUST_URL="http://127.0.0.1:8080" # 从环境变量获取测试域名deploy.sh 传入)
PRODUCTION_DOMAIN="https://xmclassmate.top" DOMAIN="${TEST_DOMAIN:-https://xmclassmate.top}"
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -14,56 +14,58 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
check_local_backend() { check_health() {
log_info "测试本地后端服务 (127.0.0.1:8080)..." local url="${1}/health"
log_info "检查健康端点: ${url}"
local status_code local status_code
status_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${DEVELOPMENT_RUST_URL}/health" 2>/dev/null || echo "000") status_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${url}" 2>/dev/null || echo "000")
if [ "$status_code" = "200" ]; then if [ "$status_code" = "200" ]; then
log_info "本地后端服务正常 (HTTP $status_code)" log_info "健康检查通过 (HTTP $status_code)"
return 0 return 0
else else
log_error "本地后端服务异常 (HTTP $status_code)" log_error "健康检查失败 (HTTP $status_code)"
return 1 return 1
fi fi
} }
check_domain_access() { check_api() {
log_info "测试域名访问 (${PRODUCTION_DOMAIN}/dev)..." local url="${1}/dev"
log_info "检查 API 端点: ${url}"
local status_code local status_code
status_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -L -k \ status_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -k \
-H "Host: xmclassmate.top" \ -H "Host: xmclassmate.top" \
"${PRODUCTION_DOMAIN}/dev" 2>/dev/null || echo "000") "${url}" 2>/dev/null || echo "000")
if [ "$status_code" = "200" ] || [ "$status_code" = "301" ] || [ "$status_code" = "302" ]; then if [ "$status_code" = "200" ] || [ "$status_code" = "401" ] || [ "$status_code" = "301" ] || [ "$status_code" = "302" ]; then
log_info "域名访问正常 (HTTP $status_code)" log_info "API 端点正常 (HTTP $status_code)"
return 0 return 0
else else
log_error "域名访问异常 (HTTP $status_code)" log_error "API 端点异常 (HTTP $status_code)"
return 1 return 1
fi fi
} }
check_static_files() { check_static_files() {
log_info "测试静态文件服务..." local static_files=(
local test_files=(
"/static/katex/katex.min.css" "/static/katex/katex.min.css"
"/static/css/style.css" "/static/css/style.css"
"/static/favicon.ico" "/static/favicon.ico"
) )
log_info "检查静态文件..."
local failed=0 local failed=0
for file in "${test_files[@]}"; do for file in "${static_files[@]}"; do
local status_code local status_code
status_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -L -k \ status_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -k \
-H "Host: xmclassmate.top" \ -H "Host: xmclassmate.top" \
"${PRODUCTION_DOMAIN}${file}" 2>/dev/null || echo "000") "${DOMAIN}${file}" 2>/dev/null || echo "000")
if [ "$status_code" = "200" ]; then if [ "$status_code" = "200" ]; then
log_info "静态文件 ${file} 正常 (HTTP $status_code)" log_info "静态文件 ${file} 正常"
else else
log_error "静态文件 ${file} 异常 (HTTP $status_code)" log_error "静态文件 ${file} 异常 (HTTP $status_code)"
failed=1 failed=1
@@ -76,11 +78,17 @@ check_static_files() {
main() { main() {
local failed=0 local failed=0
if ! check_local_backend; then echo ""
echo -e "${YELLOW}========== 部署后测试 ==========${NC}"
echo -e " 测试域名: ${DOMAIN}"
echo -e "================================${NC}"
echo ""
if ! check_health "${DOMAIN}"; then
failed=1 failed=1
fi fi
if ! check_domain_access; then if ! check_api "${DOMAIN}"; then
failed=1 failed=1
fi fi
@@ -88,6 +96,7 @@ main() {
failed=1 failed=1
fi fi
echo ""
if [ $failed -eq 0 ]; then if [ $failed -eq 0 ]; then
log_info "========== 所有测试通过 ==========" log_info "========== 所有测试通过 =========="
exit 0 exit 0
@@ -97,4 +106,4 @@ main() {
fi fi
} }
main "$@" main "$@"