- Add NEXT_PUBLIC_SENTRY_DSN to CI/CD workflows (build-time injection) - Add SENTRY_DSN build arg to web Dockerfile - Sentry Self-Hosted deployed on 192.168.0.110:9000 - GeoIP database configured (MaxMind GeoLite2-City 61MB) - awoooi-web project: http://da02...@192.168.0.110:9000/2 - awoooi-api project: http://8c4a...@192.168.0.110:9000/3 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
256 lines
9.9 KiB
YAML
256 lines
9.9 KiB
YAML
name: CD
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths-ignore:
|
||
- 'docs/**'
|
||
- '*.md'
|
||
workflow_dispatch:
|
||
inputs:
|
||
skip_api:
|
||
description: '跳過 API 建構 (只改前端時)'
|
||
type: boolean
|
||
default: false
|
||
skip_web:
|
||
description: '跳過 Web 建構 (只改後端時)'
|
||
type: boolean
|
||
default: false
|
||
|
||
# 沿用 AIOPS 設計: 新 commit 自動取消舊 workflow
|
||
concurrency:
|
||
group: cd-${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
env:
|
||
REGISTRY: 192.168.0.110:5000
|
||
IMAGE_PREFIX: library/awoooi
|
||
# 本地快取路徑 (比 Registry cache 更快)
|
||
LOCAL_CACHE_DIR: /home/wooo/build-cache/awoooi
|
||
|
||
jobs:
|
||
# ==================== 變更偵測 ====================
|
||
detect-changes:
|
||
name: Detect Changes
|
||
runs-on: self-hosted
|
||
outputs:
|
||
api_changed: ${{ steps.changes.outputs.api }}
|
||
web_changed: ${{ steps.changes.outputs.web }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 2
|
||
|
||
- name: Check changed files
|
||
id: changes
|
||
run: |
|
||
# 取得變更的檔案
|
||
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
|
||
echo "Changed files: $CHANGED_FILES"
|
||
|
||
# 偵測 API 變更
|
||
if echo "$CHANGED_FILES" | grep -qE "^apps/api/|^packages/"; then
|
||
echo "api=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "api=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
# 偵測 Web 變更
|
||
if echo "$CHANGED_FILES" | grep -qE "^apps/web/|^packages/"; then
|
||
echo "web=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "web=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
# ==================== Build API ====================
|
||
build-api:
|
||
name: Build & Push API
|
||
runs-on: self-hosted
|
||
needs: detect-changes
|
||
if: |
|
||
github.event_name == 'workflow_dispatch' && !inputs.skip_api ||
|
||
github.event_name == 'push' && needs.detect-changes.outputs.api_changed == 'true' ||
|
||
github.event_name == 'push' && needs.detect-changes.outputs.api_changed == 'false' && needs.detect-changes.outputs.web_changed == 'false'
|
||
outputs:
|
||
image_tag: ${{ steps.tag.outputs.tag }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Generate image tag
|
||
id: tag
|
||
run: |
|
||
SHA=$(git rev-parse --short HEAD)
|
||
RUN_ID=${{ github.run_id }}
|
||
echo "tag=${SHA}-${RUN_ID}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Login to Harbor
|
||
run: |
|
||
echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.HARBOR_USER }} --password-stdin
|
||
|
||
# 🚀 沿用 AIOPS: 原生 BuildKit (無中間層損耗)
|
||
- name: Build & Push API (Native BuildKit)
|
||
env:
|
||
DOCKER_BUILDKIT: 1
|
||
run: |
|
||
echo "🐳 使用原生 Docker BuildKit 建構 API..."
|
||
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 }}"
|
||
|
||
# ==================== Build Web ====================
|
||
build-web:
|
||
name: Build & Push Web
|
||
runs-on: self-hosted
|
||
needs: detect-changes
|
||
if: |
|
||
github.event_name == 'workflow_dispatch' && !inputs.skip_web ||
|
||
github.event_name == 'push' && needs.detect-changes.outputs.web_changed == 'true' ||
|
||
github.event_name == 'push' && needs.detect-changes.outputs.api_changed == 'false' && needs.detect-changes.outputs.web_changed == 'false'
|
||
outputs:
|
||
image_tag: ${{ steps.tag.outputs.tag }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Generate image tag
|
||
id: tag
|
||
run: |
|
||
SHA=$(git rev-parse --short HEAD)
|
||
RUN_ID=${{ github.run_id }}
|
||
echo "tag=${SHA}-${RUN_ID}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Login to Harbor
|
||
run: |
|
||
echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.HARBOR_USER }} --password-stdin
|
||
|
||
# 🚀 沿用 AIOPS: 恢復本地 Next.js 快取
|
||
- name: Restore Next.js cache
|
||
run: |
|
||
mkdir -p apps/web/.next/cache
|
||
if [ -d "${{ env.LOCAL_CACHE_DIR }}/nextjs" ]; then
|
||
cp -r ${{ env.LOCAL_CACHE_DIR }}/nextjs/* apps/web/.next/cache/ 2>/dev/null || true
|
||
echo "✅ Next.js 快取已恢復"
|
||
fi
|
||
|
||
# 🚀 沿用 AIOPS: 原生 BuildKit
|
||
- name: Build & Push Web (Native BuildKit)
|
||
env:
|
||
DOCKER_BUILDKIT: 1
|
||
run: |
|
||
echo "🎨 使用原生 Docker BuildKit 建構 Web..."
|
||
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 }}"
|
||
|
||
# 🚀 沿用 AIOPS: 儲存本地快取
|
||
- name: Save Next.js cache
|
||
run: |
|
||
mkdir -p ${{ env.LOCAL_CACHE_DIR }}/nextjs
|
||
if [ -d "apps/web/.next/cache" ]; then
|
||
cp -r apps/web/.next/cache/* ${{ env.LOCAL_CACHE_DIR }}/nextjs/ 2>/dev/null || true
|
||
echo "✅ Next.js 快取已儲存"
|
||
fi
|
||
|
||
# ==================== Deploy to Production ====================
|
||
# Memory 鐵律: 禁止 UAT,只有 Dev + Prod
|
||
deploy-prod:
|
||
name: Deploy to Production
|
||
runs-on: self-hosted
|
||
needs: [detect-changes, build-api, build-web]
|
||
# 允許部分 build 被跳過
|
||
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 Kubeconfig
|
||
run: |
|
||
mkdir -p ~/.kube
|
||
echo "${{ secrets.KUBE_CONFIG_PROD }}" | base64 -d > ~/.kube/config
|
||
chmod 600 ~/.kube/config
|
||
|
||
- name: Install kubectl and Kustomize
|
||
run: |
|
||
mkdir -p $HOME/.local/bin
|
||
# Install kubectl (110 主機應已預裝)
|
||
if ! command -v kubectl &> /dev/null; then
|
||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||
chmod +x kubectl
|
||
mv kubectl $HOME/.local/bin/
|
||
fi
|
||
# Install kustomize (110 主機應已預裝)
|
||
if ! command -v kustomize &> /dev/null; then
|
||
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
||
mv kustomize $HOME/.local/bin/
|
||
fi
|
||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||
|
||
- name: Generate image tag
|
||
id: tag
|
||
run: |
|
||
SHA=$(git rev-parse --short HEAD)
|
||
RUN_ID=${{ github.run_id }}
|
||
echo "tag=${SHA}-${RUN_ID}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Verify Kubeconfig
|
||
run: |
|
||
export PATH="$HOME/.local/bin:$PATH"
|
||
echo "Checking kubeconfig..."
|
||
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'
|
||
echo ""
|
||
kubectl cluster-info
|
||
|
||
- name: Deploy with Kustomize
|
||
run: |
|
||
export PATH="$HOME/.local/bin:$PATH"
|
||
cd k8s/awoooi-prod
|
||
# 使用 kustomize edit set image: OLD_IMAGE=NEW_IMAGE
|
||
# OLD_IMAGE 必須與 deployment YAML 中的 image 欄位完全匹配
|
||
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: |
|
||
export PATH="$HOME/.local/bin:$PATH"
|
||
kubectl rollout status deployment/awoooi-web -n awoooi-prod --timeout=300s
|
||
kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=300s
|
||
|
||
- name: Health check
|
||
run: |
|
||
export PATH="$HOME/.local/bin:$PATH"
|
||
sleep 10
|
||
# 使用 kubectl 驗證 Pod 健康 (避免 runner DNS 問題)
|
||
echo "🔍 檢查 API Pod 狀態..."
|
||
kubectl get pods -n awoooi-prod -l app=awoooi-api -o jsonpath='{.items[*].status.phase}' | grep -q Running
|
||
echo "✅ API Pod Running"
|
||
|
||
# 透過 kubectl exec 測試內部健康端點
|
||
API_POD=$(kubectl get pods -n awoooi-prod -l app=awoooi-api -o jsonpath='{.items[0].metadata.name}')
|
||
kubectl exec -n awoooi-prod $API_POD -- curl -sf http://localhost:8000/api/v1/health || exit 1
|
||
echo "✅ API 內部健康檢查通過"
|
||
|
||
- name: Notify Telegram on Success
|
||
if: success()
|
||
run: |
|
||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.OPENCLAW_TG_BOT_TOKEN }}/sendMessage" \
|
||
-d chat_id="${{ secrets.OPENCLAW_TG_CHAT_ID }}" \
|
||
-d text="✅ *AWOOOI 部署成功*%0A%0ACommit: \`${{ github.sha }}\`%0ABranch: \`${{ github.ref_name }}\`%0AWorkflow: [查看](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" \
|
||
-d parse_mode="Markdown"
|
||
|
||
- name: Notify Telegram on Failure
|
||
if: failure()
|
||
run: |
|
||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.OPENCLAW_TG_BOT_TOKEN }}/sendMessage" \
|
||
-d chat_id="${{ secrets.OPENCLAW_TG_CHAT_ID }}" \
|
||
-d text="❌ *AWOOOI 部署失敗*%0A%0ACommit: \`${{ github.sha }}\`%0ABranch: \`${{ github.ref_name }}\`%0AWorkflow: [查看](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" \
|
||
-d parse_mode="Markdown"
|