feat(phase8): CI/CD Pipeline 與 K8s 部署自動化

Phase 8 CI/CD 藍圖:
- GitHub Actions deploy-prod.yml (沿用 AIOPS 成熟模式)
- Signal Worker K8s Deployment
- Telegram Notify 閉環
- Bootstrap 自動化腳本

架構鐵律:
- Build: 110 金庫 (Harbor + Self-Hosted Runner)
- Deploy: 120 K3s Master
- 嚴禁 Docker Compose,K8s 唯一合法部署

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-22 18:01:01 +08:00
parent ccdf757edd
commit f037812f15
11 changed files with 1236 additions and 0 deletions

300
.github/workflows/deploy-prod.yml vendored Normal file
View File

@@ -0,0 +1,300 @@
# =============================================================================
# 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 ComposeK8s 唯一合法部署
# - 110 金庫 Build + Push
# - 120 K3s Deploy
# =============================================================================
name: Deploy to Production
on:
push:
branches:
- main
paths:
- 'apps/api/**'
- 'apps/web/**'
- 'k8s/awoooi-prod/**'
- '.github/workflows/deploy-prod.yml'
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
env:
# Harbor 金庫 (110 主機)
REGISTRY: harbor.wooo.work
HARBOR_PROJECT: awoooi
# 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"
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: Verify Docker Auth
run: |
if docker pull ${{ env.REGISTRY }}/library/hello-world:latest 2>/dev/null || true; then
echo "✅ Harbor 認證有效"
else
echo "⚠️ Harbor 認證可能需要更新"
fi
# ----- Build API Image -----
- 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 \
apps/api
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/api \
-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
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..."
kubectl apply -f k8s/awoooi-prod/ --namespace=${{ env.K8S_NAMESPACE }}
- 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
kubectl rollout restart deployment/awoooi-worker -n ${{ env.K8S_NAMESPACE }} || true
- 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
kubectl rollout status deployment/awoooi-worker -n ${{ env.K8S_NAMESPACE }} --timeout=180s || true
- 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: ${{ !inputs.skip_tests }}
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: always()
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
env:
TELEGRAM_TOKEN: ${{ secrets.OPENCLAW_TG_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.OPENCLAW_TG_CHAT_ID }}
GH_SHA: ${{ github.sha }}
GH_SERVER: ${{ github.server_url }}
GH_REPO: ${{ github.repository }}
GH_ACTOR: ${{ github.actor }}
GH_RUN_ID: ${{ github.run_id }}
run: |-
EMOJI="${{ steps.status.outputs.emoji }}"
MSG="${{ steps.status.outputs.message }}"
TAG="${{ needs.build.outputs.image_tag }}"
COMMIT_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 || echo "N/A")
TEXT=$(printf "%s *AWOOOI %s*\n\nTag: %s\nCommit: %s\nMsg: %s\nActor: %s\nEnv: Production (K3s 120)\n\nDetails: %s/%s/actions/runs/%s" \
"$EMOJI" "$MSG" "$TAG" "${GH_SHA:0:7}" "$COMMIT_MSG" "$GH_ACTOR" "$GH_SERVER" "$GH_REPO" "$GH_RUN_ID")
if [ -n "$TELEGRAM_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
jq -n --arg text "$TEXT" --arg chat "$TELEGRAM_CHAT_ID" '{chat_id: $chat, text: $text, disable_web_page_preview: true}' | \
curl -sf -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" -H "Content-Type: application/json" -d @- && echo "Telegram sent" || echo "Telegram failed"
else
echo "Telegram secrets not configured"
fi
- 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 發送失敗 (非阻塞)"