2026-07-18 17:40:05 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Health check for the heavy-duty org watcher. Exit 0=ok, 1=degraded, 2=down.
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
2026-07-22 23:23:33 +00:00
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
# shellcheck source=lib/common.sh
|
|
|
|
|
source "${SCRIPT_DIR}/lib/common.sh"
|
|
|
|
|
|
2026-07-18 17:40:05 +00:00
|
|
|
HEALTH_LOG="${LOG_DIR}/health.log"
|
|
|
|
|
mkdir -p "${LOG_DIR}"
|
|
|
|
|
|
|
|
|
|
NOW_EPOCH="$(date -u +%s)"
|
2026-07-22 23:23:33 +00:00
|
|
|
NOW_ISO="$(utc_now)"
|
2026-07-18 17:40:05 +00:00
|
|
|
STATUS="ok"
|
|
|
|
|
ISSUES=()
|
|
|
|
|
|
|
|
|
|
ok() { ISSUES+=("OK: $1"); }
|
|
|
|
|
warn() { STATUS="degraded"; ISSUES+=("WARN: $1"); }
|
|
|
|
|
fail() { STATUS="down"; ISSUES+=("FAIL: $1"); }
|
|
|
|
|
|
2026-07-22 23:23:33 +00:00
|
|
|
# --- auth ---
|
|
|
|
|
case "${FORGE_BACKEND}" in
|
|
|
|
|
github)
|
|
|
|
|
if command -v gh >/dev/null 2>&1 && gh api user --jq .login >/dev/null 2>&1; then
|
|
|
|
|
LOGIN="$(gh api user --jq .login 2>/dev/null || echo unknown)"
|
|
|
|
|
ok "gh authenticated as ${LOGIN}"
|
|
|
|
|
if [[ -n "${BOT_LOGIN}" && "${LOGIN}" != "${BOT_LOGIN}" ]]; then
|
|
|
|
|
warn "gh login ${LOGIN} != expected BOT_LOGIN ${BOT_LOGIN}"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
fail "gh not authenticated"
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
forgejo)
|
|
|
|
|
if command -v stoke >/dev/null 2>&1 && stoke auth status 2>/dev/null | grep -q 'Login:'; then
|
|
|
|
|
LOGIN="$(stoke auth status 2>/dev/null | awk '/^Login:/{print $2; exit}')"
|
|
|
|
|
ok "stoke authenticated as ${LOGIN:-unknown}"
|
|
|
|
|
else
|
|
|
|
|
fail "stoke not authenticated (FORGE_BACKEND=forgejo)"
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
esac
|
2026-07-18 17:40:05 +00:00
|
|
|
|
2026-07-22 23:23:33 +00:00
|
|
|
# --- tmux ---
|
2026-07-18 17:40:05 +00:00
|
|
|
ACTIVE_TMUX=""
|
2026-07-22 23:23:33 +00:00
|
|
|
if command -v tmux >/dev/null 2>&1; then
|
|
|
|
|
if tmux has-session -t "${TMUX_SESSION}" 2>/dev/null; then
|
|
|
|
|
ACTIVE_TMUX="${TMUX_SESSION}"
|
|
|
|
|
elif tmux has-session -t "${LEGACY_TMUX}" 2>/dev/null; then
|
|
|
|
|
ACTIVE_TMUX="${LEGACY_TMUX}"
|
|
|
|
|
warn "using legacy tmux session ${LEGACY_TMUX} (prefer ${TMUX_SESSION})"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
warn "tmux not installed"
|
2026-07-18 17:40:05 +00:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ -n "${ACTIVE_TMUX}" ]]; then
|
|
|
|
|
ok "tmux session ${ACTIVE_TMUX} exists"
|
|
|
|
|
if tmux list-windows -t "${ACTIVE_TMUX}" -F '#{window_name}' 2>/dev/null | grep -Eqx 'poll-loop'; then
|
|
|
|
|
ok "tmux window poll-loop present"
|
|
|
|
|
else
|
|
|
|
|
warn "tmux window poll-loop missing"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
fail "tmux session missing (run scripts/restore.sh)"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-22 23:23:33 +00:00
|
|
|
if process_running 'poll-loop\.sh'; then
|
2026-07-18 17:40:05 +00:00
|
|
|
ok "poll-loop.sh process running"
|
|
|
|
|
else
|
|
|
|
|
fail "poll-loop.sh process not running"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-22 23:23:33 +00:00
|
|
|
# --- state ---
|
2026-07-18 17:40:05 +00:00
|
|
|
if [[ -f "${STATE_FILE}" ]]; then
|
2026-07-22 23:23:33 +00:00
|
|
|
if command -v jq >/dev/null 2>&1; then
|
|
|
|
|
LAST_POLL="$(jq -r '.last_poll_at // empty' "${STATE_FILE}" 2>/dev/null || true)"
|
|
|
|
|
SCOPE="$(jq -r '.scope // .repo // "unknown"' "${STATE_FILE}" 2>/dev/null || echo unknown)"
|
|
|
|
|
ok "scope: ${SCOPE}"
|
|
|
|
|
if [[ -n "${LAST_POLL}" && "${LAST_POLL}" != "null" ]]; then
|
|
|
|
|
if LAST_EPOCH="$(date -u -d "${LAST_POLL}" +%s 2>/dev/null)"; then
|
|
|
|
|
AGE_MIN=$(( (NOW_EPOCH - LAST_EPOCH) / 60 ))
|
|
|
|
|
if (( AGE_MIN > STALE_POLL_MINUTES )); then
|
|
|
|
|
warn "last_poll_at is ${AGE_MIN}m old (${LAST_POLL})"
|
|
|
|
|
else
|
|
|
|
|
ok "last_poll_at fresh (${AGE_MIN}m ago: ${LAST_POLL})"
|
|
|
|
|
fi
|
2026-07-18 17:40:05 +00:00
|
|
|
else
|
2026-07-22 23:23:33 +00:00
|
|
|
ok "last_poll_at present (${LAST_POLL})"
|
2026-07-18 17:40:05 +00:00
|
|
|
fi
|
|
|
|
|
else
|
2026-07-22 23:23:33 +00:00
|
|
|
warn "state.json has no last_poll_at"
|
2026-07-18 17:40:05 +00:00
|
|
|
fi
|
2026-07-22 23:23:33 +00:00
|
|
|
SUMMARY="$(jq -r '.last_poll_summary // "n/a"' "${STATE_FILE}" 2>/dev/null || echo n/a)"
|
|
|
|
|
ok "last_poll_summary: ${SUMMARY}"
|
2026-07-18 17:40:05 +00:00
|
|
|
else
|
2026-07-22 23:23:33 +00:00
|
|
|
warn "jq missing; cannot parse state.json details"
|
2026-07-18 17:40:05 +00:00
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
fail "state.json missing"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
[[ -f "${WATCHER_DIR}/POLL_INSTRUCTIONS.md" ]] && ok "POLL_INSTRUCTIONS.md present" || fail "POLL_INSTRUCTIONS.md missing"
|
|
|
|
|
|
2026-07-22 23:23:33 +00:00
|
|
|
# --- write health JSON via jq (safe escaping) ---
|
2026-07-18 17:40:05 +00:00
|
|
|
RESULT_JSON="${LOG_DIR}/health-latest.json"
|
2026-07-22 23:23:33 +00:00
|
|
|
if command -v jq >/dev/null 2>&1; then
|
|
|
|
|
jq -n \
|
|
|
|
|
--arg t "${NOW_ISO}" \
|
|
|
|
|
--arg s "${STATUS}" \
|
|
|
|
|
--arg backend "${FORGE_BACKEND}" \
|
|
|
|
|
--arg org "${ORG}" \
|
|
|
|
|
--args \
|
|
|
|
|
'{
|
|
|
|
|
checked_at: $t,
|
|
|
|
|
status: $s,
|
|
|
|
|
backend: $backend,
|
|
|
|
|
org: $org,
|
|
|
|
|
issues: $ARGS.positional
|
|
|
|
|
}' -- "${ISSUES[@]}" > "${RESULT_JSON}"
|
|
|
|
|
else
|
|
|
|
|
echo "{\"checked_at\":\"${NOW_ISO}\",\"status\":\"${STATUS}\",\"issues\":[]}" > "${RESULT_JSON}"
|
|
|
|
|
fi
|
2026-07-18 17:40:05 +00:00
|
|
|
|
2026-07-22 23:23:33 +00:00
|
|
|
watcher_log "${HEALTH_LOG}" "status=${STATUS} | $(IFS=' ; '; echo "${ISSUES[*]}")"
|
2026-07-18 17:40:05 +00:00
|
|
|
|
|
|
|
|
if command -v jq >/dev/null 2>&1 && [[ -f "${STATE_FILE}" ]]; then
|
|
|
|
|
tmp="$(mktemp)"
|
|
|
|
|
jq --arg t "${NOW_ISO}" --arg s "${STATUS}" \
|
|
|
|
|
'.health = {last_check_at: $t, status: $s}' "${STATE_FILE}" > "${tmp}" \
|
|
|
|
|
&& mv "${tmp}" "${STATE_FILE}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "status=${STATUS}"
|
|
|
|
|
printf '%s\n' "${ISSUES[@]}"
|
|
|
|
|
case "${STATUS}" in
|
|
|
|
|
ok) exit 0 ;;
|
|
|
|
|
degraded) exit 1 ;;
|
|
|
|
|
*) exit 2 ;;
|
|
|
|
|
esac
|