# ============================================================================= # AWOOOI - Production Deployment Pipeline (Phase 8) # ============================================================================= # 沿用 WOOO AIOps 穩定架構 # Registry: harbor.wooo.work (192.168.0.110) # K8s: 192.168.0.120 (K3s Master) # Runner: 192.168.0.110 (Self-Hosted) # # 鐵律: # - 生產環境嚴禁 Docker Compose,K8s 唯一合法部署 # - 110 金庫 Build + Push # - 120 K3s Deploy # ============================================================================= name: Deploy to Production on: push: branches: - main paths: - 'apps/api/**' - 'apps/web/**' - 'k8s/awoooi-prod/**' - '.github/workflows/deploy-prod.yml' workflow_dispatch: inputs: deploy_api: description: 'Deploy API' required: false default: true type: boolean deploy_web: description: 'Deploy Web' required: false default: true type: boolean deploy_worker: description: 'Deploy Worker' required: false default: true type: boolean skip_tests: description: 'Skip smoke tests (emergency only)' required: false default: false type: boolean env: # Harbor 金庫 (110 主機) - 使用 IP 避免 TLS 證書問題 REGISTRY: 192.168.0.110:5000 HARBOR_PROJECT: library # K3s 叢集 (120 主機) K8S_NAMESPACE: awoooi-prod KUBECONFIG: /home/wooo/.kube/config-120 # 加速配置 (沿用 AIOPS) BASE_REGISTRY: "192.168.0.110:5000/dockerhub-cache" PYPI_INDEX_URL: "http://192.168.0.110:3141/root/pypi/+simple/" PYPI_TRUSTED_HOST: "192.168.0.110" NPM_REGISTRY: "http://192.168.0.110:4873" jobs: # =========================================================================== # Stage 1: Build & Push Images (110 金庫) # =========================================================================== build: name: "Build Images" runs-on: [self-hosted, harbor, k8s] outputs: image_tag: ${{ steps.meta.outputs.tag }} api_image: ${{ steps.meta.outputs.api_image }} web_image: ${{ steps.meta.outputs.web_image }} steps: - name: Checkout uses: actions/checkout@v4 - name: Generate Image Tags id: meta run: | SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) TAG="${SHORT_SHA}-${{ github.run_id }}" echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "api_image=${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/api:${TAG}" >> $GITHUB_OUTPUT echo "web_image=${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/web:${TAG}" >> $GITHUB_OUTPUT echo "📦 Image Tag: ${TAG}" - name: Verify Docker Auth run: | if docker pull ${{ env.REGISTRY }}/library/hello-world:latest 2>/dev/null || true; then echo "✅ Harbor 認證有效" else echo "⚠️ Harbor 認證可能需要更新" fi # ----- Build API Image ----- - name: "Build API Image" if: ${{ github.event_name == 'push' || inputs.deploy_api }} env: DOCKER_BUILDKIT: 1 run: | echo "🐳 Building API image..." docker build \ --push \ -t ${{ steps.meta.outputs.api_image }} \ -t ${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/api:latest \ --build-arg BASE_REGISTRY=${{ env.BASE_REGISTRY }} \ --build-arg PYPI_INDEX_URL=${{ env.PYPI_INDEX_URL }} \ --build-arg PYPI_TRUSTED_HOST=${{ env.PYPI_TRUSTED_HOST }} \ -f apps/api/Dockerfile \ apps/api echo "✅ API Image: ${{ steps.meta.outputs.api_image }}" # ----- Build Web Image ----- - name: "Build Web Image" if: ${{ github.event_name == 'push' || inputs.deploy_web }} env: DOCKER_BUILDKIT: 1 run: | echo "🎨 Building Web image..." docker build \ --push \ -t ${{ steps.meta.outputs.web_image }} \ -t ${{ env.REGISTRY }}/${{ env.HARBOR_PROJECT }}/web:latest \ --build-arg BASE_REGISTRY=${{ env.BASE_REGISTRY }} \ --build-arg NEXT_PUBLIC_API_URL=https://awoooi.wooo.work/api \ -f apps/web/Dockerfile \ . echo "✅ Web Image: ${{ steps.meta.outputs.web_image }}" # =========================================================================== # Stage 2: Deploy to K3s (120 主機) # =========================================================================== deploy: name: "Deploy to K3s" needs: build runs-on: [self-hosted, harbor, k8s] steps: - name: Checkout uses: actions/checkout@v4 - name: Configure kubectl run: | export KUBECONFIG=${{ env.KUBECONFIG }} export PATH=$HOME/bin:$PATH echo "🔌 Connecting to K3s cluster..." kubectl cluster-info kubectl get nodes -o wide - name: Update Image Tags in Manifests run: | IMAGE_TAG="${{ needs.build.outputs.image_tag }}" echo "🔄 Updating manifests with tag: ${IMAGE_TAG}" sed -i "s|IMAGE_TAG_PLACEHOLDER|${IMAGE_TAG}|g" k8s/awoooi-prod/*.yaml grep -r "image:" k8s/awoooi-prod/ | head -10 - name: Apply Kubernetes Manifests run: | export KUBECONFIG=${{ env.KUBECONFIG }} export PATH=$HOME/bin:$PATH echo "📦 Applying K8s manifests..." # 排除 kustomization.yaml (那是給 -k 用的,不能直接 apply) for f in k8s/awoooi-prod/*.yaml; do if [[ "$(basename "$f")" != "kustomization.yaml" ]]; then kubectl apply -f "$f" --namespace=${{ env.K8S_NAMESPACE }} fi done - name: Rollout Restart Deployments run: | export KUBECONFIG=${{ env.KUBECONFIG }} export PATH=$HOME/bin:$PATH echo "🔄 Triggering rolling restart..." kubectl rollout restart deployment/awoooi-api -n ${{ env.K8S_NAMESPACE }} || true kubectl rollout restart deployment/awoooi-web -n ${{ env.K8S_NAMESPACE }} || true kubectl rollout restart deployment/awoooi-worker -n ${{ env.K8S_NAMESPACE }} || true - name: Wait for Rollout run: | export KUBECONFIG=${{ env.KUBECONFIG }} export PATH=$HOME/bin:$PATH echo "⏳ Waiting for rollout..." kubectl rollout status deployment/awoooi-api -n ${{ env.K8S_NAMESPACE }} --timeout=300s kubectl rollout status deployment/awoooi-web -n ${{ env.K8S_NAMESPACE }} --timeout=300s kubectl rollout status deployment/awoooi-worker -n ${{ env.K8S_NAMESPACE }} --timeout=180s || true - name: Verify Deployment run: | export KUBECONFIG=${{ env.KUBECONFIG }} export PATH=$HOME/bin:$PATH echo "=== Deployment Status ===" kubectl get pods -n ${{ env.K8S_NAMESPACE }} -l system=awoooi -o wide echo "" echo "=== Services ===" kubectl get svc -n ${{ env.K8S_NAMESPACE }} -l system=awoooi # =========================================================================== # Stage 3: Smoke Tests # =========================================================================== smoke-test: name: "Smoke Tests" needs: deploy if: ${{ !inputs.skip_tests }} runs-on: [self-hosted, harbor, k8s] steps: - name: API Health Check run: | echo "🏥 Running API health check..." for i in {1..10}; do if curl -sf http://192.168.0.120:32334/api/v1/health; then echo "" echo "✅ API is healthy" exit 0 fi echo "Attempt $i/10 failed, retrying in 10s..." sleep 10 done echo "❌ API health check failed after 10 attempts" exit 1 - name: Web Health Check run: | echo "🏥 Running Web health check..." for i in {1..5}; do if curl -sf http://192.168.0.120:32335/ -o /dev/null; then echo "✅ Web is healthy" exit 0 fi echo "Attempt $i/5 failed, retrying in 5s..." sleep 5 done echo "⚠️ Web health check failed" # =========================================================================== # Stage 4: Notify (閉環通報) # =========================================================================== notify: name: "Send Notification" needs: [build, deploy, smoke-test] if: always() runs-on: [self-hosted, harbor, k8s] steps: - name: Determine Status id: status run: | BUILD="${{ needs.build.result }}" DEPLOY="${{ needs.deploy.result }}" SMOKE="${{ needs.smoke-test.result }}" if [ "$BUILD" = "success" ] && [ "$DEPLOY" = "success" ]; then if [ "$SMOKE" = "success" ] || [ "$SMOKE" = "skipped" ]; then echo "status=success" >> $GITHUB_OUTPUT echo "emoji=✅" >> $GITHUB_OUTPUT echo "message=部署成功" >> $GITHUB_OUTPUT else echo "status=warning" >> $GITHUB_OUTPUT echo "emoji=⚠️" >> $GITHUB_OUTPUT echo "message=部署完成但健康檢查失敗" >> $GITHUB_OUTPUT fi else echo "status=failure" >> $GITHUB_OUTPUT echo "emoji=❌" >> $GITHUB_OUTPUT echo "message=部署失敗" >> $GITHUB_OUTPUT fi - name: Send Telegram Notification env: TELEGRAM_TOKEN: ${{ secrets.OPENCLAW_TG_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.OPENCLAW_TG_CHAT_ID }} BUILD_TAG: ${{ needs.build.outputs.image_tag }} BUILD_STATUS: ${{ steps.status.outputs.message }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | STATUS="${{ steps.status.outputs.status }}" if [ "$STATUS" = "success" ]; then EMOJI="✅"; else EMOJI="❌"; fi MSG="${EMOJI} AWOOOI ${BUILD_STATUS} | Tag: ${BUILD_TAG} | Actor: ${{ github.actor }} | ${RUN_URL}" curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" -d chat_id="${TELEGRAM_CHAT_ID}" -d text="${MSG}" - name: Send OpenClaw Webhook if: always() run: | curl -sf -X POST "http://192.168.0.188:8088/api/v1/webhook/deploy" \ -H "Content-Type: application/json" \ -d "{ \"event\": \"deploy_${{ steps.status.outputs.status }}\", \"system\": \"awoooi\", \"env\": \"prod\", \"tag\": \"${{ needs.build.outputs.image_tag }}\", \"actor\": \"${{ github.actor }}\", \"commit\": \"${{ github.sha }}\", \"run_url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\" }" || echo "⚠️ OpenClaw webhook 發送失敗 (非阻塞)"