# ============================================================================= # AWOOOI CD Pipeline - 開發環境 (dev branch) # ============================================================================= # 流程: Build → Push to Harbor (dev tag) → Deploy to awoooi-dev namespace # 用途: 驗證修改,確認無誤後才 merge main → 觸發正式環境部署 # 2026-04-01 ogt: 建立開發環境 CI/CD 分離機制 name: CD Pipeline (Dev) on: push: branches: [dev] workflow_dispatch: concurrency: group: cd-dev-deploy-${{ github.ref }} cancel-in-progress: false env: HARBOR: 192.168.0.110:5000 HARBOR_MIRROR: 192.168.0.110:5001 SRE_GROUP_CHAT_ID: "-1003711974679" OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.0.188:24318 OTEL_SERVICE_NAME: awoooi-cd-dev OTEL_RESOURCE_ATTRIBUTES: service.version=${{ github.sha }},deployment.environment=dev jobs: build-and-deploy-dev: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - 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 Dev Deploy Start run: | MSG="🔧 [DEV] 部署開始 ├ 📝 ${{ steps.commit.outputs.message }} ├ 🔖 ${{ steps.commit.outputs.short_sha }} └ 🌿 dev branch" if AWOOI_CICD_STATUS=running \ AWOOI_CICD_STAGE=dev-deploy \ AWOOI_CICD_JOB_NAME="[DEV] 部署開始" \ AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \ AWOOI_CICD_SUMMARY="${{ steps.commit.outputs.message }}" \ scripts/ci/notify-awoooi-cicd.sh; then echo "Dev deploy start notification mirrored through AWOOI API" else printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text@-" fi # API 測試 (同 prod CI,確保 dev 也通過) - name: Run API Tests run: | VENV=/opt/api-venv HASH_FILE=/opt/api-venv/.deps_hash CURRENT_HASH=$(md5sum apps/api/pyproject.toml | awk '{print $1}') if [ ! -d "$VENV" ] || [ "$(cat $HASH_FILE 2>/dev/null)" != "$CURRENT_HASH" ]; then python3 -m venv $VENV source $VENV/bin/activate pip install -q uv cd apps/api && uv pip install -q -e ".[dev]" && cd - echo "$CURRENT_HASH" > $HASH_FILE else source $VENV/bin/activate fi cd apps/api # 2026-04-22 ogt: DATABASE_URL 改為必填,單元測試需要此 env var 讓 Settings 通過驗證 DATABASE_URL="${DATABASE_URL:-postgresql+asyncpg://ci:ci@localhost/ci}" \ pytest tests/ -v --tb=short -x \ --ignore=tests/test_anomaly_counter.py \ --ignore=tests/test_global_repair_cooldown.py \ --ignore=tests/test_redis_multisig.py \ --ignore=tests/test_model_regression.py \ --ignore=tests/test_prompt_validation.py \ 2>&1 | tail -50 echo "✅ API 測試通過" - name: Login to Harbor run: | HARBOR_USERNAME="$(cat <<'AWOOOI_SECRET_HARBOR_USERNAME' ${{ secrets.HARBOR_USERNAME }} AWOOOI_SECRET_HARBOR_USERNAME )" HARBOR_PASSWORD="$(cat <<'AWOOOI_SECRET_HARBOR_PASSWORD' ${{ secrets.HARBOR_PASSWORD }} AWOOOI_SECRET_HARBOR_PASSWORD )" printf '%s' "$HARBOR_PASSWORD" | docker login "${{ env.HARBOR }}" \ -u "$HARBOR_USERNAME" \ --password-stdin # Dev API 鏡像:強制重建,不用 cache(確保 models.json 等配置文件更新) - name: Build and Push API (Dev) run: | docker build -f apps/api/Dockerfile \ --no-cache \ -t ${{ env.HARBOR }}/awoooi/api:dev-${{ github.sha }} \ -t ${{ env.HARBOR }}/awoooi/api:dev-latest \ . docker push ${{ env.HARBOR }}/awoooi/api:dev-${{ github.sha }} docker push ${{ env.HARBOR }}/awoooi/api:dev-latest echo "✅ Dev API 鏡像建置完成" # 注入 Dev K8s Secrets - name: Inject Dev K8s Secrets run: | secret_b64() { python3 -c 'import base64, sys; data=sys.stdin.buffer.read(); data=data[:-1] if data.endswith(b"\n") else data; sys.stdout.write(base64.b64encode(data).decode())' } write_deploy_key() { mkdir -p ~/.ssh umask 077 cat > ~/.ssh/deploy_key <<'AWOOOI_DEPLOY_KEY' ${{ secrets.DEPLOY_SSH_KEY }} AWOOOI_DEPLOY_KEY chmod 600 ~/.ssh/deploy_key } TG_BOT_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_TG_BOT_TOKEN' ${{ secrets.TELEGRAM_BOT_TOKEN }} AWOOOI_SECRET_TG_BOT_TOKEN )" TG_CHAT_ID_B64="$(secret_b64 <<'AWOOOI_SECRET_SRE_GROUP_CHAT_ID_COMPAT' ${{ secrets.SRE_GROUP_CHAT_ID }} AWOOOI_SECRET_SRE_GROUP_CHAT_ID_COMPAT )" NVIDIA_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_NVIDIA_API_KEY' ${{ secrets.NVIDIA_API_KEY }} AWOOOI_SECRET_NVIDIA_API_KEY )" GEMINI_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_GEMINI_API_KEY' ${{ secrets.GEMINI_API_KEY }} AWOOOI_SECRET_GEMINI_API_KEY )" mkdir -p ~/.ssh write_deploy_key ssh-keyscan -T 5 -t ed25519,rsa,ecdsa 192.168.0.120 > "${HOME}/.ssh/known_hosts" 2>/dev/null test -s "${HOME}/.ssh/known_hosts" || { echo "❌ K8S host keyscan failed: 192.168.0.120"; exit 1; } SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${HOME}/.ssh/known_hosts -i ~/.ssh/deploy_key" # 2026-05-05 Codex: kubectl runs on 120 control-plane. 121 is a # worker and its local kubeconfig points at 127.0.0.1:6443. ssh $SSH_OPTS wooo@192.168.0.120 << SECRETS set -e export KUBECONFIG=/etc/rancher/k3s/k3s.yaml sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[ {"op":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"${TG_BOT_TOKEN_B64}"}, {"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"${TG_CHAT_ID_B64}"} ]' || echo "⚠️ Telegram Secrets patch 跳過" if [ -n "${NVIDIA_API_KEY_B64}" ]; then sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[ {"op":"replace","path":"/data/NVIDIA_API_KEY","value":"${NVIDIA_API_KEY_B64}"} ]' && echo "✅ NVIDIA_API_KEY 已注入 dev" fi if [ -n "${GEMINI_API_KEY_B64}" ]; then sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[ {"op":"replace","path":"/data/GEMINI_API_KEY","value":"${GEMINI_API_KEY_B64}"} ]' && echo "✅ GEMINI_API_KEY 已注入 dev" fi echo "✅ Dev Secrets 注入完成" SECRETS # 部署到 awoooi-dev - name: Deploy to Dev K8s run: | ssh-keyscan -T 5 -t ed25519,rsa,ecdsa 192.168.0.120 > "${HOME}/.ssh/known_hosts" 2>/dev/null test -s "${HOME}/.ssh/known_hosts" || { echo "❌ K8S host keyscan failed: 192.168.0.120"; exit 1; } SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${HOME}/.ssh/known_hosts -i ~/.ssh/deploy_key" cat k8s/awoooi-dev/02-configmap.yaml | \ ssh $SSH_OPTS wooo@192.168.0.120 \ "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -" ssh $SSH_OPTS wooo@192.168.0.120 << 'DEPLOY' set -e export KUBECONFIG=/etc/rancher/k3s/k3s.yaml sudo kubectl set image deployment/awoooi-api \ api=192.168.0.110:5000/awoooi/api:dev-${{ github.sha }} \ -n awoooi-dev sudo kubectl rollout status deployment/awoooi-api -n awoooi-dev --timeout=120s echo "✅ Dev 部署完成" # Health Check sleep 10 HEALTH_PASS=0 for i in 1 2 3; do HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null --connect-timeout 10 "http://localhost:32344/api/v1/health") if [ "$HTTP_CODE" = "200" ]; then echo "✅ Dev API 健康檢查通過 (port 32344)" HEALTH_PASS=1 break fi echo "⏳ 嘗試 #$i: HTTP $HTTP_CODE,等待 10s..." sleep 10 done if [ "$HEALTH_PASS" = "0" ]; then echo "❌ Dev API 健康檢查失敗" exit 1 fi DEPLOY - name: Notify Dev Deploy Success run: | END_TIME=$(date +%s) DURATION=$((END_TIME - ${{ steps.commit.outputs.start_time }})) MINUTES=$((DURATION / 60)) SECONDS=$((DURATION % 60)) MSG="✅ [DEV] 部署完成 ├ 📝 ${{ steps.commit.outputs.message }} ├ 🔖 ${{ steps.commit.outputs.short_sha }} ├ ⏱️ 耗時: ${MINUTES}m ${SECONDS}s └ 🩺 http://192.168.0.125:32344/api/v1/health" if AWOOI_CICD_STATUS=success \ AWOOI_CICD_STAGE=dev-deploy \ AWOOI_CICD_JOB_NAME="[DEV] 部署完成" \ AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \ AWOOI_CICD_DURATION_SECONDS="${DURATION}" \ AWOOI_CICD_SUMMARY="${{ steps.commit.outputs.message }}" \ scripts/ci/notify-awoooi-cicd.sh; then echo "Dev deploy success notification mirrored through AWOOI API" else printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text@-" fi - name: Notify Dev Deploy Failure if: failure() run: | MSG="❌ [DEV] 部署失敗 ├ 📝 ${{ steps.commit.outputs.message }} ├ 🔖 ${{ steps.commit.outputs.short_sha }} └ 🔗 查看日誌" if AWOOI_CICD_STATUS=failed \ AWOOI_CICD_STAGE=dev-deploy \ AWOOI_CICD_JOB_NAME="[DEV] 部署失敗" \ AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \ AWOOI_CICD_SUMMARY="${{ steps.commit.outputs.message }}" \ scripts/ci/notify-awoooi-cicd.sh; then echo "Dev deploy failure notification mirrored through AWOOI API" else printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text@-" fi