#!/bin/bash # ============================================================ # 智慧水务管理系统 - 部署脚本 # 用法: # ./scripts/deploy.sh --env testing --host testing.example.com --user deploy --tag feature-abc12345 # ./scripts/deploy.sh --notify --env production --status success --webhook "https://..." --branch main --commit abc123 --project "智慧水务" # ============================================================ set -euo pipefail # ==================== 默认配置 ==================== ENV="" HOST="" USER="deploy" TAG="latest" SSH_KEY_PATH="${SSH_KEY_PATH:-~/.ssh/id_rsa}" COMPOSE_FILE="docker-compose.yml" HEALTH_TIMEOUT=120 HEALTH_INTERVAL=5 ROLLBACK_ON_FAILURE=true # 通知参数 NOTIFY_MODE=false STATUS="" WEBHOOK="" BRANCH="" COMMIT="" PROJECT="" # ==================== 参数解析 ==================== while [[ $# -gt 0 ]]; do case $1 in --env) ENV="$2"; shift 2 ;; --host) HOST="$2"; shift 2 ;; --user) USER="$2"; shift 2 ;; --tag) TAG="$2"; shift 2 ;; --ssh-key) SSH_KEY_PATH="$2"; shift 2 ;; --compose-file) COMPOSE_FILE="$2"; shift 2 ;; --health-timeout) HEALTH_TIMEOUT="$2"; shift 2 ;; --no-rollback) ROLLBACK_ON_FAILURE=false; shift ;; --notify) NOTIFY_MODE=true; shift ;; --status) STATUS="$2"; shift 2 ;; --webhook) WEBHOOK="$2"; shift 2 ;; --branch) BRANCH="$2"; shift 2 ;; --commit) COMMIT="$2"; shift 2 ;; --project) PROJECT="$2"; shift 2 ;; -h|--help) echo "用法: $0 [选项]" echo "" echo "部署模式:" echo " --env ENV 部署环境: testing | production" echo " --host HOST 目标服务器地址" echo " --user USER SSH 用户 (默认: deploy)" echo " --tag TAG 镜像标签 (默认: latest)" echo " --ssh-key PATH SSH 私钥路径" echo " --compose-file FILE docker-compose 文件 (默认: docker-compose.yml)" echo " --health-timeout SEC 健康检查超时秒数 (默认: 120)" echo " --no-rollback 禁用失败自动回滚" echo "" echo "通知模式 (--notify):" echo " --status STATUS 部署状态: success | failure" echo " --webhook URL 企业微信 Webhook URL" echo " --branch BRANCH Git 分支名" echo " --commit SHA Git 提交 SHA" echo " --project NAME 项目名称" exit 0 ;; *) echo "❌ 未知参数: $1" exit 1 ;; esac done # ==================== 通知函数 ==================== send_notification() { if [ -z "$WEBHOOK" ] || [ "$WEBHOOK" = "" ]; then echo "⚠️ 未配置 Webhook URL,跳过通知" return 0 fi local emoji="✅" local status_text="部署成功" if [ "$STATUS" = "failure" ]; then emoji="❌" status_text="部署失败" fi local commit_short="${COMMIT:0:8}" # 企业微信 Markdown 消息格式 local payload payload=$(cat <${status_text}\n**时间:** $(date '+%Y-%m-%d %H:%M:%S')" } } EOF ) echo "📤 发送部署通知..." local response response=$(curl -s -w "\n%{http_code}" -X POST "$WEBHOOK" \ -H "Content-Type: application/json" \ -d "$payload" 2>/dev/null || echo -e "\n000") local http_code http_code=$(echo "$response" | tail -1) local body body=$(echo "$response" | head -n -1) if [ "$http_code" = "200" ]; then echo "✅ 通知发送成功" else echo "⚠️ 通知发送失败 (HTTP ${http_code}): ${body}" fi } # ==================== 健康检查 ==================== wait_for_healthy() { local host="$1" local timeout="$2" local elapsed=0 echo "⏳ 等待服务健康检查通过 (超时: ${timeout}s)..." while [ $elapsed -lt $timeout ]; do # 检查 docker compose 服务状态 local unhealthy unhealthy=$(ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "${USER}@${host}" \ "docker compose -f ${COMPOSE_FILE} ps --format '{{.Service}} {{.Health}}' 2>/dev/null | grep -v 'healthy' | grep -v 'running' | head -5" 2>/dev/null || echo "") if [ -z "$unhealthy" ]; then echo "✅ 所有服务健康运行 (${elapsed}s)" return 0 fi echo " 等待中... (${elapsed}s / ${timeout}s)" sleep "$HEALTH_INTERVAL" elapsed=$((elapsed + HEALTH_INTERVAL)) done echo "❌ 健康检查超时 (${timeout}s)" echo " 未健康的服务:" echo " $unhealthy" return 1 } # ==================== 回滚 ==================== rollback() { local host="$1" echo "🔄 执行回滚..." ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "${USER}@${host}" bash </dev/null || cd ~/water-management echo "回滚到上一个版本..." # docker compose 会自动保留上一版本镜像 docker compose -f ${COMPOSE_FILE} up -d --force-recreate 2>/dev/null || { echo "❌ 回滚失败!请手动检查" exit 1 } echo "✅ 回滚完成" ROLLBACK } # ==================== 部署 ==================== deploy() { if [ -z "$ENV" ]; then echo "❌ 必须指定 --env 参数 (testing|production)" exit 1 fi if [ -z "$HOST" ]; then echo "❌ 必须指定 --host 参数" exit 1 fi echo "=========================================" echo " 智慧水务管理系统 - 部署" echo " 环境: ${ENV}" echo " 主机: ${HOST}" echo " 标签: ${TAG}" echo "=========================================" # 1. 记录当前版本(用于回滚) echo "" echo "📋 记录当前版本..." ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "${USER}@${HOST}" bash <<'SAVE_VERSION' cd /opt/water-management 2>/dev/null || cd ~/water-management mkdir -p .deploy docker compose -f docker-compose.yml config --images 2>/dev/null | sort > .deploy/previous_images.txt || true echo "VERSION=$(date +%Y%m%d%H%M%S)" > .deploy/previous_version.txt SAVE_VERSION # 2. 更新环境变量中的镜像标签 echo "" echo "📝 更新镜像标签为: ${TAG}" ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "${USER}@${HOST}" bash </dev/null || cd ~/water-management # 更新 .env 中的 IMAGE_TAG if [ -f .env ]; then sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=${TAG}/" .env 2>/dev/null || echo "IMAGE_TAG=${TAG}" >> .env else echo "IMAGE_TAG=${TAG}" > .env fi echo "IMAGE_TAG=${TAG}" UPDATE_TAG # 3. 拉取新镜像 echo "" echo "📥 拉取新镜像..." ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "${USER}@${HOST}" bash </dev/null || cd ~/water-management docker compose -f ${COMPOSE_FILE} pull 2>&1 || { echo "❌ 镜像拉取失败" exit 1 } PULL # 4. 启动服务 echo "" echo "🚀 启动服务..." ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "${USER}@${HOST}" bash </dev/null || cd ~/water-management docker compose -f ${COMPOSE_FILE} up -d --remove-orphans 2>&1 || { echo "❌ 服务启动失败" exit 1 } UP # 5. 健康检查 echo "" if wait_for_healthy "$HOST" "$HEALTH_TIMEOUT"; then echo "" echo "=========================================" echo " ✅ 部署成功!" echo " 环境: ${ENV}" echo " 标签: ${TAG}" echo "=========================================" return 0 else echo "" echo "=========================================" echo " ❌ 部署失败!" echo "=========================================" if [ "$ROLLBACK_ON_FAILURE" = true ]; then rollback "$HOST" fi return 1 fi } # ==================== 主逻辑 ==================== if [ "$NOTIFY_MODE" = true ]; then send_notification else deploy fi