diff --git a/.gitignore b/.gitignore index c622b30..9482034 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ * # But track these -!AGENTS.md \ No newline at end of file +!AGENTS.md +!*.service +!*.sh +!deploy.sh +!test_deployment.sh \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 67a9ee4..f5974f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -346,16 +346,75 @@ let new_expires = base_time + days(pkg_days); | `--dry-run` | 预览模式,不执行实际操作 | | `--yes, -y` | 跳过确认提示 | | `--skip-tests` | 跳过部署后测试 | +| `--rollback` | 回滚到上一个备份版本 | +| `--backup-list` | 列出可用备份 | | `--help, -h` | 显示帮助信息 | **示例:** ```bash ./deploy.sh production --dry-run # 预览生产部署 -./deploy.sh production --yes # 无需确认直接部署 +./deploy.sh production --yes # 无需确认直接部署 ./deploy.sh development --skip-tests # 跳过测试 +./deploy.sh production --rollback # 回滚到上一个版本 +./deploy.sh production --backup-list # 列出可用备份 ``` -**deploy.sh 功能**:依赖检查 → 编译 → 备份旧版本 → 上传二进制/配置 → 重启服务 → 部署后测试 +**deploy.sh 功能**:依赖检查 → 编译 → 备份旧版本 → 上传二进制/配置 → 重启服务 → 部署后测试 → 记录日志 → Webhook 通知 + +**回滚:** +```bash +./deploy.sh production --rollback # 回滚生产环境 +``` + +**Webhook 通知(可选):** +```bash +WEBHOOK_URL="https://example.com/webhook" ./deploy.sh production +``` + +**部署日志位置**:`/root/rust/rust_backend/deploy.log` + +### 部署规范 + +> **重要**:所有部署操作**必须使用 `deploy.sh`**,禁止手动操作服务器文件。 + +**禁止事项:** +- ❌ 直接 SSH 到服务器手动上传文件 +- ❌ 直接 `scp` 或 `rsync` 二进制文件到服务器 +- ❌ 直接 `systemctl restart` 服务而不通过脚本 +- ❌ 直接修改服务器上的配置文件 + +**deploy.sh 已包含的安全保障:** +- ✅ 部署前自动备份旧版本 +- ✅ 自动检查依赖 +- ✅ 自动测试 +- ✅ 部署日志记录 + +### 首次部署准备 + +首次在服务器上部署时,需要手动创建 systemd 服务文件和环境配置: + +**1. 创建 systemd 服务文件:** + +开发环境服务:`/etc/systemd/system/rust-backend-dev.service` + +生产环境服务:`/etc/systemd/system/rust-backend.service` + +参考本地文件:`rust-backend.service`(生产)、`rust-backend-dev.service`(开发) + +**2. 创建 .env 环境配置:** + +开发环境:`/root/rust/rust_backend_dev/.env` +生产环境:`/root/rust/rust_backend/.env` + +参考 `.env.example` 或询问运维获取。 + +**3. 启用服务:** + +```bash +systemctl daemon-reload +systemctl enable rust-backend-dev.service # 开发环境 +systemctl enable rust-backend.service # 生产环境 +``` ### 前端部署 @@ -370,9 +429,22 @@ const CURRENT_ENV: 'development' | 'production' = 'production'; // 发布前切 ### 部署后测试 -自动执行 `test_deployment.sh`,测试内容: +部署后验证是否成功,**直接运行** `test_deployment.sh`: + +```bash +# 设置测试域名 +export TEST_DOMAIN="https://xmclassmate.top/dev" # 开发环境 +export TEST_DOMAIN="https://xmclassmate.top" # 生产环境 + +# 运行测试 +./test_deployment.sh +``` + +**注意**:`deploy.sh` 会自动执行此测试,但也可手动单独运行验证。 + +测试内容: - 本地后端健康检查 -- 域名访问检查 +- API 端点检查 - 静态文件检查 --- diff --git a/deploy.sh b/deploy.sh index 1da711f..31709b2 100755 --- a/deploy.sh +++ b/deploy.sh @@ -9,6 +9,8 @@ # --dry-run 预览模式,不执行实际操作 # --yes, -y 跳过确认提示 # --skip-tests 跳过部署后测试 +# --rollback 回滚到上一个备份版本 +# --backup-list 列出可用备份 # =========================================== set -euo pipefail @@ -22,6 +24,8 @@ APP_ENV="${1:-production}" DRY_RUN=false SKIP_CONFIRM=false SKIP_TESTS=false +ROLLBACK=false +LIST_BACKUPS=false # 解析选项 shift 2>/dev/null || true @@ -36,6 +40,12 @@ while [[ $# -gt 0 ]]; do --skip-tests) SKIP_TESTS=true ;; + --rollback) + ROLLBACK=true + ;; + --backup-list) + LIST_BACKUPS=true + ;; --help|-h) echo "用法: $0 [development|production] [options]" echo "" @@ -43,6 +53,8 @@ while [[ $# -gt 0 ]]; do echo " --dry-run 预览模式,不执行实际操作" echo " --yes, -y 跳过确认提示" echo " --skip-tests 跳过部署后测试" + echo " --rollback 回滚到上一个备份版本" + echo " --backup-list 列出可用备份" exit 0 ;; *) @@ -98,6 +110,126 @@ get_git_info() { 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 </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 "检查依赖..." @@ -323,6 +455,18 @@ 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 @@ -336,6 +480,9 @@ main() { restart_service run_tests + log_to_file "DEPLOY success" + send_webhook "success" "部署完成" + echo "" log_info "========== 部署完成! ==========" echo -e " Git: ${GIT_VERSION}" diff --git a/rust-backend-dev.service b/rust-backend-dev.service new file mode 100644 index 0000000..d9154a0 --- /dev/null +++ b/rust-backend-dev.service @@ -0,0 +1,28 @@ +[Unit] +Description=Rust Backend Development Service +After=network.target postgresql.service +Wants=network.target + +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=/root/rust/rust_backend_dev +ExecStart=/root/rust/rust_backend_dev/rust-backend +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +EnvironmentFile=/root/rust/rust_backend_dev/.env + +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=read-only +ReadWritePaths=/root/rust/rust_backend_dev + +SyslogIdentifier=rust-backend-dev + +[Install] +WantedBy=multi-user.target