#!/bin/sh # # 小虾剪辑 SaaS 预发布环境部署脚本 # # 用法: # ./deploy_staging.sh # 部署指定 commit # ./deploy_staging.sh # 使用当前 HEAD # # 环境变量: # GITHUB_SHA - Git commit SHA # SKIP_MIGRATION - 跳过数据库迁移 (true/false) # SKIP_HEALTH_CHECK - 跳过健康检查 (true/false) # BUILD_WEB - 是否构建 Web 镜像 (0/1) # REBUILD_BACKEND - 是否重新构建后端镜像 (0/1) # set -eu SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" ROOT_DIR="$SCRIPT_DIR" COMPOSE_DIR="$ROOT_DIR/repo/infra/docker" ENV_FILE="$ROOT_DIR/.env" # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo "${GREEN}[INFO]${NC} $1"; } log_warn() { echo "${YELLOW}[WARN]${NC} $1"; } log_error() { echo "${RED}[ERROR]${NC} $1"; } log_step() { echo "${BLUE}[STEP]${NC} $1"; } # 显示横幅 show_banner() { echo "" echo "==========================================" echo " 小虾剪辑 SaaS 预发布环境部署脚本" echo "==========================================" echo "" } # 检查前置条件 check_prerequisites() { log_step "检查前置条件..." # 检查 Docker if ! command -v docker >/dev/null 2>&1; then log_error "Docker 未安装" exit 1 fi # 检查 Docker Compose if ! docker compose version >/dev/null 2>&1; then log_error "Docker Compose 未安装" exit 1 fi # 检查目录 if [ ! -d "$COMPOSE_DIR" ]; then log_error "找不到部署目录: $COMPOSE_DIR" exit 1 fi # 检查环境变量文件 if [ ! -f "$ENV_FILE" ]; then log_error "找不到环境变量文件: $ENV_FILE" exit 1 fi # 加载环境变量 set -a . "$ENV_FILE" set +a log_info "前置条件检查通过" } # 检查系统资源 check_resources() { log_step "检查系统资源..." # 检查内存 available_mb=$(free -m | awk '/^Mem:/{print $7}') min_memory_mb=1024 if [ "$available_mb" -lt "$min_memory_mb" ]; then log_warn "可用内存不足: ${available_mb}MiB (建议至少 ${min_memory_mb}MiB)" else log_info "内存检查通过: ${available_mb}MiB 可用" fi # 检查磁盘空间 available_gb=$(df -BG "$ROOT_DIR" | awk 'NR==2 {print $4}' | tr -d 'G') min_disk_gb=5 if [ "$available_gb" -lt "$min_disk_gb" ]; then log_error "磁盘空间不足: ${available_gb}GB (建议至少 ${min_disk_gb}GB)" exit 1 fi log_info "磁盘空间检查通过: ${available_gb}GB 可用" } # 检查基础设施容器 check_infrastructure() { log_step "检查基础设施..." containers="xiaoxia-postgres xiaoxia-redis" for name in $containers; do if docker inspect "$name" >/dev/null 2>&1; then state=$(docker inspect -f '{{.State.Status}}' "$name") if [ "$state" = "running" ]; then log_info "容器正常: $name ($state)" else log_error "容器未运行: $name ($state)" log_info "请先启动基础设施: docker compose -f infra/docker/infra.yml up -d" exit 1 fi else log_error "容器不存在: $name" log_info "请先启动基础设施: docker compose -f infra/docker/infra.yml up -d" exit 1 fi done } # 确保目录存在 ensure_directories() { log_step "确保必要目录存在..." mkdir -p "$ROOT_DIR/repo/apps/web/public" mkdir -p "$ROOT_DIR/generated" # 创建占位文件 if [ ! -f "$ROOT_DIR/repo/apps/web/public/.keep" ]; then printf 'placeholder' > "$ROOT_DIR/repo/apps/web/public/.keep" fi log_info "目录检查完成" } # 部署服务 deploy_services() { commit_sha="${1:-${GITHUB_SHA:-local}}" log_step "部署服务..." cd "$COMPOSE_DIR" # 设置环境变量 export COMPOSE_PROJECT_NAME=xiaoxia-staging-app export WEB_PORT="${WEB_PORT:-3001}" export DOCKER_BUILDKIT=0 export COMPOSE_DOCKER_CLI_BUILD=0 export APP_ENV="staging" export APP_VERSION="$commit_sha" # 设置镜像标签 web_tag="staging-${commit_sha:0:8}" export WEB_IMAGE="xiaoxia-saas-web:$web_tag" # 构建后端镜像 if [ "${REBUILD_BACKEND:-0}" = "1" ]; then if [ "${ALLOW_STAGING_BUILDS:-false}" != "true" ]; then log_error "不允许在业务服务器构建后端镜像" log_info "请在 CI 服务器构建后上传,或设置 ALLOW_STAGING_BUILDS=true" exit 1 fi log_info "构建后端镜像..." docker compose --env-file "$ENV_FILE" build --pull=false api docker compose --env-file "$ENV_FILE" build --pull=false worker fi # 构建 Web 镜像 if [ "${BUILD_WEB:-0}" = "1" ]; then if [ "${ALLOW_STAGING_BUILDS:-false}" != "true" ]; then log_error "不允许在业务服务器构建 Web 镜像" log_info "请在 CI 服务器构建后上传,或设置 ALLOW_STAGING_BUILDS=true" exit 1 fi log_info "构建 Web 镜像..." docker compose --env-file "$ENV_FILE" build --pull=false web fi # 执行数据库迁移 if [ "${SKIP_MIGRATION:-0}" != "1" ]; then log_step "执行数据库迁移..." docker compose --env-file "$ENV_FILE" run --rm --no-deps api \ sh -c 'cd /app && alembic upgrade head' || true fi # 启动服务 log_info "启动服务..." docker compose --env-file "$ENV_FILE" up -d api worker web log_info "服务部署完成" } # 健康检查 health_check() { if [ "${SKIP_HEALTH_CHECK:-false}" = "true" ]; then log_warn "跳过健康检查" return 0 fi log_step "执行健康检查..." max_attempts=30 attempt=0 # 检查 API while [ $attempt -lt $max_attempts ]; do if curl -sf "http://127.0.0.1:8000/health" >/dev/null 2>&1; then log_info "API 健康检查通过" break fi attempt=$((attempt + 1)) echo -n "." sleep 2 done echo "" if [ $attempt -eq $max_attempts ]; then log_error "API 健康检查超时" log_warn "请检查日志: docker compose -f infra/docker/compose.yml logs api" return 1 fi # 检查 Web attempt=0 while [ $attempt -lt 10 ]; do if curl -sf "http://127.0.0.1:${WEB_PORT:-3001}/" >/dev/null 2>&1; then log_info "Web 健康检查通过" break fi attempt=$((attempt + 1)) echo -n "." sleep 2 done echo "" log_info "健康检查完成" } # 显示部署状态 show_status() { log_step "部署状态:" echo "" cd "$COMPOSE_DIR" docker compose --env-file "$ENV_FILE" ps echo "" log_info "部署完成!" log_info "Commit: ${1:-${GITHUB_SHA:-local}}" log_info "Web: http://127.0.0.1:${WEB_PORT:-3001}/" log_info "API: http://127.0.0.1:8000/health" } # 主流程 main() { commit_sha="${1:-${GITHUB_SHA:-}}" show_banner # 显示配置 echo "部署配置:" echo " Commit: ${commit_sha:-当前 HEAD}" echo " 环境文件: $ENV_FILE" echo " Web 端口: ${WEB_PORT:-3001}" echo " 跳过迁移: ${SKIP_MIGRATION:-0}" echo " 跳过健康检查: ${SKIP_HEALTH_CHECK:-false}" echo " 构建后端: ${REBUILD_BACKEND:-0}" echo " 构建 Web: ${BUILD_WEB:-0}" echo "" # 执行部署流程 check_prerequisites check_resources check_infrastructure ensure_directories deploy_services "$commit_sha" if ! health_check; then log_warn "健康检查失败,但服务可能正在启动" fi show_status "$commit_sha" echo "" log_info "部署成功完成!" } main "$@"