根本原因: Notify 步驟中的 text= 參數包含真實換行符, Gitea YAML 解析器在 line 51 報錯「could not find expected ':'」, 導致 cd.yaml 無法被解析,整個 CD 管道失效超過 10+ 次 push。 修復: 換行符改用 URL encode %0A,符合 Telegram Bot API 格式。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
224 lines
10 KiB
YAML
224 lines
10 KiB
YAML
# =============================================================================
|
||
# 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]
|
||
workflow_dispatch:
|
||
|
||
# 2026-03-30 ogt: 佇列模式 - 等待前一個 run 完成,不取消
|
||
concurrency:
|
||
group: cd-deploy-${{ github.ref }}
|
||
cancel-in-progress: false
|
||
|
||
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:
|
||
runs-on: ubuntu-latest
|
||
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
|
||
run: |
|
||
curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||
-d parse_mode="HTML" \
|
||
-d "text=🚀 <b>AWOOOI 部署開始</b>%0A├ 📝 ${{ steps.commit.outputs.message }}%0A├ 🔖 <code>${{ steps.commit.outputs.short_sha }}</code>%0A├ 👤 ${{ github.actor }}%0A└ 🌿 main"
|
||
|
||
|
||
|
||
# 2026-03-31 ogt: Phase 22.0 CI 測試 (禁止 Mock - feedback_no_mock_testing.md)
|
||
- name: Run API Tests
|
||
run: |
|
||
cd apps/api
|
||
pip install -q uv
|
||
uv pip install --system -e ".[dev]" -q
|
||
pytest tests/ -v --tb=short --timeout=60 2>&1 | tail -50
|
||
echo "✅ API 測試通過"
|
||
|
||
- name: Login to Harbor
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: ${{ env.HARBOR }}
|
||
username: ${{ secrets.HARBOR_USERNAME }}
|
||
password: ${{ secrets.HARBOR_PASSWORD }}
|
||
|
||
# ── API 鏡像建置(含 Layer Cache 加速)──────────────────────────────
|
||
- name: Build and Push API
|
||
run: |
|
||
docker build -f apps/api/Dockerfile \
|
||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||
--cache-from ${{ env.HARBOR }}/awoooi/api:latest \
|
||
-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 寫死)
|
||
# 內網 IP 會觸發瀏覽器「存取區域網路」權限對話框
|
||
# 2026-04-01 Claude Code: 移除 --cache-from,加 --no-cache
|
||
# 原因: BuildKit inline cache 導致 COPY . . 層被重用,CSRF fix 未進入 bundle
|
||
- name: Build and Push Web
|
||
run: |
|
||
docker build -f apps/web/Dockerfile \
|
||
--build-arg NEXT_PUBLIC_API_URL=https://awoooi.wooo.work \
|
||
--no-cache \
|
||
-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 }}
|
||
run: |
|
||
mkdir -p ~/.ssh
|
||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
|
||
chmod 600 ~/.ssh/deploy_key
|
||
ssh -o StrictHostKeyChecking=no -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":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'$(echo -n "${TG_BOT_TOKEN}" | base64)'"},
|
||
{"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'$(echo -n "${TG_CHAT_ID}" | base64)'"}
|
||
]' || echo "⚠️ Telegram Secrets patch 跳過"
|
||
|
||
# 2026-03-31 ogt: 注入 AI API Keys (修復 NVIDIA/Gemini mock_fallback)
|
||
# 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":"replace","path":"/data/NVIDIA_API_KEY","value":"'$(echo -n "${NVIDIA_API_KEY}" | base64)'"}
|
||
]' && 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":"replace","path":"/data/GEMINI_API_KEY","value":"'$(echo -n "${GEMINI_API_KEY}" | base64)'"}
|
||
]' && echo "✅ GEMINI_API_KEY 已注入" || echo "⚠️ GEMINI_API_KEY patch 失敗"
|
||
else
|
||
echo "⚠️ GEMINI_API_KEY 未設定,跳過"
|
||
fi
|
||
|
||
echo "✅ 所有 Secrets 注入完成"
|
||
SECRETS
|
||
|
||
# 2026-03-31 ogt: Phase 22 修復 - CD 必須 apply ConfigMap (之前只 set image)
|
||
- name: Apply K8s ConfigMap
|
||
env:
|
||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||
run: |
|
||
cat k8s/awoooi-prod/04-configmap.yaml | \
|
||
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.121 \
|
||
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -"
|
||
echo "✅ ConfigMap 已更新"
|
||
|
||
- name: Deploy to K8s
|
||
env:
|
||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||
run: |
|
||
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.121 << 'DEPLOY'
|
||
set -e
|
||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||
|
||
# 2026-03-30 ogt: sudoers NOPASSWD 已設定,無需密碼
|
||
sudo kubectl set image deployment/awoooi-api \
|
||
api=192.168.0.110:5000/awoooi/api:${{ github.sha }} \
|
||
-n awoooi-prod
|
||
|
||
sudo kubectl set image deployment/awoooi-web \
|
||
web=192.168.0.110:5000/awoooi/web:${{ github.sha }} \
|
||
-n awoooi-prod
|
||
|
||
sudo kubectl set image deployment/awoooi-worker \
|
||
worker=192.168.0.110:5000/awoooi/api:${{ github.sha }} \
|
||
-n awoooi-prod
|
||
|
||
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 "✅ 部署完成"
|
||
DEPLOY
|
||
|
||
# 2026-03-31 ogt: 移除中間通知
|
||
|
||
# ── Health Check ─────────────────────────────────────────────────────
|
||
- name: Health Check
|
||
env:
|
||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||
run: |
|
||
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.121 << 'CHECK'
|
||
sleep 10
|
||
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 健康檢查通過"
|
||
exit 0
|
||
fi
|
||
echo "⏳ 嘗試 #$i: HTTP $HTTP_CODE,等待 10s..."
|
||
sleep 10
|
||
done
|
||
echo "❌ API 健康檢查失敗"
|
||
exit 1
|
||
CHECK
|
||
|
||
- name: Notify Health Check Success
|
||
run: |
|
||
END_TIME=$(date +%s)
|
||
DURATION=$((END_TIME - ${{ steps.commit.outputs.start_time }}))
|
||
MINUTES=$((DURATION / 60))
|
||
SECONDS=$((DURATION % 60))
|
||
curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||
-d parse_mode="HTML" \
|
||
-d "text=✅ <b>AWOOOI 部署完成</b>%0A├ 📝 ${{ steps.commit.outputs.message }}%0A├ 🔖 <code>${{ steps.commit.outputs.short_sha }}</code>%0A├ ⏱️ 耗時: ${MINUTES}m ${SECONDS}s%0A├ 📦 API: ✅ Web: ✅%0A└ 🩺 Health: ✅"
|
||
|
||
- name: Notify Pipeline Failure
|
||
if: failure()
|
||
run: |
|
||
curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||
-d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||
-d parse_mode="HTML" \
|
||
-d "text=❌ <b>AWOOOI 部署失敗</b>%0A├ 📝 ${{ steps.commit.outputs.message }}%0A├ 🔖 <code>${{ steps.commit.outputs.short_sha }}</code>%0A├ 👤 ${{ github.actor }}%0A└ 🔗 <a href=\"http://192.168.0.110:3001/wooo/awoooi/actions\">查看日誌</a>"
|