Files
awoooi/.github/workflows/deploy-prod.yml
2026-03-22 18:37:41 +08:00

301 lines
11 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 - 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 發送失敗 (非阻塞)"