From 5761aeb1ce07087be8450c5084fb31d8cc8392a5 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 22 Apr 2026 01:53:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(cd):=20=E4=BF=AE=E5=BE=A9=20CD=20Pipeline?= =?UTF-8?q?=2011=20=E9=A0=85=E5=AE=89=E5=85=A8/=E5=8F=AF=E9=9D=A0=E6=80=A7?= =?UTF-8?q?=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔴 Critical: C1 commit message injection: 所有 ${{ }} 值改走 env: 區塊隔離,不直接嵌入 shell C2 SSH StrictHostKeyChecking: 改用 known_hosts 驗證,支援 SSH_HOST_KEY secret 🟠 High: H1 rsync excludes 對齊: Rebuild 模式補齊 .gitea/ .claude/ docs/ *.md 等 7 條 H2 --force-recreate: Rebuild 模式加入強制重建,防止靜默更新失敗 H3 健康檢查強化: 加入 SSH 驗三容器 Running 狀態(scheduler/telegram-bot) H4 緊急回滾: 部署失敗時自動嘗試 docker restart 三容器回復服務 H5 ADR-011 守衛: Rebuild 前確認 momo-postgres 存活才繼續 🟡 Medium: M1 .claude/ 加入 rsync excludes(不同步 hook 腳本至 188) M2 *.md 加入 rsync excludes(根目錄 markdown 不需同步) M3 workflow_dispatch 新增 force_rebuild boolean 輸入 M4 cancel-in-progress 已知風險記入檔頭注意事項 Co-Authored-By: Claude Sonnet 4.6 --- .gitea/workflows/cd.yaml | 162 +++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 42 deletions(-) diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index a0c389bfd..ccb18a1b5 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -5,6 +5,13 @@ # 部署架構: Docker Compose on ollama@192.168.0.188 (Volume Mount) # 加速措施: Python 檔案走 rsync,僅 Dockerfile/requirements 變動才重建 image # 參考: AWOOOI cd.yaml pattern (ADR-008 — Docker Compose 非 K8s) +# +# 安全注意: +# SSH_HOST_KEY secret: 請執行 ssh-keyscan 192.168.0.188 並將輸出存入 Gitea Secret +# 若未設定,自動 ssh-keyscan(私有網段可接受的降級) +# +# 已知風險: +# cancel-in-progress: rsync 非原子,若新 push 在傳輸中取消,188 可能半更新狀態 name: CD Pipeline @@ -36,7 +43,11 @@ on: - '.gitea/workflows/**' # docs/、memory/、ADR、k8s/ 等不觸發 workflow_dispatch: - # 手動觸發永遠可用(用於補跑、緊急部署) + inputs: + force_rebuild: + description: '強制重建 Docker Image(不論變更檔案)' + type: boolean + default: false # 新 push 立即取消舊 job,只部署最新版本 concurrency: @@ -59,12 +70,16 @@ jobs: echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT echo "message=$(git log -1 --pretty=%s | head -c 60)" >> $GITHUB_OUTPUT echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT - echo "actor=${{ github.actor }}" >> $GITHUB_OUTPUT - # 偵測是否需重建 Docker image(Dockerfile / requirements.txt / docker-compose.yml 變動) + # 偵測是否需重建 Docker image(force_rebuild 優先,其次看變更檔案) - name: 偵測部署類型 id: deploy_type run: | + if [[ "${{ github.event.inputs.force_rebuild }}" == "true" ]]; then + echo "type=rebuild" >> $GITHUB_OUTPUT + echo "label=🔨 強制重建 Docker Image" >> $GITHUB_OUTPUT + exit 0 + fi CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") if echo "$CHANGED" | grep -qE '^(Dockerfile|requirements\.txt|docker-compose\.yml)$'; then echo "type=rebuild" >> $GITHUB_OUTPUT @@ -74,32 +89,46 @@ jobs: echo "label=📁 同步 Python 檔案" >> $GITHUB_OUTPUT fi - # 設定 SSH 金鑰(用於連線到 188) + # 設定 SSH 金鑰 + 主機驗證(C2 fix: 移除 StrictHostKeyChecking no) - name: 設定 SSH 金鑰 + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + SSH_HOST_KEY: ${{ secrets.SSH_HOST_KEY }} run: | mkdir -p ~/.ssh - echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_deploy + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_deploy chmod 600 ~/.ssh/id_deploy + # 主機驗證:優先使用 SSH_HOST_KEY secret,否則動態掃描(私有網段降級) + if [[ -n "$SSH_HOST_KEY" ]]; then + echo "$SSH_HOST_KEY" >> ~/.ssh/known_hosts + else + ssh-keyscan -H 192.168.0.188 >> ~/.ssh/known_hosts 2>/dev/null + fi + chmod 644 ~/.ssh/known_hosts cat > ~/.ssh/config << 'EOF' Host 192.168.0.188 HostName 192.168.0.188 User ollama IdentityFile ~/.ssh/id_deploy - StrictHostKeyChecking no ConnectTimeout 10 EOF + # 通知部署開始(C1 fix: 所有 ${{ }} 值改走 env: 區塊,不直接嵌入 shell) - name: 通知部署開始 + env: + COMMIT_MSG: ${{ steps.commit.outputs.message }} + COMMIT_SHA: ${{ steps.commit.outputs.short_sha }} + COMMIT_ACTOR: ${{ github.actor }} + DEPLOY_LABEL: ${{ steps.deploy_type.outputs.label }} + TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }} run: | - COMMIT_ESC=$(echo "${{ steps.commit.outputs.message }}" | sed 's/&/\&/g; s//\>/g') + COMMIT_ESC=$(printf '%s' "$COMMIT_MSG" | sed 's/&/\&/g; s//\>/g') MSG=$(printf '🚀 EwoooC 部署開始\n├ 📝 %s\n├ 🔖 %s\n├ 👤 %s\n└ %s' \ - "${COMMIT_ESC}" \ - "${{ steps.commit.outputs.short_sha }}" \ - "${{ steps.commit.outputs.actor }}" \ - "${{ steps.deploy_type.outputs.label }}") - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + "${COMMIT_ESC}" "${COMMIT_SHA}" "${COMMIT_ACTOR}" "${DEPLOY_LABEL}") + curl -fS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ - -d "$(jq -n --arg c "${{ secrets.TELEGRAM_CHAT_ID }}" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')" + -d "$(jq -n --arg c "$TG_CHAT" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')" # ── 安裝部署工具 ──────────────────────────────────────────────────── - name: 安裝 rsync / ssh @@ -111,9 +140,10 @@ jobs: if: steps.deploy_type.outputs.type == 'sync' run: | rsync -avz \ - -e "ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=no" \ + -e "ssh -i ~/.ssh/id_deploy" \ --exclude='.git/' \ --exclude='.gitea/' \ + --exclude='.claude/' \ --exclude='data/' \ --exclude='logs/' \ --exclude='backups/' \ @@ -125,6 +155,7 @@ jobs: --exclude='.env' \ --exclude='*.db' \ --exclude='*.db-journal' \ + --exclude='*.md' \ --exclude='docs/' \ --exclude='memory/' \ --exclude='k8s/' \ @@ -135,7 +166,7 @@ jobs: - name: 重啟容器(Sync 模式) if: steps.deploy_type.outputs.type == 'sync' run: | - ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=no ollama@192.168.0.188 \ + ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \ "docker restart momo-pro-system momo-scheduler momo-telegram-bot 2>&1 && \ echo '✅ 三容器已重啟(app/scheduler/telegram-bot)'" @@ -143,10 +174,16 @@ jobs: - name: 同步所有檔案並重建 Image if: steps.deploy_type.outputs.type == 'rebuild' run: | - # 先同步全部檔案 + # H5: ADR-011 守衛 — momo-postgres 必須存活才允許 rebuild + ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \ + "docker ps --format '{{.Names}}' | grep -q 'momo-postgres' || \ + (echo 'ABORT: momo-postgres not running' && exit 1)" + # H1: 與 Sync 模式對齊的完整 excludes(含 .gitea/ .claude/ docs/ *.md) rsync -avz \ - -e "ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=no" \ + -e "ssh -i ~/.ssh/id_deploy" \ --exclude='.git/' \ + --exclude='.gitea/' \ + --exclude='.claude/' \ --exclude='data/' \ --exclude='logs/' \ --exclude='backups/' \ @@ -157,17 +194,24 @@ jobs: --exclude='*.pyc' \ --exclude='.env' \ --exclude='*.db' \ + --exclude='*.db-journal' \ + --exclude='*.md' \ + --exclude='docs/' \ + --exclude='memory/' \ + --exclude='k8s/' \ + --exclude='n8n-workflows/' \ + --exclude='aiops-core/' \ ./ ollama@192.168.0.188:/home/ollama/momo-pro/ - # 重建並重啟(先強制移除舊容器,避免 container name conflict) - ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=no ollama@192.168.0.188 \ + # H2: --force-recreate 確保容器強制重建(避免靜默更新失敗) + ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \ "cd /home/ollama/momo-pro && \ docker stop momo-pro-system momo-scheduler momo-telegram-bot 2>/dev/null; \ docker rm momo-pro-system momo-scheduler momo-telegram-bot 2>/dev/null; \ docker compose build momo-app && \ - docker compose up -d --no-deps momo-app scheduler telegram-bot && \ + docker compose up -d --no-deps --force-recreate momo-app scheduler telegram-bot && \ echo '✅ Image 重建完成(三容器)'" - # ── 健康檢查(最多重試 5 次,每次間隔 10s) ─────────────────────────── + # ── 健康檢查(H3: HTTP + 三容器狀態雙重驗證) ───────────────────────── - name: 健康檢查 run: | echo "⏳ 等待服務啟動(15s)..." @@ -175,19 +219,32 @@ jobs: for i in $(seq 1 5); do HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://mo.wooo.work/health --max-time 10 || echo "000") if [ "$HTTP_CODE" = "200" ]; then - echo "✅ 健康檢查通過(HTTP $HTTP_CODE)" - exit 0 + echo "✅ HTTP 健康檢查通過(HTTP $HTTP_CODE)" + break fi echo "⏳ 嘗試 $i/5,HTTP $HTTP_CODE,等待 10s..." + [ "$i" -eq 5 ] && echo "❌ HTTP 健康檢查失敗" && exit 1 sleep 10 done - echo "❌ 健康檢查失敗" - exit 1 + # 驗證三應用容器均在 Running 狀態 + ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \ + 'RUNNING=$(docker ps --format "{{.Names}}" | grep -cE "momo-(pro-system|scheduler|telegram-bot)" || true); \ + if [ "$RUNNING" -lt 3 ]; then \ + docker ps --format "{{.Names}}\t{{.Status}}" | grep momo-; \ + echo "❌ 容器未全部就緒(Running: $RUNNING/3)"; exit 1; \ + else \ + echo "✅ 三容器均正常運行($RUNNING/3)"; \ + fi' # ── 觸發 Post-Deploy Code Review ───────────────────────────────────── - name: 觸發 AI Code Review if: success() continue-on-error: true + env: + WEBHOOK_TOKEN: ${{ secrets.INTERNAL_WEBHOOK_TOKEN }} + COMMIT_SHA_FULL: ${{ github.sha }} + BRANCH_NAME: ${{ github.ref_name }} + DEPLOY_TYPE: ${{ steps.deploy_type.outputs.type }} run: | CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") FILES_JSON=$(echo "$CHANGED" | grep -E '\.(py|yaml|yml|json)$' | \ @@ -195,34 +252,55 @@ jobs: curl -fS --max-time 10 \ -X POST "https://mo.wooo.work/code-review/api/internal/trigger" \ -H "Content-Type: application/json" \ - -H "X-Internal-Token: ${{ secrets.INTERNAL_WEBHOOK_TOKEN }}" \ - -d "{\"commit_sha\":\"${{ github.sha }}\",\"changed_files\":${FILES_JSON},\"branch\":\"${{ github.ref_name }}\",\"deploy_type\":\"${{ steps.deploy_type.outputs.type }}\"}" \ + -H "X-Internal-Token: ${WEBHOOK_TOKEN}" \ + -d "$(jq -n \ + --arg sha "$COMMIT_SHA_FULL" \ + --argjson files "$FILES_JSON" \ + --arg branch "$BRANCH_NAME" \ + --arg type "$DEPLOY_TYPE" \ + '{commit_sha:$sha,changed_files:$files,branch:$branch,deploy_type:$type}')" \ && echo "✅ Code Review Pipeline 已觸發" \ || echo "⚠️ Code Review webhook 呼叫失敗(不影響部署結果)" - # ── 部署成功通知 ────────────────────────────────────────────────────── + # ── 部署成功通知(C1 fix: env: 區塊隔離)──────────────────────────── - name: 通知部署成功 if: success() + env: + COMMIT_MSG: ${{ steps.commit.outputs.message }} + COMMIT_SHA: ${{ steps.commit.outputs.short_sha }} + START_TIME: ${{ steps.commit.outputs.start_time }} + TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }} run: | END_TIME=$(date +%s) - DURATION=$((END_TIME - ${{ steps.commit.outputs.start_time }})) - COMMIT_ESC=$(echo "${{ steps.commit.outputs.message }}" | sed 's/&/\&/g; s//\>/g') + DURATION=$((END_TIME - START_TIME)) + COMMIT_ESC=$(printf '%s' "$COMMIT_MSG" | sed 's/&/\&/g; s//\>/g') MSG=$(printf '✅ EwoooC 部署成功\n├ 📝 %s\n├ 🔖 %s\n├ ⏱ 耗時 %ss\n└ 🌐 https://mo.wooo.work' \ - "${COMMIT_ESC}" \ - "${{ steps.commit.outputs.short_sha }}" \ - "${DURATION}") - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + "${COMMIT_ESC}" "${COMMIT_SHA}" "${DURATION}") + curl -fS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ - -d "$(jq -n --arg c "${{ secrets.TELEGRAM_CHAT_ID }}" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')" + -d "$(jq -n --arg c "$TG_CHAT" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')" - # ── 部署失敗通知 ────────────────────────────────────────────────────── - - name: 通知部署失敗 + # ── H4: 緊急回滾嘗試(部署失敗時嘗試重啟三容器回復服務)─────────────── + - name: 緊急回滾嘗試 if: failure() run: | - COMMIT_ESC=$(echo "${{ steps.commit.outputs.message }}" | sed 's/&/\&/g; s//\>/g') + echo "⚠️ 部署失敗,嘗試重啟三容器回復服務..." + ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \ + "docker restart momo-pro-system momo-scheduler momo-telegram-bot 2>&1 || true" || true + + # ── 部署失敗通知(C1 fix: env: 區塊隔離)──────────────────────────── + - name: 通知部署失敗 + if: failure() + env: + COMMIT_MSG: ${{ steps.commit.outputs.message }} + COMMIT_SHA: ${{ steps.commit.outputs.short_sha }} + TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }} + run: | + COMMIT_ESC=$(printf '%s' "$COMMIT_MSG" | sed 's/&/\&/g; s//\>/g') MSG=$(printf '❌ EwoooC 部署失敗\n├ 📝 %s\n├ 🔖 %s\n└ 🔍 請查看 Gitea Actions 日誌' \ - "${COMMIT_ESC}" \ - "${{ steps.commit.outputs.short_sha }}") - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + "${COMMIT_ESC}" "${COMMIT_SHA}") + curl -fS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ - -d "$(jq -n --arg c "${{ secrets.TELEGRAM_CHAT_ID }}" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')" + -d "$(jq -n --arg c "$TG_CHAT" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')"