# ============================================================================= # 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 # 2026-05-12 Codex: GitHub 是唯讀備份,production deploy 只能從 Gitea 進入。 # 這份歷史 workflow 仍含 Harbor build/push 與 kubectl apply/rollout,會和 Gitea CD 競爭。 # 保留檔案供稽核,但停用所有 job。 on: 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 # 沿用 AIOPS 設計: 新 commit 自動取消舊 workflow concurrency: group: deploy-prod-${{ github.ref }} cancel-in-progress: true 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" if: ${{ false }} 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: Login to Harbor Registry run: | echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.HARBOR_USER }} --password-stdin echo "✅ Harbor 登入成功" # ----- Build API Image ----- # Phase 6.4i: 必須從 monorepo 根目錄建構 (context: .) # 因為 Dockerfile 需要複製 packages/lewooogo-* 本地套件 - 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 \ . 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 \ -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 if: ${{ false }} 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 與 secrets (Secrets 由手動管理,避免覆蓋) for f in k8s/awoooi-prod/*.yaml; do BASENAME="$(basename "$f")" if [[ "$BASENAME" != "kustomization.yaml" && "$BASENAME" != "03-secrets.yaml" && "$BASENAME" != "03-secrets.example.yaml" ]]; then kubectl apply -f "$f" --namespace=${{ env.K8S_NAMESPACE }} else echo "⏭️ Skipped: $BASENAME (managed separately)" 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 # Worker 暫停,不重啟 echo "ℹ️ Worker restart skipped" - 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 # Worker 暫停 (replicas=0),Phase 6.5 完善後啟用 echo "ℹ️ Worker deployment skipped (replicas=0)" - 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: ${{ false }} 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: ${{ false }} 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 # Phase 8: 升級為結構化 HTML + Inline Keyboard UX run: | STATUS="${{ steps.status.outputs.status }}" BUILD_TAG="${{ needs.build.outputs.image_tag }}" SHORT_SHA="${{ github.sha }}" SHORT_SHA="${SHORT_SHA:0:7}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" ACTOR="${{ github.actor }}" # 根據狀態設定 Emoji 與標題 if [ "$STATUS" = "success" ]; then EMOJI="✅" TITLE="部署成功" elif [ "$STATUS" = "warning" ]; then EMOJI="⚠️" TITLE="部署警告" else EMOJI="❌" TITLE="部署失敗" fi # HTML 結構化訊息 (使用 \n 換行) MESSAGE="${EMOJI} AWOOOI 部署通知\n\n━━━━━━━━━━━━━━━━━\n📦 狀態: ${TITLE}\n🌍 環境: Production\n🏷️ 版本: ${BUILD_TAG}\n🔗 Commit: ${SHORT_SHA}\n👤 觸發者: ${ACTOR}\n━━━━━━━━━━━━━━━━━" # Inline Keyboard 按鈕 (成功時多一個按鈕) if [ "$STATUS" = "success" ]; then KEYBOARD='{"inline_keyboard":[[{"text":"📋 查看部署紀錄","url":"'"${RUN_URL}"'"},{"text":"🚀 開啟正式站","url":"https://awoooi.wooo.work"}]]}' else KEYBOARD='{"inline_keyboard":[[{"text":"📋 查看部署紀錄","url":"'"${RUN_URL}"'"}]]}' fi # 發送 Telegram 訊息 (JSON payload with HTML + Inline Keyboard) # 鐵律: Token 必須用 Secrets,禁止硬編碼 curl -s -X POST "https://api.telegram.org/bot${{ secrets.OPENCLAW_TG_BOT_TOKEN }}/sendMessage" \ -H "Content-Type: application/json" \ -d "{\"chat_id\":\"${{ secrets.OPENCLAW_TG_CHAT_ID }}\",\"text\":\"${MESSAGE}\",\"parse_mode\":\"HTML\",\"reply_markup\":${KEYBOARD}}" - 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 發送失敗 (非阻塞)"