From cc210f9fda05141f45debacc9a2e10307d449c49 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 22 Oct 2025 11:02:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E5=8C=96=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E4=BB=A3=E7=A0=81=E5=88=9D=E6=AD=A5=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- .env.deploy.example | 63 +++++ package.json | 4 +- scripts/deploy-from-local.sh | 392 +++++++++++++++++++++++++++++++ scripts/deploy-on-server.sh | 313 +++++++++++++++++++++++++ scripts/notify-wechat.sh | 234 +++++++++++++++++++ scripts/rollback-from-local.sh | 187 +++++++++++++++ scripts/rollback-on-server.sh | 176 ++++++++++++++ scripts/setup-deployment.sh | 407 +++++++++++++++++++++++++++++++++ 9 files changed, 1777 insertions(+), 2 deletions(-) create mode 100644 .env.deploy.example create mode 100755 scripts/deploy-from-local.sh create mode 100755 scripts/deploy-on-server.sh create mode 100755 scripts/notify-wechat.sh create mode 100755 scripts/rollback-from-local.sh create mode 100755 scripts/rollback-on-server.sh create mode 100755 scripts/setup-deployment.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 726f6789..ee4365a3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,8 @@ "allow": [ "Read(//Users/qiye/**)", "Bash(npm run lint:check)", - "Bash(npm run build)" + "Bash(npm run build)", + "Bash(chmod +x /Users/qiye/Desktop/jzqy/vf_react/scripts/*.sh)" ], "deny": [], "ask": [] diff --git a/.env.deploy.example b/.env.deploy.example new file mode 100644 index 00000000..bdaea6a1 --- /dev/null +++ b/.env.deploy.example @@ -0,0 +1,63 @@ +# 部署配置文件 +# 首次使用请复制此文件为 .env.deploy 并填写真实配置 + +# ==================== 服务器配置 ==================== +# 服务器 IP 或域名 +SERVER_HOST=your-server-ip-or-domain + +# SSH 用户名 +SERVER_USER=ubuntu + +# SSH 端口 +SERVER_PORT=22 + +# SSH 密钥路径(留空使用默认 ~/.ssh/id_rsa) +SSH_KEY_PATH= + +# ==================== 路径配置 ==================== +# 服务器上的 Git 仓库路径 +REMOTE_PROJECT_PATH=/home/ubuntu/vf_react + +# 生产环境部署路径 +PRODUCTION_PATH=/var/www/valuefrontier.cn + +# 部署备份目录 +BACKUP_DIR=/home/ubuntu/deployments + +# 部署日志目录 +LOG_DIR=/home/ubuntu/deploy-logs + +# ==================== Git 配置 ==================== +# 部署分支 +DEPLOY_BRANCH=feature + +# ==================== 备份配置 ==================== +# 保留备份数量 +KEEP_BACKUPS=5 + +# ==================== 企业微信通知配置 ==================== +# 是否启用企业微信通知 (true/false) +ENABLE_WECHAT_NOTIFY=false + +# 企业微信机器人 Webhook URL +WECHAT_WEBHOOK_URL= + +# 通知提及的用户(@all 或 手机号/userid) +WECHAT_MENTIONED_LIST= + +# ==================== 部署配置 ==================== +# 是否在部署前运行 npm install (true/false) +RUN_NPM_INSTALL=true + +# 是否在部署前运行 npm test (true/false) +RUN_NPM_TEST=false + +# 构建命令 +BUILD_COMMAND=npm run build + +# ==================== 高级配置 ==================== +# SSH 连接超时时间(秒) +SSH_TIMEOUT=30 + +# 部署超时时间(秒) +DEPLOY_TIMEOUT=600 diff --git a/package.json b/package.json index 5ef170c7..29d5fb90 100755 --- a/package.json +++ b/package.json @@ -97,7 +97,9 @@ "build:analyze": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' ANALYZE=true craco build", "test": "craco test --env=jsdom", "eject": "react-scripts eject", - "deploy": "npm run build", + "deploy": "bash scripts/deploy-from-local.sh", + "deploy:setup": "bash scripts/setup-deployment.sh", + "rollback": "bash scripts/rollback-from-local.sh", "lint:check": "eslint . --ext=js,jsx; exit 0", "lint:fix": "eslint . --ext=js,jsx --fix; exit 0", "install:clean": "rm -rf node_modules/ && rm -rf package-lock.json && npm install && npm start" diff --git a/scripts/deploy-from-local.sh b/scripts/deploy-from-local.sh new file mode 100755 index 00000000..692b1af4 --- /dev/null +++ b/scripts/deploy-from-local.sh @@ -0,0 +1,392 @@ +#!/bin/bash + +############################################################################### +# 本地部署脚本 +# 在本地运行,通过 SSH 连接服务器并执行部署 +############################################################################### + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +############################################################################### +# 函数:打印带颜色的消息 +############################################################################### +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[✓]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[⚠]${NC} $1"; } +log_error() { echo -e "${RED}[✗]${NC} $1"; } +log_step() { echo -e "${CYAN}${BOLD}[$1]${NC} $2"; } + +############################################################################### +# 函数:加载配置文件 +############################################################################### +load_config() { + if [ ! -f "$PROJECT_ROOT/.env.deploy" ]; then + log_error "配置文件不存在: $PROJECT_ROOT/.env.deploy" + echo "" + echo "请先运行以下命令进行配置:" + echo " npm run deploy:setup" + echo "" + echo "或者手动创建配置文件:" + echo " cp .env.deploy.example .env.deploy" + echo "" + exit 1 + fi + + # 加载配置 + source "$PROJECT_ROOT/.env.deploy" + + # 检查必需的配置项 + if [ -z "$SERVER_HOST" ] || [ -z "$SERVER_USER" ]; then + log_error "配置不完整,请检查 .env.deploy 文件" + echo "必需配置项:" + echo " - SERVER_HOST: 服务器地址" + echo " - SERVER_USER: SSH 用户名" + exit 1 + fi + + log_success "配置加载完成" +} + +############################################################################### +# 函数:检查本地 Git 状态 +############################################################################### +check_local_git() { + log_step "1/8" "检查本地代码" + + cd "$PROJECT_ROOT" + + # 检查是否是 Git 仓库 + if [ ! -d ".git" ]; then + log_error "当前目录不是 Git 仓库" + exit 1 + fi + + # 获取当前分支 + local current_branch=$(git branch --show-current) + log_info "当前分支: $current_branch" + + # 检查是否有未提交的更改 + if ! git diff-index --quiet HEAD --; then + log_warning "存在未提交的更改" + echo "" + git status --short + echo "" + read -p "是否继续部署? (y/n): " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "部署已取消" + exit 0 + fi + fi + + # 获取最新提交信息 + COMMIT_HASH=$(git rev-parse --short HEAD) + COMMIT_MESSAGE=$(git log -1 --pretty=%B | head -n 1) + COMMIT_AUTHOR=$(git log -1 --pretty=%an) + + log_info "最新提交: $COMMIT_HASH - $COMMIT_MESSAGE" + log_info "提交作者: $COMMIT_AUTHOR" + + log_success "本地代码检查完成" + echo "" +} + +############################################################################### +# 函数:显示部署预览 +############################################################################### +show_deploy_preview() { + log_step "2/8" "部署预览" + + echo "" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ 部署预览 ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + echo -e "${BOLD}项目信息:${NC}" + echo " 项目名称: vf_react" + echo " 部署环境: 生产环境" + echo " 目标服务器: $SERVER_USER@$SERVER_HOST" + echo "" + echo -e "${BOLD}代码信息:${NC}" + echo " 当前分支: $(git branch --show-current)" + echo " 提交版本: $COMMIT_HASH" + echo " 提交信息: $COMMIT_MESSAGE" + echo " 提交作者: $COMMIT_AUTHOR" + echo "" + echo -e "${BOLD}部署路径:${NC}" + echo " Git 仓库: $REMOTE_PROJECT_PATH" + echo " 生产目录: $PRODUCTION_PATH" + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + + # 询问是否继续 + read -p "确认部署到生产环境? (yes/no): " -r + echo "" + if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + log_info "部署已取消" + exit 0 + fi +} + +############################################################################### +# 函数:测试 SSH 连接 +############################################################################### +test_ssh_connection() { + log_step "3/8" "测试 SSH 连接" + + local ssh_options="-o ConnectTimeout=${SSH_TIMEOUT:-30} -o BatchMode=yes" + + if [ -n "$SSH_KEY_PATH" ]; then + ssh_options="$ssh_options -i $SSH_KEY_PATH" + fi + + if [ -n "$SERVER_PORT" ]; then + ssh_options="$ssh_options -p $SERVER_PORT" + fi + + # 测试连接 + if ssh $ssh_options "$SERVER_USER@$SERVER_HOST" "echo 'SSH 连接成功'" > /dev/null 2>&1; then + log_success "SSH 连接成功" + else + log_error "SSH 连接失败" + echo "" + echo "请检查:" + echo " 1. 服务器地址是否正确: $SERVER_HOST" + echo " 2. SSH 用户名是否正确: $SERVER_USER" + echo " 3. SSH 密钥是否配置正确" + echo " 4. 服务器端口是否正确: ${SERVER_PORT:-22}" + exit 1 + fi + + echo "" +} + +############################################################################### +# 函数:上传服务器端脚本 +############################################################################### +upload_server_scripts() { + log_step "4/8" "上传部署脚本" + + local ssh_options="" + if [ -n "$SSH_KEY_PATH" ]; then + ssh_options="-i $SSH_KEY_PATH" + fi + if [ -n "$SERVER_PORT" ]; then + ssh_options="$ssh_options -P $SERVER_PORT" + fi + + # 创建远程脚本目录 + ssh $ssh_options "$SERVER_USER@$SERVER_HOST" "mkdir -p /tmp/deploy-scripts" || { + log_error "创建远程目录失败" + exit 1 + } + + # 上传脚本 + scp $ssh_options \ + "$SCRIPT_DIR/deploy-on-server.sh" \ + "$SCRIPT_DIR/rollback-on-server.sh" \ + "$SCRIPT_DIR/notify-wechat.sh" \ + "$SERVER_USER@$SERVER_HOST":/tmp/deploy-scripts/ > /dev/null 2>&1 || { + log_error "上传脚本失败" + exit 1 + } + + # 设置执行权限 + ssh $ssh_options "$SERVER_USER@$SERVER_HOST" "chmod +x /tmp/deploy-scripts/*.sh" || { + log_error "设置脚本权限失败" + exit 1 + } + + log_success "部署脚本上传完成" + echo "" +} + +############################################################################### +# 函数:执行服务器端部署 +############################################################################### +execute_remote_deployment() { + log_step "5/8" "执行远程部署" + echo "" + + local ssh_options="" + if [ -n "$SSH_KEY_PATH" ]; then + ssh_options="-i $SSH_KEY_PATH" + fi + if [ -n "$SERVER_PORT" ]; then + ssh_options="$ssh_options -p $SERVER_PORT" + fi + + # 构建环境变量 + local env_vars="REMOTE_PROJECT_PATH=$REMOTE_PROJECT_PATH " + env_vars+="PRODUCTION_PATH=$PRODUCTION_PATH " + env_vars+="BACKUP_DIR=$BACKUP_DIR " + env_vars+="LOG_DIR=$LOG_DIR " + env_vars+="DEPLOY_BRANCH=$DEPLOY_BRANCH " + env_vars+="KEEP_BACKUPS=$KEEP_BACKUPS " + env_vars+="RUN_NPM_INSTALL=$RUN_NPM_INSTALL" + + # 记录开始时间 + DEPLOY_START_TIME=$(date +%s) + + # 执行部署脚本 + ssh $ssh_options "$SERVER_USER@$SERVER_HOST" "$env_vars bash /tmp/deploy-scripts/deploy-on-server.sh" || { + log_error "远程部署失败" + send_failure_notification "部署脚本执行失败" + exit 1 + } + + # 记录结束时间 + DEPLOY_END_TIME=$(date +%s) + DEPLOY_DURATION=$((DEPLOY_END_TIME - DEPLOY_START_TIME)) + + echo "" + log_success "远程部署完成" + echo "" +} + +############################################################################### +# 函数:发送成功通知 +############################################################################### +send_success_notification() { + log_step "6/8" "发送部署通知" + + if [ "$ENABLE_WECHAT_NOTIFY" = "true" ]; then + local minutes=$((DEPLOY_DURATION / 60)) + local seconds=$((DEPLOY_DURATION % 60)) + local duration="${minutes}分${seconds}秒" + + bash "$SCRIPT_DIR/notify-wechat.sh" success \ + "$DEPLOY_BRANCH" \ + "$COMMIT_HASH" \ + "$COMMIT_MESSAGE" \ + "$duration" \ + "$USER" || { + log_warning "企业微信通知发送失败" + } + else + log_info "企业微信通知未启用" + fi + + echo "" +} + +############################################################################### +# 函数:发送失败通知 +############################################################################### +send_failure_notification() { + local error_message="$1" + + if [ "$ENABLE_WECHAT_NOTIFY" = "true" ]; then + bash "$SCRIPT_DIR/notify-wechat.sh" failure \ + "$DEPLOY_BRANCH" \ + "$error_message" \ + "$USER" 2>/dev/null || true + fi +} + +############################################################################### +# 函数:清理临时文件 +############################################################################### +cleanup() { + log_step "7/8" "清理临时文件" + + local ssh_options="" + if [ -n "$SSH_KEY_PATH" ]; then + ssh_options="-i $SSH_KEY_PATH" + fi + if [ -n "$SERVER_PORT" ]; then + ssh_options="$ssh_options -p $SERVER_PORT" + fi + + ssh $ssh_options "$SERVER_USER@$SERVER_HOST" "rm -rf /tmp/deploy-scripts" > /dev/null 2>&1 || true + + log_success "清理完成" + echo "" +} + +############################################################################### +# 函数:显示部署结果 +############################################################################### +show_deployment_result() { + log_step "8/8" "部署完成" + + local minutes=$((DEPLOY_DURATION / 60)) + local seconds=$((DEPLOY_DURATION % 60)) + + echo "" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ 🎉 部署成功! ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + echo -e "${BOLD}部署信息:${NC}" + echo " 版本: $COMMIT_HASH" + echo " 分支: $DEPLOY_BRANCH" + echo " 提交: $COMMIT_MESSAGE" + echo " 作者: $COMMIT_AUTHOR" + echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')" + echo " 耗时: ${minutes}分${seconds}秒" + echo "" + echo -e "${BOLD}访问地址:${NC}" + echo " https://valuefrontier.cn" + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" +} + +############################################################################### +# 主函数 +############################################################################### +main() { + echo "" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ VF React - 生产环境部署工具 ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + + # 加载配置 + load_config + + # 检查本地 Git 状态 + check_local_git + + # 显示部署预览 + show_deploy_preview + + # 测试 SSH 连接 + test_ssh_connection + + # 上传服务器端脚本 + upload_server_scripts + + # 执行远程部署 + execute_remote_deployment + + # 发送成功通知 + send_success_notification + + # 清理临时文件 + cleanup + + # 显示部署结果 + show_deployment_result +} + +# 错误处理 +trap 'log_error "部署过程中发生错误"; send_failure_notification "部署异常中断"; exit 1' ERR + +# 执行主函数 +main "$@" diff --git a/scripts/deploy-on-server.sh b/scripts/deploy-on-server.sh new file mode 100755 index 00000000..69f953e4 --- /dev/null +++ b/scripts/deploy-on-server.sh @@ -0,0 +1,313 @@ +#!/bin/bash + +############################################################################### +# 服务器端部署脚本 +# 此脚本在服务器上执行,由本地部署脚本远程调用 +############################################################################### + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +############################################################################### +# 配置变量(通过环境变量传入) +############################################################################### +PROJECT_PATH="${REMOTE_PROJECT_PATH:-/home/ubuntu/vf_react}" +PRODUCTION_PATH="${PRODUCTION_PATH:-/var/www/valuefrontier.cn}" +BACKUP_DIR="${BACKUP_DIR:-/home/ubuntu/deployments}" +LOG_DIR="${LOG_DIR:-/home/ubuntu/deploy-logs}" +DEPLOY_BRANCH="${DEPLOY_BRANCH:-feature}" +KEEP_BACKUPS="${KEEP_BACKUPS:-5}" +RUN_NPM_INSTALL="${RUN_NPM_INSTALL:-true}" + +############################################################################### +# 函数:打印带颜色的消息 +############################################################################### +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +############################################################################### +# 函数:创建必要的目录 +############################################################################### +create_directories() { + log_info "创建必要的目录..." + mkdir -p "$BACKUP_DIR" + mkdir -p "$LOG_DIR" + mkdir -p "$PRODUCTION_PATH" + log_success "目录创建完成" +} + +############################################################################### +# 函数:检查 Git 仓库 +############################################################################### +check_git_repo() { + log_info "检查 Git 仓库..." + + if [ ! -d "$PROJECT_PATH/.git" ]; then + log_error "Git 仓库不存在: $PROJECT_PATH" + exit 1 + fi + + cd "$PROJECT_PATH" + log_success "Git 仓库检查通过" +} + +############################################################################### +# 函数:切换到目标分支 +############################################################################### +checkout_branch() { + log_info "切换到 $DEPLOY_BRANCH 分支..." + + cd "$PROJECT_PATH" + + # 获取当前分支 + current_branch=$(git branch --show-current) + + if [ "$current_branch" != "$DEPLOY_BRANCH" ]; then + log_warning "当前分支是 $current_branch,正在切换到 $DEPLOY_BRANCH..." + git checkout "$DEPLOY_BRANCH" || { + log_error "切换分支失败" + exit 1 + } + fi + + log_success "已在 $DEPLOY_BRANCH 分支" +} + +############################################################################### +# 函数:拉取最新代码 +############################################################################### +pull_latest_code() { + log_info "拉取最新代码..." + + cd "$PROJECT_PATH" + + # 保存本地修改(如果有) + if ! git diff-index --quiet HEAD --; then + log_warning "检测到本地修改,正在暂存..." + git stash + fi + + # 拉取最新代码 + git pull origin "$DEPLOY_BRANCH" || { + log_error "拉取代码失败" + exit 1 + } + + log_success "代码更新完成" +} + +############################################################################### +# 函数:获取当前提交信息 +############################################################################### +get_commit_info() { + cd "$PROJECT_PATH" + COMMIT_HASH=$(git rev-parse --short HEAD) + COMMIT_MESSAGE=$(git log -1 --pretty=%B | head -n 1) + COMMIT_AUTHOR=$(git log -1 --pretty=%an) + COMMIT_TIME=$(git log -1 --pretty=%cd --date=format:'%Y-%m-%d %H:%M:%S') + + echo "提交哈希: $COMMIT_HASH" + echo "提交信息: $COMMIT_MESSAGE" + echo "提交作者: $COMMIT_AUTHOR" + echo "提交时间: $COMMIT_TIME" +} + +############################################################################### +# 函数:安装依赖 +############################################################################### +install_dependencies() { + if [ "$RUN_NPM_INSTALL" = "true" ]; then + log_info "安装依赖..." + + cd "$PROJECT_PATH" + + # 检查 package.json 是否变化 + if git diff HEAD@{1} HEAD --name-only | grep -q "package.json"; then + log_info "package.json 有变化,执行 npm install..." + npm install || { + log_error "依赖安装失败" + exit 1 + } + else + log_info "package.json 无变化,跳过 npm install" + fi + + log_success "依赖检查完成" + else + log_info "跳过依赖安装 (RUN_NPM_INSTALL=false)" + fi +} + +############################################################################### +# 函数:构建项目 +############################################################################### +build_project() { + log_info "构建项目..." + + cd "$PROJECT_PATH" + + # 执行构建 + npm run build || { + log_error "构建失败" + exit 1 + } + + # 检查构建产物 + if [ ! -d "$PROJECT_PATH/build" ]; then + log_error "构建产物不存在" + exit 1 + fi + + log_success "构建完成" +} + +############################################################################### +# 函数:备份当前版本 +############################################################################### +backup_current_version() { + log_info "备份当前版本..." + + local timestamp=$(date +%Y%m%d-%H%M%S) + local backup_path="$BACKUP_DIR/backup-$timestamp" + + if [ -d "$PRODUCTION_PATH" ] && [ "$(ls -A $PRODUCTION_PATH)" ]; then + mkdir -p "$backup_path" + cp -r "$PRODUCTION_PATH"/* "$backup_path/" || { + log_error "备份失败" + exit 1 + } + + # 创建符号链接指向当前版本 + ln -snf "$backup_path" "$BACKUP_DIR/current" + + log_success "备份完成: $backup_path" + echo "$backup_path" + else + log_warning "生产目录为空,跳过备份" + echo "no-backup" + fi +} + +############################################################################### +# 函数:清理旧备份 +############################################################################### +cleanup_old_backups() { + log_info "清理旧备份..." + + cd "$BACKUP_DIR" + + # 获取所有备份目录(排除 current 符号链接) + local backup_count=$(find . -maxdepth 1 -type d -name "backup-*" | wc -l) + + if [ "$backup_count" -gt "$KEEP_BACKUPS" ]; then + local to_delete=$((backup_count - KEEP_BACKUPS)) + log_info "当前有 $backup_count 个备份,保留 $KEEP_BACKUPS 个,删除 $to_delete 个" + + find . -maxdepth 1 -type d -name "backup-*" | sort | head -n "$to_delete" | while read dir; do + log_info "删除旧备份: $dir" + rm -rf "$dir" + done + + log_success "旧备份清理完成" + else + log_info "当前有 $backup_count 个备份,无需清理" + fi +} + +############################################################################### +# 函数:部署到生产环境 +############################################################################### +deploy_to_production() { + log_info "部署到生产环境..." + + # 清空生产目录 + log_info "清空生产目录: $PRODUCTION_PATH" + rm -rf "$PRODUCTION_PATH"/* + + # 复制构建产物 + log_info "复制构建产物..." + cp -r "$PROJECT_PATH/build"/* "$PRODUCTION_PATH/" || { + log_error "复制文件失败" + exit 1 + } + + # 设置权限 + chmod -R 755 "$PRODUCTION_PATH" + + log_success "部署完成" +} + +############################################################################### +# 主函数 +############################################################################### +main() { + local start_time=$(date +%s) + + echo "" + echo "========================================" + echo " 服务器端部署脚本" + echo "========================================" + echo "" + + # 创建目录 + create_directories + + # 检查 Git 仓库 + check_git_repo + + # 切换分支 + checkout_branch + + # 拉取最新代码 + pull_latest_code + + # 获取提交信息 + get_commit_info + + # 安装依赖 + install_dependencies + + # 构建项目 + build_project + + # 备份当前版本 + backup_path=$(backup_current_version) + + # 部署到生产环境 + deploy_to_production + + # 清理旧备份 + cleanup_old_backups + + # 计算耗时 + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + local minutes=$((duration / 60)) + local seconds=$((duration % 60)) + + echo "" + echo "========================================" + echo " 部署成功!" + echo "========================================" + echo "提交: $COMMIT_HASH - $COMMIT_MESSAGE" + echo "备份: $backup_path" + echo "耗时: ${minutes}分${seconds}秒" + echo "" + + # 输出结果供本地脚本解析 + echo "DEPLOY_SUCCESS=true" + echo "COMMIT_HASH=$COMMIT_HASH" + echo "COMMIT_MESSAGE=$COMMIT_MESSAGE" + echo "DEPLOY_DURATION=${minutes}分${seconds}秒" +} + +# 执行主函数 +main "$@" diff --git a/scripts/notify-wechat.sh b/scripts/notify-wechat.sh new file mode 100755 index 00000000..dea4652b --- /dev/null +++ b/scripts/notify-wechat.sh @@ -0,0 +1,234 @@ +#!/bin/bash + +############################################################################### +# 企业微信通知脚本 +# 用于发送部署成功/失败通知到企业微信群 +############################################################################### + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# 加载配置文件 +if [ -f "$PROJECT_ROOT/.env.deploy" ]; then + source "$PROJECT_ROOT/.env.deploy" +else + echo -e "${YELLOW}警告: 配置文件 .env.deploy 不存在,跳过通知${NC}" + exit 0 +fi + +# 检查是否启用通知 +if [ "$ENABLE_WECHAT_NOTIFY" != "true" ]; then + echo "企业微信通知未启用" + exit 0 +fi + +# 检查 Webhook URL +if [ -z "$WECHAT_WEBHOOK_URL" ]; then + echo -e "${YELLOW}警告: 未配置企业微信 Webhook URL${NC}" + exit 0 +fi + +############################################################################### +# 函数:发送文本消息 +############################################################################### +send_text_message() { + local content="$1" + local mentioned_list="${2:-[]}" + + local json_data=$(cat <&1) + + if echo "$rollback_output" | grep -q "ROLLBACK_SUCCESS=true"; then + # 提取回滚版本 + local rollback_version=$(echo "$rollback_output" | grep "ROLLBACK_VERSION=" | cut -d= -f2) + + # 发送通知 + if [ "$ENABLE_WECHAT_NOTIFY" = "true" ]; then + bash "$SCRIPT_DIR/notify-wechat.sh" rollback \ + "$rollback_version" \ + "$USER" || { + log_warning "企业微信通知发送失败" + } + fi + + # 显示结果 + echo "" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ 🎉 回滚成功! ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + echo -e "${BOLD}回滚信息:${NC}" + echo " 目标版本: $rollback_version" + echo " 回滚时间: $(date '+%Y-%m-%d %H:%M:%S')" + echo "" + echo -e "${BOLD}访问地址:${NC}" + echo " https://valuefrontier.cn" + echo "" + echo "════════════════════════════════════════════════════════════════" + echo "" + + log_success "回滚完成" + else + log_error "回滚失败" + echo "$rollback_output" + exit 1 + fi + + # 清理临时文件 + ssh $ssh_options "$SERVER_USER@$SERVER_HOST" "rm -f /tmp/rollback-on-server.sh" > /dev/null 2>&1 || true +} + +############################################################################### +# 主函数 +############################################################################### +main() { + local action="${1:-rollback}" + local version_index="${2:-1}" + + # 加载配置 + load_config + + case "$action" in + list) + list_backup_versions + ;; + rollback|*) + execute_rollback "$version_index" + ;; + esac +} + +# 错误处理 +trap 'log_error "回滚过程中发生错误"; exit 1' ERR + +# 执行主函数 +main "$@" diff --git a/scripts/rollback-on-server.sh b/scripts/rollback-on-server.sh new file mode 100755 index 00000000..409fbbc3 --- /dev/null +++ b/scripts/rollback-on-server.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +############################################################################### +# 服务器端回滚脚本 +# 此脚本在服务器上执行,由本地回滚脚本远程调用 +############################################################################### + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +############################################################################### +# 配置变量(通过环境变量传入) +############################################################################### +PRODUCTION_PATH="${PRODUCTION_PATH:-/var/www/valuefrontier.cn}" +BACKUP_DIR="${BACKUP_DIR:-/home/ubuntu/deployments}" + +############################################################################### +# 函数:打印带颜色的消息 +############################################################################### +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +############################################################################### +# 函数:列出可用的备份版本 +############################################################################### +list_backups() { + log_info "可用的备份版本:" + echo "" + + if [ ! -d "$BACKUP_DIR" ]; then + log_error "备份目录不存在: $BACKUP_DIR" + return 1 + fi + + cd "$BACKUP_DIR" + + # 获取所有备份目录,按时间倒序 + local backups=($(find . -maxdepth 1 -type d -name "backup-*" | sort -r)) + + if [ ${#backups[@]} -eq 0 ]; then + log_warning "没有可用的备份版本" + return 1 + fi + + local index=1 + for backup in "${backups[@]}"; do + local backup_name=$(basename "$backup") + local backup_time=$(echo "$backup_name" | sed 's/backup-//' | sed 's/-/ /') + local is_current="" + + # 检查是否是当前版本 + if [ -L "$BACKUP_DIR/current" ]; then + local current_link=$(readlink "$BACKUP_DIR/current") + if [ "$current_link" = "$backup" ] || [ "$current_link" = "$BACKUP_DIR/$backup_name" ]; then + is_current=" ${GREEN}[当前版本]${NC}" + fi + fi + + echo -e " $index. $backup_name ($backup_time)$is_current" + ((index++)) + done + + echo "" + return 0 +} + +############################################################################### +# 函数:回滚到指定版本 +############################################################################### +rollback_to_version() { + local version_index="${1:-1}" + + log_info "开始回滚到版本 #$version_index..." + + if [ ! -d "$BACKUP_DIR" ]; then + log_error "备份目录不存在: $BACKUP_DIR" + exit 1 + fi + + cd "$BACKUP_DIR" + + # 获取所有备份目录,按时间倒序 + local backups=($(find . -maxdepth 1 -type d -name "backup-*" | sort -r)) + + if [ ${#backups[@]} -eq 0 ]; then + log_error "没有可用的备份版本" + exit 1 + fi + + # 检查索引是否有效 + if [ "$version_index" -lt 1 ] || [ "$version_index" -gt "${#backups[@]}" ]; then + log_error "无效的版本索引: $version_index (可用范围: 1-${#backups[@]})" + exit 1 + fi + + # 获取目标备份 + local target_index=$((version_index - 1)) + local target_backup="${backups[$target_index]}" + local backup_name=$(basename "$target_backup") + + log_info "目标版本: $backup_name" + + # 检查备份是否存在 + if [ ! -d "$target_backup" ]; then + log_error "备份版本不存在: $target_backup" + exit 1 + fi + + # 清空生产目录 + log_info "清空生产目录: $PRODUCTION_PATH" + rm -rf "$PRODUCTION_PATH"/* + + # 恢复备份 + log_info "恢复备份文件..." + cp -r "$target_backup"/* "$PRODUCTION_PATH/" || { + log_error "恢复备份失败" + exit 1 + } + + # 设置权限 + chmod -R 755 "$PRODUCTION_PATH" + + # 更新 current 符号链接 + ln -snf "$target_backup" "$BACKUP_DIR/current" + + log_success "回滚完成" + echo "" + echo "========================================" + echo " 回滚成功!" + echo "========================================" + echo "目标版本: $backup_name" + echo "" + + # 输出结果供本地脚本解析 + echo "ROLLBACK_SUCCESS=true" + echo "ROLLBACK_VERSION=$backup_name" +} + +############################################################################### +# 主函数 +############################################################################### +main() { + local action="${1:-list}" + + echo "" + echo "========================================" + echo " 服务器端回滚脚本" + echo "========================================" + echo "" + + case "$action" in + list) + list_backups + ;; + rollback) + local version_index="${2:-1}" + rollback_to_version "$version_index" + ;; + *) + log_error "未知操作: $action" + echo "用法: $0 {list|rollback} [version_index]" + exit 1 + ;; + esac +} + +# 执行主函数 +main "$@" diff --git a/scripts/setup-deployment.sh b/scripts/setup-deployment.sh new file mode 100755 index 00000000..d048f2b3 --- /dev/null +++ b/scripts/setup-deployment.sh @@ -0,0 +1,407 @@ +#!/bin/bash + +############################################################################### +# 部署配置向导 +# 首次使用时运行,引导用户完成部署配置 +############################################################################### + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +CONFIG_FILE="$PROJECT_ROOT/.env.deploy" +EXAMPLE_FILE="$PROJECT_ROOT/.env.deploy.example" + +############################################################################### +# 函数:打印带颜色的消息 +############################################################################### +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[✓]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[⚠]${NC} $1"; } +log_error() { echo -e "${RED}[✗]${NC} $1"; } +log_step() { echo -e "${CYAN}${BOLD}[$1]${NC} $2"; } + +############################################################################### +# 函数:读取用户输入(带默认值) +############################################################################### +read_input() { + local prompt="$1" + local default="$2" + local result + + if [ -n "$default" ]; then + read -p "$prompt [$default]: " result + echo "${result:-$default}" + else + read -p "$prompt: " result + echo "$result" + fi +} + +############################################################################### +# 函数:读取密码(隐藏输入) +############################################################################### +read_password() { + local prompt="$1" + local result + + read -sp "$prompt: " result + echo "" + echo "$result" +} + +############################################################################### +# 函数:测试 SSH 连接 +############################################################################### +test_ssh() { + local host="$1" + local user="$2" + local port="$3" + local key_path="$4" + + local ssh_options="-o ConnectTimeout=10 -o BatchMode=yes" + + if [ -n "$key_path" ]; then + ssh_options="$ssh_options -i $key_path" + fi + + if [ -n "$port" ] && [ "$port" != "22" ]; then + ssh_options="$ssh_options -p $port" + fi + + ssh $ssh_options "$user@$host" "echo 'SSH 连接测试成功'" 2>/dev/null + return $? +} + +############################################################################### +# 函数:测试企业微信 Webhook +############################################################################### +test_wechat_webhook() { + local webhook_url="$1" + + local test_message='{"msgtype":"text","text":{"content":"企业微信通知测试\n发送时间: '$(date +"%Y-%m-%d %H:%M:%S")'"}}' + + local response=$(curl -s -w "\n%{http_code}" \ + -H "Content-Type: application/json" \ + -d "$test_message" \ + "$webhook_url") + + local http_code=$(echo "$response" | tail -n1) + + if [ "$http_code" -eq 200 ]; then + return 0 + else + return 1 + fi +} + +############################################################################### +# 函数:显示欢迎信息 +############################################################################### +show_welcome() { + clear + echo "" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ VF React 部署配置向导 ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + echo "本向导将帮助您完成以下配置:" + echo " 1. 服务器连接配置 (SSH)" + echo " 2. 部署路径配置" + echo " 3. 企业微信通知配置 (可选)" + echo " 4. 初始化服务器环境" + echo "" + read -p "按 Enter 键继续..." + echo "" +} + +############################################################################### +# 函数:配置服务器信息 +############################################################################### +configure_server() { + log_step "1/4" "服务器配置" + echo "" + + # 服务器地址 + SERVER_HOST=$(read_input "请输入服务器 IP 或域名") + while [ -z "$SERVER_HOST" ]; do + log_error "服务器地址不能为空" + SERVER_HOST=$(read_input "请输入服务器 IP 或域名") + done + + # SSH 用户名 + SERVER_USER=$(read_input "请输入 SSH 用户名" "ubuntu") + + # SSH 端口 + SERVER_PORT=$(read_input "请输入 SSH 端口" "22") + + # SSH 密钥路径 + local default_key="$HOME/.ssh/id_rsa" + if [ -f "$default_key" ]; then + log_info "检测到 SSH 密钥: $default_key" + local use_default=$(read_input "是否使用该密钥? (y/n)" "y") + if [ "$use_default" = "y" ] || [ "$use_default" = "Y" ]; then + SSH_KEY_PATH="$default_key" + else + SSH_KEY_PATH=$(read_input "请输入 SSH 密钥路径") + fi + else + SSH_KEY_PATH=$(read_input "请输入 SSH 密钥路径 (留空使用默认)") + fi + + # 测试 SSH 连接 + echo "" + log_info "正在测试 SSH 连接..." + if test_ssh "$SERVER_HOST" "$SERVER_USER" "$SERVER_PORT" "$SSH_KEY_PATH"; then + log_success "SSH 连接测试成功" + else + log_error "SSH 连接测试失败" + echo "" + echo "请检查:" + echo " 1. 服务器地址是否正确" + echo " 2. SSH 用户名和端口是否正确" + echo " 3. SSH 密钥是否配置正确" + echo "" + read -p "是否继续配置? (y/n): " continue_setup + if [ "$continue_setup" != "y" ] && [ "$continue_setup" != "Y" ]; then + exit 1 + fi + fi + + echo "" +} + +############################################################################### +# 函数:配置部署路径 +############################################################################### +configure_paths() { + log_step "2/4" "部署路径配置" + echo "" + + # Git 仓库路径 + REMOTE_PROJECT_PATH=$(read_input "Git 仓库路径" "/home/ubuntu/vf_react") + + # 生产环境路径 + PRODUCTION_PATH=$(read_input "生产环境路径" "/var/www/valuefrontier.cn") + + # 备份目录 + BACKUP_DIR=$(read_input "备份目录" "/home/ubuntu/deployments") + + # 日志目录 + LOG_DIR=$(read_input "日志目录" "/home/ubuntu/deploy-logs") + + # 部署分支 + DEPLOY_BRANCH=$(read_input "部署分支" "feature") + + # 保留备份数量 + KEEP_BACKUPS=$(read_input "保留备份数量" "5") + + echo "" +} + +############################################################################### +# 函数:配置企业微信通知 +############################################################################### +configure_wechat() { + log_step "3/4" "企业微信通知配置" + echo "" + + local enable_notify=$(read_input "是否启用企业微信通知? (y/n)" "n") + + if [ "$enable_notify" = "y" ] || [ "$enable_notify" = "Y" ]; then + ENABLE_WECHAT_NOTIFY="true" + + echo "" + echo "请按以下步骤获取企业微信 Webhook URL:" + echo " 1. 打开企业微信群聊" + echo " 2. 点击群设置 -> 群机器人 -> 添加机器人" + echo " 3. 复制 Webhook URL" + echo "" + + WECHAT_WEBHOOK_URL=$(read_input "请输入企业微信 Webhook URL") + + if [ -n "$WECHAT_WEBHOOK_URL" ]; then + log_info "正在测试企业微信通知..." + if test_wechat_webhook "$WECHAT_WEBHOOK_URL"; then + log_success "企业微信通知测试成功" + else + log_warning "企业微信通知测试失败,请检查 Webhook URL" + fi + fi + + WECHAT_MENTIONED_LIST=$(read_input "提及用户 (手机号/userid,留空不提及)" "") + else + ENABLE_WECHAT_NOTIFY="false" + WECHAT_WEBHOOK_URL="" + WECHAT_MENTIONED_LIST="" + fi + + echo "" +} + +############################################################################### +# 函数:初始化服务器环境 +############################################################################### +initialize_server() { + log_step "4/4" "初始化服务器环境" + echo "" + + local ssh_options="" + if [ -n "$SSH_KEY_PATH" ]; then + ssh_options="-i $SSH_KEY_PATH" + fi + if [ -n "$SERVER_PORT" ] && [ "$SERVER_PORT" != "22" ]; then + ssh_options="$ssh_options -p $SERVER_PORT" + fi + + log_info "正在创建服务器目录..." + + # 创建必要的目录 + ssh $ssh_options "$SERVER_USER@$SERVER_HOST" " + mkdir -p $BACKUP_DIR + mkdir -p $LOG_DIR + mkdir -p $PRODUCTION_PATH + " || { + log_error "创建目录失败" + return 1 + } + + log_success "服务器目录创建完成" + + # 设置脚本执行权限 + log_info "设置脚本执行权限..." + chmod +x "$SCRIPT_DIR"/*.sh + + log_success "服务器环境初始化完成" + echo "" +} + +############################################################################### +# 函数:保存配置文件 +############################################################################### +save_config() { + log_info "保存配置文件..." + + # 如果配置文件已存在,先备份 + if [ -f "$CONFIG_FILE" ]; then + local backup_file="$CONFIG_FILE.backup.$(date +%Y%m%d%H%M%S)" + cp "$CONFIG_FILE" "$backup_file" + log_info "已备份原配置文件: $backup_file" + fi + + # 从示例文件复制 + if [ -f "$EXAMPLE_FILE" ]; then + cp "$EXAMPLE_FILE" "$CONFIG_FILE" + else + touch "$CONFIG_FILE" + fi + + # 写入配置 + cat > "$CONFIG_FILE" <