# =============================================================================
# AWOOOI CD Pipeline (Gitea Actions - 方案 B)
# =============================================================================
# 流程: Build → Push to Harbor → Deploy to K8s
# 加速措施:
#   1. Docker Layer Cache → Harbor registry cache
#   2. 內部 Mirror → 192.168.0.110:5001 (Harbor Proxy Cache for DockerHub)
# 2026-03-29 Claude Code (ADR-039) - Retry after creating Harbor project

name: CD Pipeline

on:
  push:
    branches: [main]
    paths:
      # 只有實際影響部署的程式碼才觸發 CD
      - 'apps/**'
      - 'k8s/**'
      - '.gitea/workflows/**'
      - '.dockerignore'
      # docs/、memory/、ADR 等不觸發
      # ops/monitoring/alerts-unified.yml 由 deploy-alerts.yaml 獨立處理 (I3)
  workflow_dispatch:
    # 手動觸發永遠可用（用於補跑、緊急部署）

# 2026-04-02 Claude Code: 改為搶佔模式 — 新 push 立即取消舊 build，只部署最新
# 原理: concurrency group 保證同時只有一個 job 跑；cancel-in-progress:true 讓新的取代舊的
# 解決: 多個 commit 快速連推時不再排隊堆積，且 docker build 卡住時不會阻塞後續部署
# 安全: deploy 步驟本身有 kubectl rollout status 保護，不會出現半部署狀態
concurrency:
  group: cd-deploy-${{ github.ref }}
  cancel-in-progress: true

env:
  HARBOR: 192.168.0.110:5000
  # Harbor Proxy Cache (指向 DockerHub 的內部 Mirror，避免拉取限額)
  HARBOR_MIRROR: 192.168.0.110:5001
  # OTEL CI/CD 監控 (2026-03-31 #46c - 遷移到 Gitea)
  OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.0.188:24318
  OTEL_SERVICE_NAME: awoooi-cd
  OTEL_RESOURCE_ATTRIBUTES: service.version=${{ github.sha }},deployment.environment=production

