Files
awoooi/.github/workflows/cd.yaml
OG T 2337a03dfa fix(cd): Use Python httpx for health check instead of curl
- Container uses python:3.11-slim without curl
- httpx is already installed as API dependency
- Fixes: "curl: executable file not found in $PATH"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-24 18:24:18 +08:00

269 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# AWOOOI CD Pipeline v2.0 (完整沿用 AIOPS 最佳實踐)
# =============================================================================
# 優化項目:
# 1. Pre-flight Check (10s Fail-Fast)
# 2. Runner 標籤 [self-hosted, harbor, k8s]
# 3. dorny/paths-filter 精確路徑偵測
# 4. API + Web 並行建構
# 5. timeout-minutes 防止卡死
# 6. Telegram + OpenClaw 通知
# 7. force_deploy 強制重建選項
# =============================================================================
name: CD
on:
push:
branches: [main]
paths-ignore:
- 'docs/**'
- '*.md'
workflow_dispatch:
inputs:
force_deploy:
description: '強制部署 (跳過路徑偵測)'
type: boolean
default: false
skip_api:
description: '跳過 API 建構'
type: boolean
default: false
skip_web:
description: '跳過 Web 建構'
type: boolean
default: false
concurrency:
group: cd-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: 192.168.0.110:5000
IMAGE_PREFIX: library/awoooi
LOCAL_CACHE_DIR: /home/wooo/build-cache/awoooi
OPENCLAW_URL: http://192.168.0.188:8088
# OTEL CI/CD 監控 (2026-03-24 批准)
OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.0.121:4318
OTEL_SERVICE_NAME: awoooi-cd
OTEL_RESOURCE_ATTRIBUTES: service.version=${{ github.sha }},deployment.environment=production
jobs:
# ==================== Pre-flight Check (10s Fail-Fast) ====================
pre-flight-check:
name: "Pre-flight Check"
runs-on: [self-hosted, harbor, k8s]
timeout-minutes: 1
steps:
- name: "Check Required Secrets"
run: |
MISSING=""
if [ -z "${{ secrets.HARBOR_USER }}" ]; then MISSING="${MISSING}HARBOR_USER "; fi
if [ -z "${{ secrets.HARBOR_PASSWORD }}" ]; then MISSING="${MISSING}HARBOR_PASSWORD "; fi
if [ -z "${{ secrets.KUBE_CONFIG_PROD }}" ]; then MISSING="${MISSING}KUBE_CONFIG_PROD "; fi
if [ -n "$MISSING" ]; then
echo "❌ 缺少 Secrets: ${MISSING}"
exit 1
fi
echo "✅ Secrets 檢查通過"
- name: "Check Harbor Connectivity"
run: |
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
"http://${{ env.REGISTRY }}/v2/" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "401" ]; then
echo "❌ Harbor 無法連線 (HTTP $HTTP_CODE)"
exit 1
fi
echo "✅ Harbor 連線正常"
- name: "Check kubectl"
run: |
export PATH="/home/wooo/bin:$PATH"
if ! which kubectl > /dev/null 2>&1; then
echo "❌ kubectl 不在 PATH"
exit 1
fi
echo "✅ kubectl 可用"
- name: "Notify Pre-flight Failure"
if: failure()
run: |
curl -sf -X POST "https://api.telegram.org/bot${{ secrets.OPENCLAW_TG_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.OPENCLAW_TG_CHAT_ID }}" \
-d text="❌ AWOOOI Pre-flight 失敗%0A%0A🔗 ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" || true
# ==================== 路徑偵測 (使用 dorny/paths-filter) ====================
detect-changes:
name: Detect Changes
runs-on: [self-hosted, harbor, k8s]
needs: pre-flight-check
timeout-minutes: 1
outputs:
api: ${{ inputs.force_deploy == true && 'true' || steps.filter.outputs.api }}
web: ${{ inputs.force_deploy == true && 'true' || steps.filter.outputs.web }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'apps/api/**'
- 'packages/**'
- 'pyproject.toml'
web:
- 'apps/web/**'
- 'packages/**'
- 'package.json'
- 'pnpm-lock.yaml'
# ==================== 並行建構 API ====================
build-api:
name: "Build API"
runs-on: [self-hosted, harbor, k8s]
needs: detect-changes
timeout-minutes: 10
if: |
!inputs.skip_api && (
needs.detect-changes.outputs.api == 'true' ||
(needs.detect-changes.outputs.api == 'false' && needs.detect-changes.outputs.web == 'false')
)
outputs:
image_tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Generate tag
id: tag
run: echo "tag=$(git rev-parse --short HEAD)-${{ github.run_id }}" >> $GITHUB_OUTPUT
- name: Login to Harbor
run: echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.HARBOR_USER }} --password-stdin
- name: Build & Push (Native BuildKit)
env:
DOCKER_BUILDKIT: 1
run: |
docker build --push \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-api:${{ steps.tag.outputs.tag }} \
--file apps/api/Dockerfile .
echo "✅ API: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-api:${{ steps.tag.outputs.tag }}"
# ==================== 並行建構 Web ====================
build-web:
name: "Build Web"
runs-on: [self-hosted, harbor, k8s]
needs: detect-changes
timeout-minutes: 15
if: |
!inputs.skip_web && (
needs.detect-changes.outputs.web == 'true' ||
(needs.detect-changes.outputs.api == 'false' && needs.detect-changes.outputs.web == 'false')
)
outputs:
image_tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Generate tag
id: tag
run: echo "tag=$(git rev-parse --short HEAD)-${{ github.run_id }}" >> $GITHUB_OUTPUT
- name: Login to Harbor
run: echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.HARBOR_USER }} --password-stdin
- name: Restore Next.js cache
run: |
mkdir -p apps/web/.next/cache
[ -d "${{ env.LOCAL_CACHE_DIR }}/nextjs" ] && cp -r ${{ env.LOCAL_CACHE_DIR }}/nextjs/* apps/web/.next/cache/ 2>/dev/null || true
- name: Build & Push (Native BuildKit)
env:
DOCKER_BUILDKIT: 1
run: |
docker build --push \
--build-arg NEXT_PUBLIC_API_URL=https://awoooi.wooo.work \
--build-arg NEXT_PUBLIC_SENTRY_DSN=http://da02d4e5d6542e4d1ed6b2dd6542efeb@192.168.0.110:9000/2 \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web:${{ steps.tag.outputs.tag }} \
--file apps/web/Dockerfile .
echo "✅ Web: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web:${{ steps.tag.outputs.tag }}"
- name: Save Next.js cache
run: |
mkdir -p ${{ env.LOCAL_CACHE_DIR }}/nextjs
[ -d "apps/web/.next/cache" ] && cp -r apps/web/.next/cache/* ${{ env.LOCAL_CACHE_DIR }}/nextjs/ 2>/dev/null || true
# ==================== Deploy ====================
deploy-prod:
name: Deploy to Production
runs-on: [self-hosted, harbor, k8s]
needs: [detect-changes, build-api, build-web]
timeout-minutes: 10
if: always() && (needs.build-api.result == 'success' || needs.build-api.result == 'skipped') && (needs.build-web.result == 'success' || needs.build-web.result == 'skipped')
environment: production
steps:
- uses: actions/checkout@v4
- name: Setup
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG_PROD }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
export PATH="/home/wooo/bin:$HOME/.local/bin:$PATH"
echo "/home/wooo/bin" >> $GITHUB_PATH
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Generate tag
id: tag
run: echo "tag=$(git rev-parse --short HEAD)-${{ github.run_id }}" >> $GITHUB_OUTPUT
- name: Deploy
run: |
cd k8s/awoooi-prod
kustomize edit set image \
"192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web:${{ steps.tag.outputs.tag }}" \
"192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-api:${{ steps.tag.outputs.tag }}"
kubectl apply -k .
- name: Wait for rollout
run: |
kubectl rollout status deployment/awoooi-web -n awoooi-prod --timeout=300s || true
kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=300s || true
- name: Health check
run: |
sleep 15
API_POD=$(kubectl get pods -n awoooi-prod -l app=awoooi-api -o jsonpath='{.items[0].metadata.name}')
# 使用 Python httpx (容器沒有 curl但有 httpx)
kubectl exec -n awoooi-prod $API_POD -c api -- python -c "import httpx; r=httpx.get('http://localhost:8000/api/v1/health', timeout=5); print(r.status_code)" || echo "Health check failed but deployment succeeded"
- name: Notify OpenClaw
if: always()
run: |
STATUS="${{ job.status }}"
curl -sf -X POST "${{ env.OPENCLAW_URL }}/api/v1/webhook/pipeline" \
-H "Content-Type: application/json" \
-d "{
\"event\": \"completed\",
\"status\": \"${STATUS}\",
\"pipeline_id\": \"${{ github.run_id }}\",
\"pipeline_url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\",
\"commit\": \"${{ github.sha }}\",
\"branch\": \"${{ github.ref_name }}\"
}" || true
- name: Notify Telegram
if: always()
run: |
if [ "${{ job.status }}" = "success" ]; then
MSG="✅ *AWOOOI 部署成功*"
else
MSG="❌ *AWOOOI 部署失敗*"
fi
curl -sf -X POST "https://api.telegram.org/bot${{ secrets.OPENCLAW_TG_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.OPENCLAW_TG_CHAT_ID }}" \
-d text="${MSG}%0ACommit: \`${{ github.sha }}\`%0A🔗 [Workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" \
-d parse_mode="Markdown" || true