# ============================================================================= # AWOOOI CI Pipeline v2.0 (沿用 AIOPS 最佳實踐) # ============================================================================= name: CI on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: NODE_VERSION: '20' PNPM_VERSION: '9' PYTHON_VERSION: '3.11' # OTEL CI/CD 監控 (2026-03-24 批准, 2026-03-28 修正: SignOz 在 188) OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.0.188:24318 OTEL_SERVICE_NAME: awoooi-ci OTEL_RESOURCE_ATTRIBUTES: service.version=${{ github.sha }},deployment.environment=ci jobs: # ==================== Pre-flight (10s Fail-Fast) ==================== pre-flight: name: "Pre-flight" runs-on: [self-hosted, harbor, k8s] timeout-minutes: 1 steps: # 2026-03-29 Claude Code: 修復 _diag/pages 檔案衝突 - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - name: Quick sanity check run: | echo "✅ Runner 可用" node --version || echo "⚠️ Node not found" python3 --version || echo "⚠️ Python not found" # ==================== Lint & Type Check ==================== lint: name: Lint & Type Check runs-on: [self-hosted, harbor, k8s] needs: pre-flight timeout-minutes: 10 steps: # 2026-03-29 Claude Code: 修復 _diag/pages 檔案衝突 - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 with: clean: true # 清理 worktrees (在 checkout 後,確保在 workspace 內) - name: Clean worktrees run: rm -rf ${{ github.workspace }}/.claude/worktrees 2>/dev/null || true - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Lint run: pnpm lint - name: Type check run: pnpm typecheck # Phase 14.2: 依賴治理 (2026-03-26) - name: Dependency Check run: pnpm dep-check - name: ADR Compliance Check run: | echo "🔍 檢查 ADR 規定..." # 檢查 1: 前端禁止直連資料庫 (ADR-005) if grep -rE "psycopg2|asyncpg|redis|sqlalchemy|pg|ioredis" apps/web/src/ 2>/dev/null; then echo "❌ ADR-005 違規: 前端禁止直連資料庫" exit 1 fi # 檢查 2: 禁止 Redux (ADR-004) if grep -rE "@reduxjs/toolkit|react-redux" apps/web/package.json 2>/dev/null; then echo "❌ ADR-004 違規: 禁止 Redux" exit 1 fi # 檢查 3: 禁止 import 舊專案 if grep -rE "from ['\"].*wooo-aiops" apps/ packages/ 2>/dev/null; then echo "❌ 禁止 import 舊專案" exit 1 fi echo "✅ ADR 檢查通過" # Phase 14.2 #96: 後端分層檢查 (輕量版 import-linter) - name: API Layer Check run: | echo "🔍 檢查後端分層規則..." # 檢查 1: services 層禁止引用 api/routers 層 (反向依賴) if grep -rE "from src\.(api|routers)" apps/api/src/services/ 2>/dev/null; then echo "❌ 分層違規: services 禁止引用 api/routers 層" exit 1 fi # 檢查 2: repositories 層禁止引用 services 層 if grep -rE "from src\.services" apps/api/src/repositories/ 2>/dev/null; then echo "❌ 分層違規: repositories 禁止引用 services 層" exit 1 fi # 檢查 3: models 層禁止引用業務層 if grep -rE "from src\.(services|api|routers)" apps/api/src/models/ 2>/dev/null; then echo "❌ 分層違規: models 禁止引用業務層" exit 1 fi echo "✅ 後端分層檢查通過" # ==================== Test ==================== test: name: Test runs-on: [self-hosted, harbor, k8s] needs: lint timeout-minutes: 15 steps: - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run tests run: pnpm test continue-on-error: true - name: Upload coverage uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false # ==================== Build ==================== build: name: Build runs-on: [self-hosted, harbor, k8s] needs: lint timeout-minutes: 15 steps: - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Setup Turborepo Cache uses: dtinth/setup-github-actions-caching-for-turbo@v1 - name: Build packages env: NEXT_PUBLIC_API_URL: https://awoooi.wooo.work # Sentry DSN (透過 /api/sentry-tunnel 避免區域網路權限問題) NEXT_PUBLIC_SENTRY_DSN: http://da02d4e5d6542e4d1ed6b2dd6542efeb@192.168.0.110:9000/2 run: pnpm turbo build - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts path: | apps/*/dist packages/*/dist retention-days: 7 # ==================== API Lint (Python) ==================== api-lint: name: API Lint runs-on: [self-hosted, harbor, k8s] needs: pre-flight timeout-minutes: 5 steps: - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v3 - name: Install & Lint working-directory: apps/api run: | uv sync uv run ruff check . - name: Type check working-directory: apps/api run: uv run mypy src/ --exclude 'tests/|scripts/' || true continue-on-error: true # ==================== API Test ==================== api-test: name: API Test runs-on: [self-hosted, harbor, k8s] needs: api-lint timeout-minutes: 10 steps: - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v3 - name: Install & Test working-directory: apps/api env: PYTHONPATH: ${{ github.workspace }}/apps/api run: | uv sync uv run pytest tests/ --cov=src --cov-report=xml -v || true continue-on-error: true # ==================== Ollama Model Test (Phase 12.3 #67) ==================== # 🤖 自動化模型回歸測試 - 確保 OpenClaw 提案品質 ollama-test: name: Ollama Model Test runs-on: [self-hosted, harbor, k8s] needs: api-lint timeout-minutes: 5 continue-on-error: true # 不阻塞主 Pipeline steps: - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 - name: Test Ollama Connectivity run: | echo "🔗 測試 Ollama 連線..." if curl -s --connect-timeout 5 http://192.168.0.188:11434/api/tags > /dev/null; then echo "✅ Ollama 可達" # 使用 Python 替代 jq (Runner 未安裝 jq) curl -s http://192.168.0.188:11434/api/tags | python3 -c "import sys,json; [print(m['name']) for m in json.load(sys.stdin).get('models',[])]" else echo "⚠️ Ollama 無法連線 (192.168.0.188:11434)" exit 0 # 不失敗,只警告 fi - name: Model Smoke Test run: | echo "🧪 模型冒煙測試..." # 使用 Python 替代 jq RESPONSE=$(curl -s --max-time 60 http://192.168.0.188:11434/api/generate -d '{ "model": "qwen2.5:7b-instruct", "prompt": "你是 AIOps 助手。回答:1+1=?", "stream": false }' | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('response','ERROR'))" 2>/dev/null || echo "ERROR") if [ "$RESPONSE" != "ERROR" ] && [ -n "$RESPONSE" ]; then echo "✅ 模型回應正常" echo "回應: $RESPONSE" else echo "⚠️ 模型回應異常" fi - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v3 - name: Action Parsing Test working-directory: apps/api env: PYTHONPATH: ${{ github.workspace }}/apps/api run: | echo "🔍 Action Parsing 回歸測試..." uv sync uv run pytest tests/test_action_parsing.py -v --tb=short || echo "⚠️ 部分測試失敗" # 🔴 LLM 測試移至 Nightly (2026-03-26) # 原因: CPU 推理需 300+ 秒/測試,不適合每次 commit # 完整測試: .github/workflows/nightly-llm.yaml - name: LLM Test (Skip - See Nightly) run: | echo "⏭️ LLM 測試已移至 Nightly workflow" echo " - test_model_regression.py" echo " - test_prompt_validation.py" echo " 原因: CPU 推理 ~300s/測試" # ==================== OpenAPI Validation ==================== openapi-validate: name: OpenAPI Validate runs-on: [self-hosted, harbor, k8s] needs: pre-flight timeout-minutes: 3 steps: - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Validate run: | # 修復 ENOTEMPTY npm 錯誤 - 先清理舊目錄 rm -rf $(npm root -g)/@stoplight/spectral-cli 2>/dev/null || true npm install -g @stoplight/spectral-cli || npm install -g @stoplight/spectral-cli spectral lint docs/api/api-contract.yaml || true # ==================== Docker Build Verify ==================== # 🚀 優化 (2026-03-24): 只在 PR 時執行,main push 跳過 (CD 會構建) docker-build: name: Docker Verify runs-on: [self-hosted, harbor, k8s] needs: [test, api-test, build] if: github.event_name == 'pull_request' timeout-minutes: 20 strategy: matrix: app: [web, api] steps: - name: "Clean Runner Diagnostics" run: | RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")") rm -rf "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build (no push) uses: docker/build-push-action@v5 with: context: . file: apps/${{ matrix.app }}/Dockerfile push: false tags: awoooi-${{ matrix.app }}:test build-args: | NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PUBLIC_SENTRY_DSN=http://da02d4e5d6542e4d1ed6b2dd6542efeb@192.168.0.110:9000/2 cache-from: type=gha cache-to: type=gha,mode=max