# =============================================================================
# EwoooC CD Pipeline (Gitea Actions)
# =============================================================================
# 流程: Sync Files → 188 Docker Restart → Health Check
# 部署架構: Docker Compose on ollama@192.168.0.188 (Volume Mount)
# 加速措施: Python 檔案走 rsync,僅 Dockerfile/requirements 變動才重建 image
# 參考: AWOOOI cd.yaml pattern (ADR-008 — Docker Compose 非 K8s)
# 注意: runner image 仍可能顯示 awoooi/ci-runner;判斷專案請看 task repo/workspace 是否為 wooo/ewoooc。
# Runner 必須使用 ewoooc-host 專屬 label,避免 user-level runner 混接 AWOOOI workflow。
#
# 安全注意:
# SSH_HOST_KEY secret: 請執行 ssh-keyscan 192.168.0.188 並將輸出存入 Gitea Secret
# 若未設定,自動 ssh-keyscan(私有網段可接受的降級)
#
# 已知風險:
# cancel-in-progress: rsync 非原子,若新 push 在傳輸中取消,188 可能半更新狀態
name: CD Pipeline
on:
push:
branches: [main]
paths:
# 應用程式碼(volume-mounted)
- 'app.py'
- 'auth.py'
- 'config.py'
- 'scheduler.py'
- 'run_scheduler.py'
- 'run_telegram_bot.py'
- 'gunicorn.conf.py'
- 'services/**'
- 'routes/**'
- 'database/**'
- 'templates/**'
- 'static/**'
- 'monitoring/prometheus.yml'
- 'monitoring/blackbox.yml'
# 需重建 image 的檔案
- 'Dockerfile'
- 'requirements.txt'
- 'docker-compose.yml'
# 腳本工具
- 'scripts/**'
# Claude Code 指令 / Hooks
- '.claude/**'
# 工作流程本身
- '.gitea/workflows/**'
# DB schema 變更(Operation Ollama-First v5.0 P1:CD 自動 apply migration)
- 'migrations/**'
# docs/、memory/、ADR、k8s/ 等不觸發
workflow_dispatch:
inputs:
force_rebuild:
description: '強制重建 Docker Image(不論變更檔案)'
type: boolean
default: false
# 新 push 立即取消舊 job,只部署最新版本
concurrency:
group: cd-deploy-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
timeout-minutes: 20
runs-on: ewoooc-host
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: 取得 Commit 資訊
id: commit
run: |
echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
echo "message=$(git log -1 --pretty=%s | head -c 60)" >> $GITHUB_OUTPUT
VERSION=$(sed -n "s/^SYSTEM_VERSION[[:space:]]*=[[:space:]]*[\"']\([^\"']*\)[\"'].*/\1/p" config.py | head -1)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
# 偵測是否需重建 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
echo "label=🔨 重建 Docker Image" >> $GITHUB_OUTPUT
else
echo "type=sync" >> $GITHUB_OUTPUT
echo "label=📁 同步 runtime 檔案" >> $GITHUB_OUTPUT
fi
- name: 偵測 AI 觀測台前端 QA 是否需要執行
id: observability_qa
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
echo "$CHANGED"
printf '%s\n' "$CHANGED" | python3 scripts/check_observability_deploy_gate.py --stdin --github-output "$GITHUB_OUTPUT"
if grep -q '^needed=true$' "$GITHUB_OUTPUT"; then
echo "🎛️ AI 觀測台 QA: required"
else
echo "ℹ️ AI 觀測台 QA: skipped"
fi
# 設定 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
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
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=$(printf '%s' "$COMMIT_MSG" | sed 's/&/\&/g; s/\</g; s/>/\>/g')
MSG=$(printf '🚀 EwoooC 部署開始\n├ 📝 %s\n├ 🔖 %s\n├ 👤 %s\n└ %s' \
"${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 "$TG_CHAT" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')"
# ── 安裝部署工具 ────────────────────────────────────────────────────
# rsync --ignore-errors 防止單一不可寫 attr 中斷整個部署
# rsync --inplace 避免 app.py/config.py 單檔 bind mount 因 inode replacement 卡住舊版本
- name: 安裝 rsync / ssh
run: |
apt-get update -qq && apt-get install -y -qq rsync openssh-client
- name: AI 觀測台 Pre-deploy 靜態 QA
if: steps.observability_qa.outputs.needed == 'true'
run: |
bash ./scripts/quick_review.sh --check-observability-css
bash ./scripts/quick_review.sh --observability-qa --skip-production
# ── 模式 A:僅同步 Python 檔案(最常見,~10s) ────────────────────────
- name: 同步 Python 檔案至 188
if: steps.deploy_type.outputs.type == 'sync'
run: |
rsync -avz --ignore-errors \
--inplace \
-e "ssh -i ~/.ssh/id_deploy" \
--exclude='.git/' \
--exclude='.gitea/' \
--exclude='.claude/' \
--exclude='data/' \
--exclude='logs/' \
--exclude='backups/' \
--exclude='config/google_credentials.json' \
--exclude='config/google_token.pickle' \
--exclude='venv/' \
--exclude='__pycache__/' \
--exclude='*.pyc' \
--exclude='.env' \
--exclude='*.db' \
--exclude='*.db-journal' \
--exclude='*.md' \
--exclude='docs/' \
--exclude='memory/' \
--exclude='k8s/' \
--exclude='n8n-workflows/' \
--exclude='aiops-core/' \
--exclude='monitoring/alertmanager/' \
--exclude='._*' \
./ ollama@192.168.0.188:/home/ollama/momo-pro/ || true
# ── Operation Ollama-First v5.0 P1:自動 apply pending migration ─────
# HOTFIX (2026-05-04 Telegram 報「ai_calls relation does not exist」):
# 原邏輯 git diff HEAD~1 HEAD 只看單一 commit,但 v5.0 migrations 024-028 在 commit
# 4648673(早期),後續 push 都不含 migration → 從未被 CD 跑過。
# 改邏輯:跑 v5.0 戰役所有 migrations(024-099 編號範圍)。
# 安全保證:所有 v5.0 migration 都是 IF NOT EXISTS / WHERE NOT EXISTS / CREATE EXTENSION IF NOT EXISTS
# 重跑 100% 冪等無害(critic-A11 修補保證)。
- name: 套用 v5.0 戰役 migration(024-099 範圍,冪等)
if: steps.deploy_type.outputs.type == 'sync' || steps.deploy_type.outputs.type == 'rebuild'
run: |
ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 '
set -e
cd /home/ollama/momo-pro
# v5.0 戰役 migration 編號 024+ 全部冪等
V5_MIGRATIONS=$(ls migrations/02[4-9]_*.sql migrations/03[0-9]_*.sql migrations/04[0-9]_*.sql migrations/05[0-9]_*.sql migrations/06[0-9]_*.sql migrations/07[0-9]_*.sql migrations/08[0-9]_*.sql migrations/09[0-9]_*.sql 2>/dev/null | sort | uniq || true)
if [ -z "$V5_MIGRATIONS" ]; then
echo "ℹ️ 無 v5.0 migration 檔案"
exit 0
fi
echo "🗄️ v5.0 戰役 migrations 全部跑(冪等 IF NOT EXISTS 保證):"
echo "$V5_MIGRATIONS"
for m in $V5_MIGRATIONS; do
echo "▶️ Applying $m"
# CONCURRENTLY 不能包 -1 transaction
if grep -q "CONCURRENTLY" "$m"; then
docker exec -i momo-db psql -U momo -d momo_pro < "$m" 2>&1 | tail -5 || \
echo "⚠️ $m apply 警告(IF NOT EXISTS 冪等可忽略)"
else
docker exec -i momo-db psql -U momo -d momo_pro -1 < "$m" 2>&1 | tail -5 || \
echo "⚠️ $m apply 警告(IF NOT EXISTS 冪等可忽略)"
fi
done
echo "✅ v5.0 migration apply 階段完成"
'
- name: 重啟容器(Sync 模式)
if: steps.deploy_type.outputs.type == 'sync'
run: |
# ROOT CAUSE FIX (2026-04-28): `up -d` 確保容器不存在時可自動 create。
# ZERO-DOWNTIME SYNC (2026-04-30): app 用 Gunicorn HUP 熱重載 workers,
# 不重啟 momo-pro-system 容器,避免一般 Python/模板同步造成短暫 502。
# MOUNT DRIFT GUARD (2026-04-30): 舊容器若尚未套用 app.py/config.py bind mount,
# health/version 會卡在 image 內舊檔;只在偵測到 drift 時 force-recreate app。
ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 '
set -e
cd /home/ollama/momo-pro
docker compose up -d --no-deps scheduler telegram-bot
if docker inspect momo-pro-system --format "{{range .Mounts}}{{println .Destination}}{{end}}" | grep -qx "/app/app.py" \
&& docker inspect momo-pro-system --format "{{range .Mounts}}{{println .Destination}}{{end}}" | grep -qx "/app/config.py"; then
docker compose up -d --no-deps momo-app
else
echo "⚠️ momo-app mount drift detected; recreating app container once to apply bind mounts"
docker compose up -d --no-deps --force-recreate momo-app
fi
docker kill -s HUP momo-pro-system
docker compose restart scheduler telegram-bot
echo "✅ Sync 已套用(app 熱重載;scheduler/telegram-bot 已重啟)"
'
# ── 模式 B:重建 Docker Image(Dockerfile / requirements.txt 變動) ──
- name: 同步所有檔案並重建 Image
if: steps.deploy_type.outputs.type == 'rebuild'
run: |
# H5: ADR-011 守衛 — momo-db 必須存活才允許 rebuild
ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \
"docker ps --format '{{.Names}}' | grep -q '^momo-db$' || \
(echo 'ABORT: momo-db not running' && exit 1)"
# H1: 與 Sync 模式對齊的完整 excludes(含 .gitea/ .claude/ docs/ *.md)
rsync -avz --ignore-errors \
--inplace \
-e "ssh -i ~/.ssh/id_deploy" \
--exclude='.git/' \
--exclude='.gitea/' \
--exclude='.claude/' \
--exclude='data/' \
--exclude='logs/' \
--exclude='backups/' \
--exclude='config/google_credentials.json' \
--exclude='config/google_token.pickle' \
--exclude='venv/' \
--exclude='__pycache__/' \
--exclude='*.pyc' \
--exclude='.env' \
--exclude='*.db' \
--exclude='*.db-journal' \
--exclude='*.md' \
--exclude='docs/' \
--exclude='memory/' \
--exclude='k8s/' \
--exclude='n8n-workflows/' \
--exclude='aiops-core/' \
--exclude='monitoring/alertmanager/' \
--exclude='._*' \
./ ollama@192.168.0.188:/home/ollama/momo-pro/ || true
# H2: 先 build 成功再短暫切換,避免 no-cache build 時間全變成 502
ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \
"cd /home/ollama/momo-pro && \
docker compose build --no-cache momo-app && \
(docker stop momo-pro-system momo-scheduler momo-telegram-bot 2>/dev/null || true) && \
(docker rm momo-pro-system momo-scheduler momo-telegram-bot 2>/dev/null || true) && \
docker compose up -d --no-deps --force-recreate momo-app scheduler telegram-bot && \
echo '✅ Image 重建完成(三容器)'"
- name: 重新載入監控設定
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
if echo "$CHANGED" | grep -qE '^(monitoring/prometheus\.yml|monitoring/blackbox\.yml)$'; then
ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \
"cd /home/ollama/momo-pro/monitoring && \
docker compose up -d prometheus blackbox-exporter && \
docker compose restart prometheus blackbox-exporter && \
echo '✅ Monitoring 設定已重新載入'"
else
echo "ℹ️ Monitoring 設定未變更,略過重新載入"
fi
# ── 健康檢查(H3: HTTP + 三容器 + Phase 34 SPA Shadow 偵測) ──────────
- name: 健康檢查
env:
COMMIT_SHA: ${{ steps.commit.outputs.short_sha }}
EXPECTED_VERSION: ${{ steps.commit.outputs.version }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
echo "⏳ 等待服務啟動(30s)..."
sleep 30
for i in $(seq 1 12); do
INTERNAL_CODE=$(ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \
"docker exec momo-pro-system curl -s -o /dev/null -w '%{http_code}' --max-time 8 http://127.0.0.1:80/health" 2>/dev/null || true)
EXTERNAL_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://mo.wooo.work/health --max-time 10 2>/dev/null || true)
INTERNAL_CODE=${INTERNAL_CODE:-000}
EXTERNAL_CODE=${EXTERNAL_CODE:-000}
if [ "$INTERNAL_CODE" = "200" ] && [ "$EXTERNAL_CODE" = "200" ]; then
echo "✅ HTTP 健康檢查通過(internal=$INTERNAL_CODE, external=$EXTERNAL_CODE)"
break
fi
echo "⏳ 嘗試 $i/12,internal=$INTERNAL_CODE external=$EXTERNAL_CODE,等待 15s..."
[ "$i" -eq 12 ] && echo "❌ HTTP 健康檢查失敗" && exit 1
sleep 15
done
EXTERNAL_HEALTH=$(curl -fsS --max-time 10 https://mo.wooo.work/health)
EXTERNAL_VERSION=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('version',''))" <<< "$EXTERNAL_HEALTH")
if [ "$EXTERNAL_VERSION" != "$EXPECTED_VERSION" ]; then
echo "❌ 正式版本未更新:expected=$EXPECTED_VERSION actual=$EXTERNAL_VERSION"
echo "$EXTERNAL_HEALTH"
exit 1
fi
echo "✅ 正式版本驗證通過:$EXTERNAL_VERSION"
# 驗證三應用容器均在 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'
# ── Phase 34 SPA Shadow 偵測 — 防 nginx fallback 偽綠 ───────────────
# 過去 5 個 deploy(run 273-277)全 success 但 prod 上 Flask 從未接到請求,
# 因為 nginx 對 /admin/* 等路徑全 fallback 到 SPA index.html(7480 byte)。
# 三條 fingerprint 任一不符 SPA = Flask 真接到:
# (a) Content-Length != 7480
# (b) etag != e167a58a1baf907f55a2925a2e8665d1
# (c) x-process-time header 存在(Flask middleware)
echo "🔍 SPA Shadow 偵測(驗 Flask 真接到請求)..."
SPA_ETAG='e167a58a1baf907f55a2925a2e8665d1'
SPA_LEN='7480'
# 用 /health(純 Flask,不會被 SPA 路徑攔)做基準探針
HDR=$(curl -sS -D - -o /dev/null --max-time 10 https://mo.wooo.work/health 2>/dev/null || echo "")
# P37: grep 沒匹配返回 1,pipefail+set -e 會殺整段腳本 — 全部加 || true
ETAG=$(echo "$HDR" | grep -i '^etag:' 2>/dev/null | tr -d '"\r' | awk '{print $2}' | tr 'A-Z' 'a-z' || true)
CLEN=$(echo "$HDR" | grep -i '^content-length:' 2>/dev/null | awk '{print $2}' | tr -d '\r' || true)
XPT=$(echo "$HDR" | grep -i '^x-process-time:' 2>/dev/null | awk '{print $2}' | tr -d '\r' || true)
FLASK_OK=0
# P36 修:用 if/then 而非 && 串連,避免 bash -e 在第一條 false 就 exit
if [ -n "$XPT" ] && [ "$XPT" != "0" ] && [ "$XPT" != "0.0" ]; then FLASK_OK=1; fi
if [ -n "$ETAG" ] && [ "$ETAG" != "$SPA_ETAG" ]; then FLASK_OK=1; fi
if [ -n "$CLEN" ] && [ "$CLEN" != "$SPA_LEN" ]; then FLASK_OK=1; fi
if [ "$FLASK_OK" != "1" ]; then
echo "❌ SPA Shadow 偵測:/health 看似 200 但 nginx fallback 攔截"
echo " etag=$ETAG (SPA=$SPA_ETAG)"
echo " content-length=$CLEN (SPA=$SPA_LEN)"
echo " x-process-time=$XPT (Flask 應 > 0)"
# Telegram 告警(如 secrets 已設)
if [ -n "${TELEGRAM_BOT_TOKEN}" ] && [ -n "${TELEGRAM_CHAT_ID}" ]; then
MSG="🚨 EwoooC SPA Shadow 偵測警告%0A├ commit ${COMMIT_SHA}%0A├ /health 被 nginx SPA fallback 攔截%0A└ 立即查 nginx upstream / Flask container"
curl -sS -m 5 -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" -d "text=${MSG}" -d "parse_mode=HTML" >/dev/null || true
fi
exit 1
fi
echo "✅ SPA Shadow OK — Flask 真接到請求(etag=$ETAG xpt=$XPT clen=$CLEN)"
- name: AI 觀測台 Post-deploy Production QA
if: success() && steps.observability_qa.outputs.needed == 'true'
run: |
bash ./scripts/quick_review.sh --observability-smoke --base-url https://mo.wooo.work --timeout 12
# ── 觸發 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)$' | \
jq -Rs '[split("\n")[] | select(. != "")]')
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: ${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 - START_TIME))
COMMIT_ESC=$(printf '%s' "$COMMIT_MSG" | sed 's/&/\&/g; s/\</g; s/>/\>/g')
MSG=$(printf '✅ EwoooC 部署成功\n├ 📝 %s\n├ 🔖 %s\n├ ⏱ 耗時 %ss\n└ 🌐 https://mo.wooo.work' \
"${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 "$TG_CHAT" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')"
# ── H4: 緊急回滾嘗試(部署失敗時嘗試起回三容器回復服務)───────────────
- name: 緊急回滾嘗試
if: failure()
run: |
echo "⚠️ 部署失敗,嘗試 compose up -d 回復三容器..."
# 同樣用 up -d 而非 restart,對「不存在的容器」也能起回來
ssh -i ~/.ssh/id_deploy ollama@192.168.0.188 \
"cd /home/ollama/momo-pro && \
docker compose up -d --no-deps momo-app scheduler 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; s/>/\>/g')
MSG=$(printf '❌ EwoooC 部署失敗\n├ 📝 %s\n├ 🔖 %s\n└ 🔍 請查看 Gitea Actions 日誌' \
"${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 "$TG_CHAT" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML"}')"