Files
awoooi/.github/workflows/deploy-prod.yml
2026-05-12 16:22:16 +08:00

327 lines
13 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
# 2026-05-12 Codex: GitHub 是唯讀備份production deploy 只能從 Gitea 進入。
# 這份歷史 workflow 仍含 Harbor build/push 與 kubectl apply/rollout會和 Gitea CD 競爭。
# 保留檔案供稽核,但停用所有 job。
on:
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
# 沿用 AIOPS 設計: 新 commit 自動取消舊 workflow
concurrency:
group: deploy-prod-${{ github.ref }}
cancel-in-progress: true
env:
# Harbor 金庫 (110 主機) - 使用 IP 避免 TLS 證書問題
REGISTRY: 192.168.0.110:5000
HARBOR_PROJECT: library
# 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"
if: ${{ false }}
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: Login to Harbor Registry
run: |
echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.HARBOR_USER }} --password-stdin
echo "✅ Harbor 登入成功"
# ----- Build API Image -----
# Phase 6.4i: 必須從 monorepo 根目錄建構 (context: .)
# 因為 Dockerfile 需要複製 packages/lewooogo-* 本地套件
- 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 \
.
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 \
-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
if: ${{ false }}
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..."
# 排除 kustomization.yaml 與 secrets (Secrets 由手動管理,避免覆蓋)
for f in k8s/awoooi-prod/*.yaml; do
BASENAME="$(basename "$f")"
if [[ "$BASENAME" != "kustomization.yaml" && "$BASENAME" != "03-secrets.yaml" && "$BASENAME" != "03-secrets.example.yaml" ]]; then
kubectl apply -f "$f" --namespace=${{ env.K8S_NAMESPACE }}
else
echo "⏭️ Skipped: $BASENAME (managed separately)"
fi
done
- 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
# Worker 暫停,不重啟
echo " Worker restart skipped"
- 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
# Worker 暫停 (replicas=0)Phase 6.5 完善後啟用
echo " Worker deployment skipped (replicas=0)"
- 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: ${{ false }}
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: ${{ false }}
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
# Phase 8: 升級為結構化 HTML + Inline Keyboard UX
run: |
STATUS="${{ steps.status.outputs.status }}"
BUILD_TAG="${{ needs.build.outputs.image_tag }}"
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
ACTOR="${{ github.actor }}"
# 根據狀態設定 Emoji 與標題
if [ "$STATUS" = "success" ]; then
EMOJI="✅"
TITLE="部署成功"
elif [ "$STATUS" = "warning" ]; then
EMOJI="⚠️"
TITLE="部署警告"
else
EMOJI="❌"
TITLE="部署失敗"
fi
# HTML 結構化訊息 (使用 \n 換行)
MESSAGE="${EMOJI} <b>AWOOOI 部署通知</b>\n\n━━━━━━━━━━━━━━━━━\n📦 <b>狀態:</b> ${TITLE}\n🌍 <b>環境:</b> Production\n🏷 <b>版本:</b> <code>${BUILD_TAG}</code>\n🔗 <b>Commit:</b> <code>${SHORT_SHA}</code>\n👤 <b>觸發者:</b> ${ACTOR}\n━━━━━━━━━━━━━━━━━"
# Inline Keyboard 按鈕 (成功時多一個按鈕)
if [ "$STATUS" = "success" ]; then
KEYBOARD='{"inline_keyboard":[[{"text":"📋 查看部署紀錄","url":"'"${RUN_URL}"'"},{"text":"🚀 開啟正式站","url":"https://awoooi.wooo.work"}]]}'
else
KEYBOARD='{"inline_keyboard":[[{"text":"📋 查看部署紀錄","url":"'"${RUN_URL}"'"}]]}'
fi
# 發送 Telegram 訊息 (JSON payload with HTML + Inline Keyboard)
# 鐵律: Token 必須用 Secrets禁止硬編碼
curl -s -X POST "https://api.telegram.org/bot${{ secrets.OPENCLAW_TG_BOT_TOKEN }}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\":\"${{ secrets.OPENCLAW_TG_CHAT_ID }}\",\"text\":\"${MESSAGE}\",\"parse_mode\":\"HTML\",\"reply_markup\":${KEYBOARD}}"
- 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 發送失敗 (非阻塞)"