jobs:
  build-and-deploy:
    # 2026-04-02 ogt: Gitea runner label 是 ubuntu-latest (非 GitHub 的 self-hosted)
    # ADR-039 鐵律: 使用自建 runner，但 Gitea label matching 不同於 GitHub
    # 2026-04-02 Claude Code: 加入 timeout 防止 docker build/push 卡住超過 45 分鐘
    timeout-minutes: 45
    runs-on: ubuntu-latest
    # 2026-04-10 ogt: B5 改用 docker run 本地啟動，移除 services: 宣告
    # Gitea act runner 的 services: container name 為空，導致 CI 失敗
    steps:
      - uses: actions/checkout@v4

      # 2026-03-31 ogt: 優化告警格式 - 提高可讀性
      - name: Get Commit Info
        id: commit
        run: |
          echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
          echo "message=$(git log -1 --pretty=%s | head -c 50)" >> $GITHUB_OUTPUT
          echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT

      - name: Notify Pipeline Start
        # 2026-04-16 ogt + Claude Sonnet 4.6: 改用 HTML 結構化格式，提升可讀性
        run: |
          COMMIT_MSG="${{ steps.commit.outputs.message }}"
          SHORT_SHA="${{ steps.commit.outputs.short_sha }}"
          ACTOR="${{ github.actor }}"
          # HTML escape commit message（防特殊字元破壞 HTML）
          COMMIT_ESC=$(echo "$COMMIT_MSG" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
          MSG=$(printf '🚀 <b>AWOOOI 部署開始</b>\n├ 📝 <code>%s</code>\n├ 🔖 <code>%s</code>\n└ 👤 %s' "${COMMIT_ESC}" "${SHORT_SHA}" "${ACTOR}")
          curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
            -H "Content-Type: application/json" \
            -d "$(jq -n --arg c "${{ secrets.TELEGRAM_CHAT_ID }}" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')"



      # 2026-03-31 ogt: Phase 22.0 CI 測試 (禁止 Mock - feedback_no_mock_testing.md)
      # 2026-04-01 ogt: 持久化 venv 加速 - /opt/api-venv 跨 run 保留
      # pyproject.toml hash 變才重裝，其餘直接 activate (節省 ~6-7 min)
      - name: Run API Tests
        run: |
          VENV=/opt/api-venv
          HASH_FILE=/opt/api-venv/.deps_hash
          CURRENT_HASH=$(md5sum apps/api/pyproject.toml | awk '{print $1}')

          # python3.11 是 runner 層級持久安裝，只在首次或版本消失時才 apt-get
          # 2026-04-05 Claude Code: 分離 apt-get 與 venv hash-guard，避免每次 deps 變更都重跑 apt
          # 2026-04-16 ogt + Claude Sonnet 4.6: 修復 apt index 失敗 → 改用 --fix-missing + retry
          if ! command -v python3.11 &>/dev/null; then
            echo "📦 安裝 python3.11..."
            apt-get clean && rm -rf /var/lib/apt/lists/*
            apt-get update -q --fix-missing || apt-get update -q || true
            apt-get install -y -q python3.11-venv python3.11 || \
              (add-apt-repository ppa:deadsnakes/python -y 2>/dev/null && apt-get update -q && apt-get install -y -q python3.11-venv python3.11) || true
          else
            echo "⚡ python3.11 已安裝，跳過 apt-get"
          fi
          # 確保 python3.11 存在，否則 fallback 到系統 python3
          if ! command -v python3.11 &>/dev/null; then
            echo "⚠️ python3.11 安裝失敗，使用 python3 fallback"
            ln -sf "$(which python3)" /usr/local/bin/python3.11 || true
          fi

          if [ ! -d "$VENV/bin" ] || [ "$(cat $HASH_FILE 2>/dev/null)" != "$CURRENT_HASH" ]; then
            echo "📦 deps 已變更，重建 venv..."
            # 2026-04-17 ogt: /opt/api-venv 是 volume mount，不能 rm -rf 目錄本身
            # 改用 find 清空內容，保留 mount point 目錄
            find "$VENV" -mindepth 1 -delete 2>/dev/null || true
            python3.11 -m venv $VENV
            source $VENV/bin/activate
            pip install -q uv
            cd apps/api && uv pip install -q -e ".[dev]" && cd -
            echo "$CURRENT_HASH" > $HASH_FILE
          else
            echo "⚡ 使用快取 venv (deps 未變更)"
            source $VENV/bin/activate
          fi

          cd apps/api
          # CI 排除需外部服務的測試 (Redis pool / Ollama — 2026-04-01 Claude Code)
          # 2026-04-05 Claude Code: 修正 exit code — | tail 會吃掉 segfault (exit 139)
          #   改用 tee + PIPESTATUS[0] 正確捕捉 pytest 本身的 exit code
          # 2026-04-05 Claude Code: 加 --ignore=tests/integration 排除需 asyncpg 連線的 DB 測試
          #   integration tests 在 prod K8s 部署後由 E2E Smoke Test 覆蓋
          # PYTHONFAULTHANDLER=1: 若 C extension segfault，輸出完整 Python stacktrace
          # 2026-04-05 Claude Code: test_github_webhook.py 已根治
          #   原問題: import src.main → asyncpg C ext segfault (exit 139)
          #   修復: 改用最小化 app，只掛載 github_webhook router，不走 DB import chain
          #   現在可安全加入 CI 測試
          PYTHONFAULTHANDLER=1 python3.11 -m pytest tests/ -v --tb=short -x \
            --ignore=tests/integration \
            --ignore=tests/test_anomaly_counter.py \
            --ignore=tests/test_global_repair_cooldown.py \
            --ignore=tests/test_redis_multisig.py \
            --ignore=tests/test_model_regression.py \
            --ignore=tests/test_prompt_validation.py \
            --ignore=tests/e2e_network_test.py \
            2>&1 | tee /tmp/pytest-output.txt; PYTEST_EXIT=${PIPESTATUS[0]}
          tail -60 /tmp/pytest-output.txt
          exit $PYTEST_EXIT

      # ── 整合測試 B5 (2026-04-10) ──────────────────────────────────────────
      # B5 整合測試 — postgres-test 由 services: 提供，localhost:15432 直連
      # 2026-04-10 Claude Sonnet 4.6: 用 psql 直連 localhost:15432 初始化 schema
      #   (docker exec 在 act runner 內無法取得 service container name)
      # B5: Gitea act runner 的 services: 實作與 GitHub Actions 不同
      # service container 啟動後需直連，但 act 的 container name 可能為空
      # 2026-04-10 ogt: 改用 docker run 本地啟動取代 services: 宣告
      - name: Integration Tests (B5 — 真實 DB)
        run: |
          cd apps/api
          # 安裝 psql client
          if ! command -v psql &>/dev/null; then
            apt-get install -y -q postgresql-client
          fi
          # 啟動測試 DB — 用 container IP 直連，避免 DinD port mapping 問題
          # 2026-04-10 Claude Sonnet 4.6: -p 15433:5432 在 act runner 內 localhost 不通
          docker rm -f pg-test-b5 2>/dev/null || true
          docker run -d --name pg-test-b5 \
            -e POSTGRES_DB=awoooi_test \
            -e POSTGRES_USER=awoooi \
            -e POSTGRES_PASSWORD=awoooi_test_2026 \
            pgvector/pgvector:pg16
          # 取得 container IP
          PG_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' pg-test-b5)
          echo "PG container IP: $PG_IP"
          # 等待就緒（用 container IP，最多 60 秒）
          for i in $(seq 1 30); do
            PGPASSWORD=awoooi_test_2026 pg_isready -h "$PG_IP" -p 5432 -U awoooi && break || sleep 2
          done
          # 初始化 schema
          PGPASSWORD=awoooi_test_2026 psql \
            -h "$PG_IP" -p 5432 -U awoooi -d awoooi_test \
            -f tests/integration/setup_test_schema.sql
          # 跑測試
          # B5 整合測試嚴格模式 (2026-04-13 ogt: 恢復 Break-Glass 移除)
          # -m integration: override pyproject.toml addopts "-m 'not integration'"，讓標記測試可執行
          TEST_DATABASE_URL="postgresql+asyncpg://awoooi:awoooi_test_2026@${PG_IP}:5432/awoooi_test?ssl=disable" \
            /opt/api-venv/bin/pytest tests/integration/test_b5_core_flows.py -v --tb=short -m integration
          # 清理
          docker rm -f pg-test-b5 || true

      - name: Login to Harbor
        uses: docker/login-action@v3
        with:
          registry: ${{ env.HARBOR }}
          username: ${{ secrets.HARBOR_USERNAME }}
          password: ${{ secrets.HARBOR_PASSWORD }}

      # ── API 鏡像建置（含 Layer Cache 加速）──────────────────────────────
      # 2026-04-01 ogt: CACHE_BUST=git_sha 確保 src/ 和 models.json 層每次重建
      # deps 層 (pip install) 仍可 cache → 加速；代碼/配置層強制失效
      # 首席架構師 Review C1 (2026-04-05 Claude Code): 補 DOCKER_BUILDKIT=1
      # BUILDKIT_INLINE_CACHE=1 只有在 BuildKit 啟用時才有效
      - name: Build and Push API
        env:
          DOCKER_BUILDKIT: "1"
        run: |
          docker build -f apps/api/Dockerfile \
            --build-arg BUILDKIT_INLINE_CACHE=1 \
            --cache-from ${{ env.HARBOR }}/awoooi/api:latest \
            --build-arg CACHE_BUST=${{ github.sha }} \
            -t ${{ env.HARBOR }}/awoooi/api:${{ github.sha }} \
            -t ${{ env.HARBOR }}/awoooi/api:latest \
            .
          docker push ${{ env.HARBOR }}/awoooi/api:${{ github.sha }}
          docker push ${{ env.HARBOR }}/awoooi/api:latest

      # 2026-03-31 ogt: 移除中間通知，減少訊息雜訊

      # ── Web 鏡像建置（精準快取失效）──────────────────────────────
      # 2026-03-30 ogt: NEXT_PUBLIC_* 必須用公網域名 (build-time 寫死)
      # 2026-04-01 Claude Code: CACHE_BUST=git_sha 取代 --no-cache
      # - deps 層 (pnpm install) 仍可 cache → 節省 ~2-3 min
      # - COPY . . 以下由 CACHE_BUST 強制失效 → 業務邏輯/CSRF 等變更正確進入 bundle
      # 2026-04-12 ogt: 實測 --no-cache=10m50s；CACHE_BUST=5m50s，恢復此方案
      - name: Build and Push Web
        env:
          DOCKER_BUILDKIT: "1"
        run: |
          docker build -f apps/web/Dockerfile \
            --build-arg NEXT_PUBLIC_API_URL=https://awoooi.wooo.work \
            --build-arg CACHE_BUST=${{ github.sha }} \
            --build-arg BUILDKIT_INLINE_CACHE=1 \
            --cache-from ${{ env.HARBOR }}/awoooi/web:latest \
            -t ${{ env.HARBOR }}/awoooi/web:${{ github.sha }} \
            -t ${{ env.HARBOR }}/awoooi/web:latest \
            .
          docker push ${{ env.HARBOR }}/awoooi/web:${{ github.sha }}
          docker push ${{ env.HARBOR }}/awoooi/web:latest

      # 2026-03-31 ogt: 移除中間通知

      # 2026-03-31 ogt: P0-1 Secrets 自動注入 (ADR-035 強制)
      # 2026-03-31 ogt: 加入 AI API Keys (修復 mock_fallback 問題)
      - name: Inject K8s Secrets
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TG_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
          NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          # 2026-04-01 Claude Code: Langfuse LLMOps keys (Phase 15.1 補齊 CD 注入)
          LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
          LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
          # 2026-04-02 Claude Code: Telegram 白名單 (授權簽核用)
          TG_USER_WHITELIST: ${{ secrets.OPENCLAW_TG_USER_WHITELIST }}
          # Phase O-4.1 2026-04-02: Sentry API Token (Wave A.1 ADR-037)
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          # ADR-059 2026-04-05: Gitea Webhook Secret (GITEA_ 前綴為保留字，改用 AWOOOI_ 前綴)
          GITEA_WEBHOOK_SECRET: ${{ secrets.AWOOOI_GITEA_WEBHOOK_SECRET }}
          # MCP Phase 3: ArgoCD API Token (2026-04-11 Claude Sonnet 4.6)
          ARGOCD_API_TOKEN: ${{ secrets.ARGOCD_API_TOKEN }}
        run: |
          # S1/S2: 統一命名 deploy_key，改用 ssh-keyscan（比 StrictHostKeyChecking=no 更安全）
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan 192.168.0.121 >> ~/.ssh/known_hosts 2>/dev/null
          ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 << SECRETS
          set -e
          export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

          # 注入 Telegram Secrets (ADR-035 鐵律)
          sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
            {"op":"add","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'$(echo -n "${TG_BOT_TOKEN}" | base64 -w 0)'"},
            {"op":"add","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'$(echo -n "${TG_CHAT_ID}" | base64 -w 0)'"}
          ]' || { echo "❌ Telegram Secrets patch 失敗 — ADR-035 鐵律"; exit 1; }

          # 2026-03-31 ogt: 注入 AI API Keys (修復 NVIDIA/Gemini mock_fallback)
          # 2026-04-01 Claude Code: base64 -w 0 防止長 key 換行破壞 JSON
          # NVIDIA NIM (免費 tier)
          if [ -n "${NVIDIA_API_KEY}" ] && [ "${NVIDIA_API_KEY}" != "" ]; then
            sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
              {"op":"add","path":"/data/NVIDIA_API_KEY","value":"'$(echo -n "${NVIDIA_API_KEY}" | base64 -w 0)'"}
            ]' && echo "✅ NVIDIA_API_KEY 已注入" || echo "⚠️ NVIDIA_API_KEY patch 失敗"
          else
            echo "⚠️ NVIDIA_API_KEY 未設定，跳過"
          fi

          # Gemini (備援)
          if [ -n "${GEMINI_API_KEY}" ] && [ "${GEMINI_API_KEY}" != "" ]; then
            sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
              {"op":"add","path":"/data/GEMINI_API_KEY","value":"'$(echo -n "${GEMINI_API_KEY}" | base64 -w 0)'"}
            ]' && echo "✅ GEMINI_API_KEY 已注入" || echo "⚠️ GEMINI_API_KEY patch 失敗"
          else
            echo "⚠️ GEMINI_API_KEY 未設定，跳過"
          fi

          # 2026-04-01 Claude Code: Langfuse LLMOps keys (補齊 CD 注入，之前只有手動設定)
          if [ -n "${LANGFUSE_PUBLIC_KEY}" ] && [ -n "${LANGFUSE_SECRET_KEY}" ]; then
            sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
              {"op":"add","path":"/data/LANGFUSE_PUBLIC_KEY","value":"'$(echo -n "${LANGFUSE_PUBLIC_KEY}" | base64 -w 0)'"},
              {"op":"add","path":"/data/LANGFUSE_SECRET_KEY","value":"'$(echo -n "${LANGFUSE_SECRET_KEY}" | base64 -w 0)'"}
            ]' && echo "✅ LANGFUSE keys 已注入" || echo "⚠️ LANGFUSE keys patch 失敗"
          else
            echo "⚠️ LANGFUSE_PUBLIC_KEY/SECRET_KEY 未設定，跳過 (現有 K8s secret 值維持不變)"
          fi

          # 2026-04-02 Claude Code: Telegram Whitelist (授權簽核用戶 ID)
          if [ -n "${TG_USER_WHITELIST}" ]; then
            sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
              {"op":"add","path":"/data/OPENCLAW_TG_USER_WHITELIST","value":"'$(echo -n "${TG_USER_WHITELIST}" | base64 -w 0)'"}
            ]' && echo "✅ TG_USER_WHITELIST 已注入" || echo "⚠️ TG_USER_WHITELIST patch 失敗"
          fi

          # Phase O-4.1 2026-04-02: Sentry Auth Token (Wave A.1 ADR-037)
          if [ -n "${SENTRY_AUTH_TOKEN}" ]; then
            sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
              {"op":"add","path":"/data/SENTRY_AUTH_TOKEN","value":"'$(echo -n "${SENTRY_AUTH_TOKEN}" | base64 -w 0)'"}
            ]' && echo "✅ SENTRY_AUTH_TOKEN 已注入" || echo "⚠️ SENTRY_AUTH_TOKEN patch 失敗"
          else
            echo "⚠️ SENTRY_AUTH_TOKEN 未設定，Sentry Comment API 將跳過"
          fi

          # ADR-059 2026-04-05 Claude Code: Gitea Webhook Secret
          if [ -n "${GITEA_WEBHOOK_SECRET}" ]; then
            sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
              {"op":"add","path":"/data/GITEA_WEBHOOK_SECRET","value":"'$(echo -n "${GITEA_WEBHOOK_SECRET}" | base64 -w 0)'"}
            ]' && echo "✅ GITEA_WEBHOOK_SECRET 已注入" || echo "⚠️ GITEA_WEBHOOK_SECRET patch 失敗"
          else
            echo "⚠️ GITEA_WEBHOOK_SECRET 未設定，Gitea Webhook 簽章驗證將在 prod 失效"
          fi

          # MCP Phase 3: ArgoCD API Token (2026-04-11 Claude Sonnet 4.6)
          if [ -n "${ARGOCD_API_TOKEN}" ]; then
            sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
              {"op":"add","path":"/data/ARGOCD_API_TOKEN","value":"'$(echo -n "${ARGOCD_API_TOKEN}" | base64 -w 0)'"}
            ]' && echo "✅ ARGOCD_API_TOKEN 已注入" || echo "⚠️ ARGOCD_API_TOKEN patch 失敗"
          else
            echo "⚠️ ARGOCD_API_TOKEN 未設定，ArgoCD MCP 將使用空 token"
          fi

          # 2026-04-06 Claude Code: Sprint 3 T2 — known_hosts Secret (Security Fix A1)
          # 替換 StrictHostKeyChecking=no，讓 SSH 修復路徑使用已知主機指紋
          ssh-keyscan -H 192.168.0.110 > /tmp/known_hosts_repair 2>/dev/null
          ssh-keyscan -H 192.168.0.188 >> /tmp/known_hosts_repair 2>/dev/null
          if [ -s /tmp/known_hosts_repair ]; then
            sudo kubectl create secret generic awoooi-repair-known-hosts \
              -n awoooi-prod \
              --from-file=known_hosts=/tmp/known_hosts_repair \
              --dry-run=client -o yaml | sudo kubectl apply -f - \
              && echo "✅ awoooi-repair-known-hosts Secret 已建立/更新" \
              || echo "⚠️ awoooi-repair-known-hosts Secret 建立失敗 (非致命)"
            rm -f /tmp/known_hosts_repair
          else
            echo "⚠️ ssh-keyscan 掃描失敗，跳過 known_hosts Secret"
          fi

          echo "✅ 所有 Secrets 注入完成"
          SECRETS

      # 2026-04-11 Claude Sonnet 4.6 (Sprint B-3 ADR-069):
      # Deploy 改為 ArgoCD GitOps 模式：更新 kustomization.yaml → git push [skip ci] → ArgoCD sync
      # 舊做法 (kubectl set image) 與 ArgoCD selfHeal 衝突 — ArgoCD 會 revert 任何直接 kubectl 操作
      # 新做法流程：
      #   1. 更新 kustomization.yaml image tag（用 kustomize edit set image）
      #   2. Apply ConfigMap/ServiceRegistry（不含 Deployment，由 ArgoCD 管）
      #   3. git commit [skip ci] + push → 觸發 ArgoCD automated sync
      #   4. 等待 ArgoCD sync + rollout 完成
      #   5. Health Check
      - name: Deploy to K8s (ArgoCD GitOps)
        env:
          SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          GITEA_TOKEN: ${{ secrets.CD_PUSH_TOKEN }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          ssh-keyscan 192.168.0.121 >> ~/.ssh/known_hosts 2>/dev/null

          IMAGE_TAG="${{ github.sha }}"
          HARBOR=192.168.0.110:5000

          # ─── Step 1: Apply ConfigMap + ServiceRegistry (ArgoCD 管的是 Deployment，ConfigMap 仍直接 apply) ───
          cat k8s/awoooi-prod/04-configmap.yaml | \
            ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 \
            "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -"
          echo "✅ ConfigMap 已更新"

          cat k8s/awoooi-prod/15-service-registry-configmap.yaml | \
            ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 \
            "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -"
          echo "✅ Service Registry ConfigMap 已更新"

          # ─── Step 2: 更新 kustomization.yaml image tag ───
          # 安裝 kustomize（若未安裝）
          if ! command -v kustomize &>/dev/null; then
            curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.3.0/kustomize_v5.3.0_linux_amd64.tar.gz | tar xz -C /usr/local/bin
          fi

          cd k8s/awoooi-prod
          # kustomize edit set image 更新 tag
          kustomize edit set image \
            192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER=${HARBOR}/awoooi/api:${IMAGE_TAG}
          kustomize edit set image \
            192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER=${HARBOR}/awoooi/web:${IMAGE_TAG}
          cd ../..

          # ─── Step 3: git commit [skip ci] + push → 觸發 ArgoCD sync ───
          git config user.email "cd@awoooi.internal"
          git config user.name "AWOOOI CD"
          git add k8s/awoooi-prod/kustomization.yaml
          git diff --cached --quiet && echo "⚡ kustomization.yaml 無變化，跳過 push" || {
            git commit -m "chore(cd): deploy ${IMAGE_TAG::7} [skip ci]"
            # 用 token 推送（避免 SSH key 需要額外設定 push 權限）
            git remote remove gitea 2>/dev/null || true
            git remote add gitea http://wooo:${GITEA_TOKEN}@192.168.0.110:3001/wooo/awoooi.git
            # 先 rebase 避免 non-fast-forward (其他 commit 在 CI 期間已推入)
            # 2026-04-17 ogt: -X theirs — kustomization.yaml 衝突時採用當次部署的 image tag
            git fetch gitea main
            git rebase -X theirs gitea/main
            git push gitea main
            echo "✅ kustomization.yaml 已 push，等待 ArgoCD sync..."
          }

          # ─── Step 4: 等待 ArgoCD sync + rollout ───
          ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 << 'ARGOCD_WAIT'
          set -e
          export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

          # 等待 ArgoCD Application Synced（最多 120s）
          echo "⏳ 等待 ArgoCD sync..."
          for i in $(seq 1 24); do
            SYNC=$(sudo kubectl get application awoooi-prod -n argocd \
              -o jsonpath='{.status.sync.status}' 2>/dev/null || echo "Unknown")
            HEALTH=$(sudo kubectl get application awoooi-prod -n argocd \
              -o jsonpath='{.status.health.status}' 2>/dev/null || echo "Unknown")
            echo "  ArgoCD: sync=$SYNC health=$HEALTH"
            if [ "$SYNC" = "Synced" ] && [ "$HEALTH" = "Healthy" ]; then
              echo "✅ ArgoCD Synced + Healthy"
              break
            fi
            sleep 5
          done

          # 確認 rollout 完成
          sudo kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s
          sudo kubectl rollout status deployment/awoooi-web -n awoooi-prod --timeout=120s
          sudo kubectl rollout status deployment/awoooi-worker -n awoooi-prod --timeout=120s
          echo "✅ 部署完成"

          # Health Check
          HEALTH_PASS=0
          for i in 1 2 3; do
            HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null --connect-timeout 10 "http://localhost:32334/api/v1/health")
            if [ "$HTTP_CODE" = "200" ]; then
              echo "✅ API 健康檢查通過"
              HEALTH_PASS=1
              break
            fi
            echo "⏳ 嘗試 #$i: HTTP $HTTP_CODE，等待 10s..."
            sleep 10
          done
          if [ "$HEALTH_PASS" = "0" ]; then
            echo "❌ API 健康檢查失敗"
            exit 1
          fi
          ARGOCD_WAIT

      # 2026-04-09 Claude Sonnet 4.6: Sprint 5.2 — 同步 ops 腳本到 188 (ollama user)
      # DEPLOY_SSH_KEY_188 = gitea-cd-deploy-188 (ed25519，只有 188 authorized_keys)
      # 腳本: docker-health-monitor.sh + pg-backup.sh (感知層 + 備份)
      - name: Sync Ops Scripts to 188
        continue-on-error: true
        env:
          SSH_KEY_188: ${{ secrets.DEPLOY_SSH_KEY_188 }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY_188" > ~/.ssh/deploy_key_188
          chmod 600 ~/.ssh/deploy_key_188
          ssh-keyscan 192.168.0.188 >> ~/.ssh/known_hosts 2>/dev/null

          # 同步 docker-health-monitor.sh
          scp -i ~/.ssh/deploy_key_188 \
            scripts/ops/docker-health-monitor.sh \
            ollama@192.168.0.188:~/awoooi-ops/docker-health-monitor.sh \
            && echo "✅ docker-health-monitor.sh 已同步" \
            || echo "⚠️ docker-health-monitor.sh 同步失敗"

          # 同步 pg-backup.sh
          scp -i ~/.ssh/deploy_key_188 \
            scripts/ops/pg-backup.sh \
            ollama@192.168.0.188:~/awoooi-ops/pg-backup.sh \
            && echo "✅ pg-backup.sh 已同步" \
            || echo "⚠️ pg-backup.sh 同步失敗"

          # 確保執行權限
          ssh -i ~/.ssh/deploy_key_188 ollama@192.168.0.188 \
            "chmod +x ~/awoooi-ops/docker-health-monitor.sh ~/awoooi-ops/pg-backup.sh && echo '✅ 權限設定完成'" \
            || echo "⚠️ 權限設定失敗"

      # Phase O-4.5 2026-04-02: Alert Chain Smoke Test (Wave A.6 + B.2 ADR-037)
      # 驗證告警鏈路 E2E: API Health + Webhook + OTEL + Event Exporter
      # 2026-04-05 Claude Code cache優化: 使用 /opt/api-venv (已有 requests)，移除 Setup Python Tools step
      # 2026-04-10 ogt: 移除 continue-on-error — 告警鏈路失敗必須阻塞部署
      - name: Alert Chain Smoke Test
        id: alert_chain_smoke
        run: |
          # 2026-04-05 Claude Code: 使用真實 API 地址（192.168.0.121:32334 NodePort）
          # CI job container 的 localhost 不等於 K3s 節點，必須用內網 IP
          # 首席架構師 Review C2: 修正永遠 pass — || true 移除，結果正確寫入 GITHUB_OUTPUT
          source /opt/api-venv/bin/activate
          python3 scripts/alert_chain_smoke_test.py \
            --api-url http://192.168.0.121:32334 \
            --json | tee /tmp/alert_chain_result.json \
            && echo "alert_chain_status=pass" >> $GITHUB_OUTPUT \
            || echo "alert_chain_status=fail" >> $GITHUB_OUTPUT

      # Phase O-5 Wave C.2 2026-04-02 ogt: 監控覆蓋率驗證 (generate_monitoring.py --check)
      # 2026-04-10 ogt: 移除 continue-on-error — 覆蓋率不足必須阻塞部署
      - name: Monitoring Coverage Check
        id: monitoring_coverage
        run: |
          source /opt/api-venv/bin/activate
          python3 scripts/generate_monitoring.py --check && echo "coverage_status=pass" >> $GITHUB_OUTPUT || echo "coverage_status=fail" >> $GITHUB_OUTPUT

      # [首席架構師] 新增 Playwright E2E Smoke Test 步驟 v1.0.0 2026-04-01 (台北時間)
      # continue-on-error: true — smoke 失敗不阻塞部署，但結果會反映在 TG 通知
      - name: E2E Smoke Test
        id: smoke
        continue-on-error: true
        run: |
          # 首席架構師 Review I4 + 2026-04-05 Claude Code cache優化:
          # playwright.config.ts import @playwright/test — 必須先安裝 pnpm node_modules
          # pnpm store 持久化到 /opt/pnpm-store，pnpm-lock.yaml hash 未變則 --prefer-offline
          PNPM_STORE=/opt/pnpm-store
          PNPM_HASH_FILE=/opt/pnpm-store/.lock_hash
          CURRENT_PNPM_HASH=$(md5sum pnpm-lock.yaml | awk '{print $1}')

          corepack enable 2>/dev/null || npm install -g pnpm@9 -q
          pnpm config set store-dir $PNPM_STORE

          if [ "$(cat $PNPM_HASH_FILE 2>/dev/null)" != "$CURRENT_PNPM_HASH" ]; then
            echo "📦 pnpm lock 已變更，重裝 node_modules..."
            pnpm install --frozen-lockfile 2>&1 | tail -5
            echo "$CURRENT_PNPM_HASH" > $PNPM_HASH_FILE
          else
            echo "⚡ 使用快取 pnpm store (lock 未變更)，prefer-offline..."
            pnpm install --frozen-lockfile --prefer-offline 2>&1 | tail -5
          fi

          cd apps/web
          # Playwright Chromium 持久化到 /opt/playwright-browsers，版本 hash guard
          export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
          PLAYWRIGHT_VER=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'] || '')" 2>/dev/null || echo "unknown")
          PLAYWRIGHT_HASH_FILE=/opt/playwright-browsers/.version_hash
          if [ "$(cat $PLAYWRIGHT_HASH_FILE 2>/dev/null)" != "$PLAYWRIGHT_VER" ]; then
            echo "📦 Playwright 版本變更 ($PLAYWRIGHT_VER)，重裝 Chromium..."
            npx playwright install chromium --with-deps 2>&1 | tail -5
            echo "$PLAYWRIGHT_VER" > $PLAYWRIGHT_HASH_FILE
          else
            echo "⚡ 使用快取 Playwright Chromium ($PLAYWRIGHT_VER)"
          fi
          # 對已部署的生產環境跑 smoke test
          npx playwright test tests/e2e/smoke.spec.ts --reporter=line \
            && echo "smoke_status=pass" >> $GITHUB_OUTPUT \
            || echo "smoke_status=fail" >> $GITHUB_OUTPUT
        env:
          CI: "true"
          # 直接測試已部署的生產環境，不啟動本地 dev server
          PLAYWRIGHT_BASE_URL: "https://awoooi.wooo.work"

      - name: Notify Health Check Success
        env:
          SMOKE_RESULT: ${{ steps.smoke.outcome == 'success' && '✅' || '⚠️' }}
          ALERT_CHAIN_RESULT: ${{ steps.alert_chain_smoke.outcome == 'success' && '✅' || '⚠️' }}
          MONITORING_RESULT: ${{ steps.monitoring_coverage.outcome == 'success' && '✅' || '⚠️' }}
        run: |
          END_TIME=$(date +%s)
          DURATION=$((END_TIME - ${{ steps.commit.outputs.start_time }}))
          MINUTES=$((DURATION / 60))
          SECONDS=$((DURATION % 60))
          # 2026-04-05 ogt: TG_MSG 必須在 shell 中組裝，才能展開 ${MINUTES}/${SECONDS} 等 shell 變數
          # 2026-04-05 ogt: 移除 parse_mode=HTML，避免 commit message 含特殊字元導致 400
          COMMIT_MSG="${{ steps.commit.outputs.message }}"
          SHORT_SHA="${{ steps.commit.outputs.short_sha }}"
          TG_MSG="✅ AWOOOI 部署完成\n├ 📝 ${COMMIT_MSG}\n├ 🔖 ${SHORT_SHA}\n├ ⏱️ 耗時: ${MINUTES}m ${SECONDS}s\n├ 📦 API: ✅ Web: ✅\n├ 🩺 Health: ✅\n├ 🔗 Alert Chain: ${ALERT_CHAIN_RESULT}\n├ 📊 Monitoring: ${MONITORING_RESULT}\n└ 🎭 Smoke: ${SMOKE_RESULT}"
          printf '%b' "$TG_MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
            -d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
            --data-urlencode "text@-" || echo "TG notify warning (non-fatal)"

      - name: Notify Pipeline Failure
        # 2026-04-16 ogt + Claude Sonnet 4.6: 改用 HTML 結構化格式
        if: failure()
        run: |
          COMMIT_MSG="${{ steps.commit.outputs.message }}"
          SHORT_SHA="${{ steps.commit.outputs.short_sha }}"
          ACTOR="${{ github.actor }}"
          COMMIT_ESC=$(echo "$COMMIT_MSG" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
          MSG=$(printf '❌ <b>AWOOOI 部署失敗</b>\n├ 📝 <code>%s</code>\n├ 🔖 <code>%s</code>\n├ 👤 %s\n└ 🔗 http://192.168.0.110:3001/wooo/awoooi/actions' "${COMMIT_ESC}" "${SHORT_SHA}" "${ACTOR}")
          curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
            -H "Content-Type: application/json" \
            -d "$(jq -n --arg c "${{ secrets.TELEGRAM_CHAT_ID }}" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')"
