-
Notifications
You must be signed in to change notification settings - Fork 44
206 lines (187 loc) · 11.1 KB
/
Copy pathcc-drift-watcher-liveness.yml
File metadata and controls
206 lines (187 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
name: CC drift watcher liveness
# Watches the watcher. The `cc-drift-template-watch.yml` workflow runs every
# 30 min on a self-hosted runner. If that runner goes offline — Hetzner
# reboot, container crash, OAuth credential revoked, CC binary missing,
# refresh failure — the watcher silently stops cycling and class-B drift
# goes uncaught. This alarm catches that case.
#
# Runs every 2 hours on a github-hosted runner (no auth, no self-hosted
# dependency — so it survives the exact failure modes it's meant to catch).
# Checks the most recent `success` run of cc-drift-template-watch.yml; if
# the latest success is more than 8 hours old (≥ 16 missed 30-min cycles),
# opens a `cc-watcher-liveness`-labeled issue. Auto-closes the issue when
# the watcher recovers.
#
# Cron offset to the :15 mark so it never overlaps with the 30-min cron
# of the watcher itself (:00 and :30) — though in practice GitHub Actions
# only fires the watcher's `*/30` schedule every ~4h on this repo (public
# free-tier scheduler is best-effort, not guaranteed). See the threshold
# rationale below.
on:
schedule:
- cron: '15 */2 * * *'
workflow_dispatch:
inputs:
force_threshold_hours:
description: 'Override THRESHOLD_HOURS for this run (validation only — leave blank for default 8h)'
required: false
type: string
default: ''
permissions:
contents: read
issues: write
jobs:
liveness:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
# v4.7.1: gh CLI commands below need a git context to resolve the
# repo (otherwise `gh issue list` exits with "fatal: not a git
# repository"). actions/checkout supplies it; we don't actually
# need any files from the checkout, just the .git directory.
- uses: askalf/checkout-with-retry@744195501c3e2b794c50370b753a7b8c93d084f5 # v1.0.0
with:
fetch-depth: 1
- name: Check watcher last-successful-run age
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Hours-since-last-success threshold for alarming. Originally
# 3h (= 6 missed 30-min cycles per the declared schedule), but
# v4.7.1 bumped to 8h after observing real-world cadence on
# this repo: GitHub Actions' free-tier cron scheduler is
# best-effort, and the watcher's `*/30 * * * *` typically fires
# every 2-4 hours in practice. 8h still catches real outages
# (anything > a day-shift of silence is signal, not noise)
# while absorbing the scheduler skew.
#
# v4.7.2: workflow_dispatch can override THRESHOLD_HOURS for
# validation runs. Lets us exercise the alert-open path on
# demand (set inputs.force_threshold_hours=1, dispatch, watch
# the issue open) without waiting for or synthesizing a real
# outage. Default empty means "use the hardcoded 8".
THRESHOLD_HOURS: ${{ github.event.inputs.force_threshold_hours != '' && github.event.inputs.force_threshold_hours || '8' }}
run: |
set -euo pipefail
# Self-heal: ensure the alarm label exists before any issue op.
# Idempotent — the `|| true` swallows "already exists" errors.
gh label create cc-watcher-liveness \
--description "Drift watcher has not had a successful run within liveness threshold" \
--color "B60205" 2>/dev/null || true
# Most recent successful run of the drift watcher. The drift
# watcher's workflow run is `success` for both exit 0 (no drift)
# AND exit 2 (drift detected) — only exit 1 (infra failure)
# surfaces as `failure`. So "success" here is "watcher cycled
# cleanly," which is what liveness means.
last_success=$(gh api \
"repos/${{ github.repository }}/actions/workflows/cc-drift-template-watch.yml/runs?status=success&per_page=1" \
--jq '.workflow_runs[0].created_at // ""')
if [ -z "$last_success" ]; then
echo "No successful watcher runs yet — likely a fresh repo or watcher disabled. Skipping alarm."
exit 0
fi
now=$(date -u +%s)
last=$(date -u -d "$last_success" +%s)
hours_since=$(( (now - last) / 3600 ))
echo "Last successful watcher run: $last_success ($hours_since hours ago, threshold ${THRESHOLD_HOURS}h)"
# Look up any existing open alert issue so we can close it on the
# healthy path. This lookup is non-blocking — a transient API
# failure (e.g. HTTP 401 during GH-Actions token provisioning race,
# observed 2026-05-23 at 11:36 UTC despite same workflow passing
# 90 min earlier) must NOT fail the whole liveness check, because
# the watcher's actual liveness was already verified above. We
# tolerate auth + transient errors and treat them as "no existing
# issue" — worst case is we don't close a stale alert this cycle
# (next cycle will retry).
existing=$(gh issue list --label cc-watcher-liveness --state open --json number --jq '.[0].number' 2>/dev/null) || existing=""
if [ "$hours_since" -lt "$THRESHOLD_HOURS" ]; then
echo "Watcher healthy."
if [ -n "$existing" ] && [ "$existing" != "null" ]; then
# Same tolerance on the close — if it fails for any transient
# reason, log a warning + continue. The issue will get closed
# on the next cycle.
if gh issue close "$existing" --comment "Watcher recovered: last successful run ${hours_since} hours ago (${last_success}). Closing this alert." 2>/dev/null; then
echo "Closed stale alert #$existing"
else
echo "::warning::Failed to close stale alert #$existing (transient API error); will retry next cycle"
fi
fi
exit 0
fi
# ============================================================
# Past threshold — try SELF-RECOVERY before notifying operator.
# (Move 6 of the 2026-05-23 dario absolute-minimal-human push.)
# The vast majority of "watcher offline" causes are recoverable
# by simply re-dispatching the watcher workflow: the most-common
# root cause is the watcher cron just got skipped by GitHub's
# free-tier scheduler. Try one workflow_dispatch + wait for the
# result before escalating to a GitHub issue.
# ============================================================
echo "::warning::cc-drift-template-watch.yml has no successful run in $hours_since hours (threshold: ${THRESHOLD_HOURS}h). Attempting self-recovery before notifying operator."
# Skip self-recovery on `workflow_dispatch` with force_threshold_hours
# set — that's the operator's deliberate "exercise the alert path"
# dry-run. Don't actually recover; just open the issue.
if [ -n "${{ github.event.inputs.force_threshold_hours }}" ]; then
echo "force_threshold_hours dry-run mode — skipping self-recovery, going straight to alert."
else
echo "[$(date -Iseconds)] Triggering cc-drift-template-watch.yml via workflow_dispatch..."
if gh workflow run cc-drift-template-watch.yml --ref master 2>/dev/null; then
echo "Dispatch sent. Waiting up to 120s for a fresh successful run..."
# Poll for a new successful run more recent than $last_success
recovered=false
for i in $(seq 1 12); do
sleep 10
latest=$(gh api "repos/${{ github.repository }}/actions/workflows/cc-drift-template-watch.yml/runs?status=success&per_page=1" --jq '.workflow_runs[0].created_at // ""' 2>/dev/null || echo "")
if [ -n "$latest" ] && [ "$latest" != "$last_success" ]; then
echo "[${i}0s] Watcher recovered — new successful run at $latest (was $last_success)"
recovered=true
break
fi
echo "[${i}0s] not yet; still showing $latest"
done
if [ "$recovered" = "true" ]; then
echo "Self-recovery succeeded. Skipping alert. Closing any stale alert issue."
if [ -n "$existing" ] && [ "$existing" != "null" ]; then
gh issue close "$existing" --comment "Watcher self-recovered via liveness-triggered dispatch. Closing this alert." 2>/dev/null || true
fi
exit 0
fi
echo "Self-recovery did NOT produce a fresh success within 120s. Escalating to operator alert."
else
echo "::warning::workflow_dispatch of cc-drift-template-watch.yml failed (gh API transient?). Escalating to operator alert."
fi
fi
body_file=$(mktemp)
{
echo "## CC drift watcher offline"
echo ""
echo "Generated by [\`cc-drift-watcher-liveness.yml\`](.github/workflows/cc-drift-watcher-liveness.yml) on $(date -Iseconds)."
echo ""
echo "**Last successful run of [\`cc-drift-template-watch.yml\`](.github/workflows/cc-drift-template-watch.yml): ${hours_since} hours ago** (\`${last_success}\`)."
echo ""
echo "Declared cadence: every 30 minutes (\`*/30 * * * *\`). Observed cadence on this repo: typically every 2-4 hours (GitHub Actions' free-tier cron scheduler is best-effort). Threshold for this alarm: ${THRESHOLD_HOURS} hours of silence."
echo ""
echo "### Likely causes"
echo ""
echo "- Self-hosted runner offline (Hetzner reboot, container restart, network issue)"
echo "- OAuth credential expired / revoked / refresh failed (look for 401 in the last run's logs)"
echo "- CC binary missing or upgraded incompatibly"
echo "- Repository or cron config changed"
echo ""
echo "### How to investigate"
echo ""
echo "1. Check runner status: [Settings → Actions → Runners](../../settings/actions/runners) — is \`askalf-platform-1\` showing **Online**?"
echo "2. Check recent runs: [Actions → CC template drift watch (self-hosted)](../../actions/workflows/cc-drift-template-watch.yml) — read the latest failure log"
echo "3. SSH to the box and probe auth: \`claude --print < /dev/null\` (shares /root/.claude/.credentials.json with the platform dario; platform dario container's /health should show \`oauth: healthy\`)"
echo "4. Re-run manually after fixing: \`gh workflow run cc-drift-template-watch.yml --ref master\`"
echo ""
echo "This alert auto-closes when the watcher next reports a successful run."
} > "$body_file"
if [ -n "$existing" ] && [ "$existing" != "null" ]; then
gh issue comment "$existing" --body-file "$body_file"
echo "Updated existing alert #$existing"
else
gh issue create \
--title "Watcher offline: no successful run in ${hours_since} hours" \
--label cc-watcher-liveness \
--body-file "$body_file"
fi