#!/usr/bin/env bash # runner-deadman —— CI runner 死信守望。CI 整条吊在一台 mac(mac-pangolin-2)上, # mac 一关/掉线 CI 全停且无人知道。本脚本放在【另一台常在线主机】上,周期查 gitea # Actions API 里该 runner 的 status,连续 offline 到阈值 → Telegram 告警;恢复也通知。 # # 仿 deploy/bootstrap/monitor/deadman-watch.sh(同一发信/去抖范式),探活对象从 # 「TCP 端口」换成「gitea runner 在线状态」。 # # 配置 /etc/pangolin-runner-deadman.env: # GITEA_URL=http://127.0.0.1:13000 # gitea 基址(经 relay 或直连) # GITEA_TOKEN=... # gitea 读写 key(Bitwarden「gitea 读写key」) # GITEA_REPO=wangjia/pangolin # owner/repo # RUNNER_NAME=mac-pangolin-2 # 要守望的 runner 名 # TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=... # FAIL_THRESHOLD=2 # 连续几次 offline 才告警(去抖) # # 在该常在线主机上每 3 分钟跑一次(cron,用户态即可): # */3 * * * * /usr/local/bin/runner-deadman >/dev/null 2>&1 # (mac 宿主可改用 launchd LaunchAgent,见 docs/ci-runner.md「韧性」节。) set -uo pipefail ENV_FILE=/etc/pangolin-runner-deadman.env [ -r "$ENV_FILE" ] || { echo "缺少 $ENV_FILE" >&2; exit 1; } # shellcheck disable=SC1090 . "$ENV_FILE" : "${FAIL_THRESHOLD:=2}" : "${GITEA_REPO:=wangjia/pangolin}" : "${RUNNER_NAME:=mac-pangolin-2}" STATE_DIR="${STATE_DIR:-$HOME/.local/state/pangolin-runner-deadman}" mkdir -p "$STATE_DIR" statef="${STATE_DIR}/${RUNNER_NAME}.fail" send_tg() { local text="$1" [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${TELEGRAM_CHAT_ID:-}" ] || { echo "$text"; return; } curl -fsS --max-time 15 \ "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \ --data-urlencode "text=${text}" >/dev/null 2>&1 || echo "Telegram 发送失败" >&2 } # 查 runner 状态。响应形如: # {"runners":[{"id":6,"name":"mac-pangolin-2","status":"online","busy":false,...}],...} resp=$(curl -fsS --max-time 15 -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_URL%/}/api/v1/repos/${GITEA_REPO}/actions/runners" 2>/dev/null) || { # API 取不到(监控主机网络抖动 / relay 没起 / gitea 宕):不计入去抖,避免误报。 echo "runner-deadman: gitea API 不可达,跳过本次(不改状态)" >&2 exit 0 } # 提取该 runner 的 status(优先 jq,缺 jq 退回 grep)。 if command -v jq >/dev/null 2>&1; then status=$(printf '%s' "$resp" | jq -r --arg n "$RUNNER_NAME" \ '.runners[]? | select(.name==$n) | .status' 2>/dev/null) else status=$(printf '%s' "$resp" \ | grep -oE "\"name\":\"${RUNNER_NAME}\"[^}]*\"status\":\"[a-z]+\"" \ | grep -oE '"status":"[a-z]+"' | tail -1 | cut -d'"' -f4) fi if [ "$status" = "online" ]; then if [ -f "$statef" ]; then send_tg "🟢 [CI runner ${RUNNER_NAME}] 恢复在线" rm -f "$statef" fi else # status 为 offline / 空(runner 已从 gitea 掉线或不存在)。 fails=$(( $(cat "$statef" 2>/dev/null || echo 0) + 1 )) echo "$fails" > "$statef" if [ "$fails" -eq "$FAIL_THRESHOLD" ]; then send_tg "🔴 [CI runner ${RUNNER_NAME}] 离线(status=${status:-缺失}),连续 ${fails} 次,CI 已停摆" fi fi