# 生产环境自动部署 # 触发条件:push 到 main 分支 # 功能:构建 backend 镜像 → 推送到 GHCR → SSH 部署到云服务器 → 健康检查 name: Deploy to Production on: push: branches: [main] workflow_dispatch: inputs: image_tag: description: '镜像标签(默认: latest)' required: false default: 'latest' env: REGISTRY: ghcr.io IMAGE_NAME_BACKEND: wecom-ai-backend jobs: # 构建并推送 Backend 镜像 build-backend: runs-on: ubuntu-latest outputs: image_tag: ${{ steps.meta.outputs.tags }} image_digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME_BACKEND }} tags: | type=raw,value=latest type=sha,prefix={{branch}}- type=sha,format=short - name: Build and push backend image id: build uses: docker/build-push-action@v6 with: context: . file: ./deploy/docker/backend.Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # 部署到生产服务器 deploy: runs-on: ubuntu-latest needs: build-backend if: github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@v4 - name: Set deployment variables id: vars run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then echo "image_tag=${{ github.event.inputs.image_tag }}" >> $GITHUB_OUTPUT else echo "image_tag=latest" >> $GITHUB_OUTPUT fi echo "domain=${{ secrets.PROD_DOMAIN }}" >> $GITHUB_OUTPUT - name: Prepare deployment token id: prepare-token run: | if [ -n "${{ secrets.GHCR_TOKEN }}" ]; then echo "token=${{ secrets.GHCR_TOKEN }}" >> $GITHUB_OUTPUT else echo "token=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_OUTPUT fi - name: Deploy to production server uses: appleboy/ssh-action@v1.0.0 env: DEPLOY_TOKEN: ${{ steps.prepare-token.outputs.token }} with: host: ${{ secrets.PROD_HOST }} username: ${{ secrets.PROD_USER }} key: ${{ secrets.PROD_SSH_KEY }} port: ${{ secrets.PROD_SSH_PORT || 22 }} script: | set -e # 设置变量 IMAGE_TAG="${{ steps.vars.outputs.image_tag }}" GITHUB_REPOSITORY_OWNER="${{ github.repository_owner }}" REGISTRY="${{ env.REGISTRY }}" IMAGE_NAME_BACKEND="${{ env.IMAGE_NAME_BACKEND }}" APP_PATH="${{ secrets.PROD_APP_PATH || '/opt/wecom-ai-assistant' }}" # 进入项目目录 cd "${APP_PATH}" || { echo "错误: 无法进入目录 ${APP_PATH}"; exit 1; } # 登录到容器镜像仓库 echo "${DEPLOY_TOKEN}" | docker login "${REGISTRY}" -u "${{ github.actor }}" --password-stdin || { echo "警告: Docker 登录失败,尝试继续部署(可能使用本地镜像)" } # 拉取最新镜像 IMAGE_FULL="${REGISTRY}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME_BACKEND}:${IMAGE_TAG}" echo "拉取镜像: ${IMAGE_FULL}" docker pull "${IMAGE_FULL}" || { echo "警告: 拉取镜像失败,尝试使用 latest 标签" docker pull "${REGISTRY}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME_BACKEND}:latest" IMAGE_TAG="latest" } # 设置环境变量供 docker-compose 使用 export IMAGE_TAG="${IMAGE_TAG}" export GITHUB_REPOSITORY_OWNER="${GITHUB_REPOSITORY_OWNER}" # 更新服务 echo "更新服务..." docker compose -f docker-compose.prod.yml --env-file .env.prod pull backend 2>/dev/null || true docker compose -f docker-compose.prod.yml --env-file .env.prod up -d --force-recreate backend # 等待服务启动 echo "等待服务启动..." sleep 15 # 检查服务状态 echo "服务状态:" docker compose -f docker-compose.prod.yml ps # 检查后端健康状态 echo "检查后端健康状态..." for i in {1..10}; do if docker compose -f docker-compose.prod.yml exec -T backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" 2>/dev/null; then echo "✓ 后端服务健康" break fi if [ $i -eq 10 ]; then echo "⚠ 后端服务可能未就绪,请检查日志" else sleep 2 fi done - name: Health check run: | DOMAIN=${{ secrets.PROD_DOMAIN }} MAX_RETRIES=30 RETRY_COUNT=0 echo "健康检查: https://${DOMAIN}/api/health" while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do if curl -f -s "https://${DOMAIN}/api/health" > /dev/null 2>&1; then echo "✓ 健康检查通过" exit 0 fi RETRY_COUNT=$((RETRY_COUNT + 1)) echo "等待服务就绪... ($RETRY_COUNT/$MAX_RETRIES)" sleep 2 done echo "✗ 健康检查失败" exit 1