Skip to content

fix(watchdog): use a scope the token actually has, and label watchdog faults - #2542

Merged
Hugo0 merged 1 commit into
mainfrom
fix/watchdog-degraded-state
Jul 28, 2026
Merged

fix(watchdog): use a scope the token actually has, and label watchdog faults#2542
Hugo0 merged 1 commit into
mainfrom
fix/watchdog-degraded-state

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Second fix for the same watchdog. It is still false-alarming to Discord every 15 min — this stops it for real.

What went wrong with fix #1

#2541 replaced the SHA comparison with a check on the mirror workflow's last run status. Correct question, wrong API:

gh: Resource not accessible by personal access token (HTTP 403)

MONO_READ_TOKEN has contents:read only — it 403s on actions:read. The step shell is bash -e, so the unguarded failure aborted mid-script and surfaced as a bare red run, indistinguishable from a real content alert. I had dry-run it with my token, which has both scopes. Same class of mistake as verifying MDX through the wrong compiler.

The fix

Answer the same question using only contents:read — walk back through mono's recent content/ commits and take the newest that touched a path the mirror actually publishes (content/** minus _system/, plus _system/generated/), then compare that to the mirror: sync from mono@<sha> stamp.

Verified against live history — note it skipping exactly the commits that caused the original false alarm:

  d9a33de published-path: <none, skip>      # content/_system/ARCHITECTURE.md
  539b81f published-path: <none, skip>      # content/_system/ only
  0b0740a published-path: content/countries/nigeria/en.md
expected mono@0b0740a | mirrored @0b0740a | peanut-content c268e5d | live c268e5d
VERDICT: CURRENT — silent

Hardening from the same incident

  • Every API call is || true. Under bash -e an unguarded non-zero aborts the script at that line, and the operator gets a red run with no message explaining it.
  • "Cannot evaluate" is now its own labelled stateWATCHDOG DEGRADED — instead of looking identical to "content is stuck". A watchdog that can't watch is still worth waking someone for, but not for the wrong reason. This is the failure mode that just cost two debugging cycles.

Honest residual risk

I still cannot execute this with MONO_READ_TOKEN itself. What I can say: repos/mono/commits?path=content is proven working with that token (it succeeded in the first live run), and repos/mono/commits/{sha} is the same contents:read family. If it 403s anyway, it now reports WATCHDOG DEGRADED rather than a misleading content alert.

Summary by CodeRabbit

  • Bug Fixes
    • Improved watchdog reliability when GitHub API requests fail or are unavailable.
    • Added clearer distinction between a degraded watchdog check and genuinely stalled content.
    • Updated mirror health checks to compare expected published changes with the mirrored content.
    • Prevented missing timestamps or publish/CI status data from terminating watchdog evaluations.
    • More accurately flags the mirror as stale when it falls behind published changes.

… faults

The first fix traded one false alarm for another. Checking the mirror's run
status via the Actions API is the natural question to ask, but MONO_READ_TOKEN
carries contents:read only and 403s on actions:read — so the step died mid-run
under `bash -e` and surfaced as a bare red run, indistinguishable from a real
content alert. I had dry-run it with my own token, which has both scopes.

Answer the same question with contents:read: walk back through mono's recent
`content/` commits and take the newest one that touched a path the mirror
actually publishes (content/** minus _system/, plus _system/generated/), then
compare that against the "mirror: sync from mono@<sha>" stamp. Verified against
live history — it skips d9a33de and 539b81f, both content/_system/ only, and
lands on 0b0740a, which matches the stamp. Silent, correctly.

Two hardening changes from the same incident:

Every API call is now `|| true`. The step shell is `bash -e`, so any unguarded
non-zero aborts the script at that line and the operator sees a red run with no
message explaining it.

"Cannot evaluate" is now its own labelled state — WATCHDOG DEGRADED — rather
than looking identical to "content is stuck". A watchdog that cannot watch is
still worth waking someone for, but not for the wrong reason.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Building Building Preview, Comment Jul 28, 2026 10:55am

Request Review

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The content pipeline watchdog now tolerates GitHub API failures, compares the latest publishable mono commit with the mirror tip, marks unevaluable checks as degraded, and hardens age, publish PR, and CI metadata handling.

Changes

Content pipeline watchdog hardening

Layer / File(s) Summary
Fault-tolerant metadata collection
.github/workflows/content-pipeline-watchdog.yml
Production and mirror metadata requests are guarded so API failures do not terminate the watchdog under bash -e.
Expected-versus-mirrored evaluation
.github/workflows/content-pipeline-watchdog.yml
The workflow derives the newest mono commit affecting published paths, compares it with the mirror tip commit, and distinguishes degraded evaluation from mirror staleness.
Grace-period and CI gate handling
.github/workflows/content-pipeline-watchdog.yml
Age calculation defaults missing timestamps to the current time, while publish PR and check-run lookups are made non-fatal.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Watchdog
  participant MonoRepository
  participant PeanutContent
  participant GitHubAPI
  Watchdog->>MonoRepository: scan recent commits affecting published paths
  MonoRepository-->>Watchdog: return expected mono commit
  Watchdog->>PeanutContent: read mirror tip commit metadata
  PeanutContent-->>Watchdog: return mirrored mono commit
  Watchdog->>GitHubAPI: fetch publish PR and CI metadata
  GitHubAPI-->>Watchdog: return available metadata or failure
  Watchdog->>Watchdog: set staleness, degraded, and gate state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR does not implement the linked wagmi-to-ethers signer conversion or wallet/link-generation fixes. Update the PR to address the linked signer-conversion requirements, or relink it to the watchdog issue it actually changes.
Out of Scope Changes check ⚠️ Warning The watchdog-only code changes are unrelated to the directly linked SDK signer-conversion issue. Move these watchdog changes to a separate PR or replace them with the linked wallet/signer compatibility work.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the watchdog fault-handling changes in this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/watchdog-degraded-state

Comment @coderabbitai help to get the list of available commands.

@Hugo0
Hugo0 merged commit d439dd4 into main Jul 28, 2026
18 of 22 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6309.47 → 6309.47 (0)
Findings: 0 net (+0 new, -0 resolved)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
.github/workflows/content-pipeline-watchdog.yml (1)

59-64: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consolidate 3 calls to the same commit into 1.

TIP, TIP_MSG, and TIP_AT all hit repos/peanutprotocol/peanut-content/commits/main separately. Besides the extra round-trips, each is an independent || true failure point — e.g. TIP can succeed while TIP_MSG transiently fails, pushing the run into DEGRADED even though the data was actually available a moment earlier.

♻️ Proposed consolidation
-                  TIP=$(GH_TOKEN=$CONTENT_TOKEN gh api repos/peanutprotocol/peanut-content/commits/main --jq '.sha' || true)
-                  TIP_MSG=$(GH_TOKEN=$CONTENT_TOKEN gh api repos/peanutprotocol/peanut-content/commits/main --jq '.commit.message' 2>/dev/null | head -1 || true)
-                  TIP_AT=$(GH_TOKEN=$CONTENT_TOKEN gh api repos/peanutprotocol/peanut-content/commits/main --jq '.commit.committer.date' || true)
+                  TIP_JSON=$(GH_TOKEN=$CONTENT_TOKEN gh api repos/peanutprotocol/peanut-content/commits/main || true)
+                  TIP=$(printf '%s' "$TIP_JSON" | jq -r '.sha // empty')
+                  TIP_MSG=$(printf '%s' "$TIP_JSON" | jq -r '.commit.message // empty' | head -1)
+                  TIP_AT=$(printf '%s' "$TIP_JSON" | jq -r '.commit.committer.date // empty')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/content-pipeline-watchdog.yml around lines 59 - 64,
Consolidate the three gh api requests in the hop-2 watchdog block into one
request for the main commit, storing its JSON response once and deriving TIP,
TIP_MSG, and TIP_AT from that shared response. Preserve the existing empty-value
and failure-tolerant behavior while eliminating the independent calls and
failure points.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/content-pipeline-watchdog.yml:
- Around line 81-88: Update the per-commit fetch logic in the SHA loop so a
failed `gh api` request for commit file names is distinguished from a successful
response containing no published paths. Track the request status separately, and
mark the watchdog as degraded or otherwise stop normal expected-SHA selection
when fetching fails instead of continuing to an older SHA; preserve the existing
behavior for successful commits with no published paths.
- Around line 131-134: Update the workflow permissions to grant checks: read
alongside the existing UI_TOKEN permissions, and guard the check-runs gh api
invocation in the CI assignment with || true so lookup failures do not abort the
watchdog step.

---

Nitpick comments:
In @.github/workflows/content-pipeline-watchdog.yml:
- Around line 59-64: Consolidate the three gh api requests in the hop-2 watchdog
block into one request for the main commit, storing its JSON response once and
deriving TIP, TIP_MSG, and TIP_AT from that shared response. Preserve the
existing empty-value and failure-tolerant behavior while eliminating the
independent calls and failure points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6c12a06-e932-4e40-822c-a75f0a95d75e

📥 Commits

Reviewing files that changed from the base of the PR and between caa01b3 and cec65f7.

📒 Files selected for processing (1)
  • .github/workflows/content-pipeline-watchdog.yml

Comment on lines +81 to +88
for SHA in $(GH_TOKEN=$MONO_TOKEN gh api \
"repos/peanutprotocol/mono/commits?path=content&per_page=15" --jq '.[].sha' || true); do
FILES=$(GH_TOKEN=$MONO_TOKEN gh api "repos/peanutprotocol/mono/commits/$SHA" --jq '.files[].filename' || true)
# Published: content/** except content/_system/**, plus content/_system/generated/**
PUBLISHED=$( { printf '%s\n' "$FILES" | grep '^content/' | grep -v '^content/_system/'
printf '%s\n' "$FILES" | grep '^content/_system/generated/'; } | head -1 )
if [ -n "$PUBLISHED" ]; then EXPECTED="$SHA"; break; fi
done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Per-commit fetch failure inside the loop is misclassified as "not published".

If gh api .../commits/$SHA --jq '.files[].filename' fails transiently for the actual newest publish-touching commit, FILES comes back empty via || true and the loop just moves on to an older SHA, silently setting EXPECTED to a stale commit. If the mirror already synced past that point, this produces exactly the false "mirror is behind mono" alert this PR is trying to eliminate — just one layer deeper. The top-level DEGRADED check (line 98) can't catch this because EXPECTED is non-empty, just wrong.

🐛 Proposed fix — distinguish fetch failure from "no published paths"
                   EXPECTED=""
+                  FETCH_FAILED=false
                   for SHA in $(GH_TOKEN=$MONO_TOKEN gh api \
                         "repos/peanutprotocol/mono/commits?path=content&per_page=15" --jq '.[].sha' || true); do
-                    FILES=$(GH_TOKEN=$MONO_TOKEN gh api "repos/peanutprotocol/mono/commits/$SHA" --jq '.files[].filename' || true)
+                    if ! FILES=$(GH_TOKEN=$MONO_TOKEN gh api "repos/peanutprotocol/mono/commits/$SHA" --jq '.files[].filename'); then
+                      FETCH_FAILED=true
+                      break
+                    fi
                     # Published: content/** except content/_system/**, plus content/_system/generated/**
                     PUBLISHED=$( { printf '%s\n' "$FILES" | grep '^content/' | grep -v '^content/_system/'
                                    printf '%s\n' "$FILES" | grep '^content/_system/generated/'; } | head -1 )
                     if [ -n "$PUBLISHED" ]; then EXPECTED="$SHA"; break; fi
                   done
+                  [ "$FETCH_FAILED" = true ] && EXPECTED=""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for SHA in $(GH_TOKEN=$MONO_TOKEN gh api \
"repos/peanutprotocol/mono/commits?path=content&per_page=15" --jq '.[].sha' || true); do
FILES=$(GH_TOKEN=$MONO_TOKEN gh api "repos/peanutprotocol/mono/commits/$SHA" --jq '.files[].filename' || true)
# Published: content/** except content/_system/**, plus content/_system/generated/**
PUBLISHED=$( { printf '%s\n' "$FILES" | grep '^content/' | grep -v '^content/_system/'
printf '%s\n' "$FILES" | grep '^content/_system/generated/'; } | head -1 )
if [ -n "$PUBLISHED" ]; then EXPECTED="$SHA"; break; fi
done
EXPECTED=""
FETCH_FAILED=false
for SHA in $(GH_TOKEN=$MONO_TOKEN gh api \
"repos/peanutprotocol/mono/commits?path=content&per_page=15" --jq '.[].sha' || true); do
if ! FILES=$(GH_TOKEN=$MONO_TOKEN gh api "repos/peanutprotocol/mono/commits/$SHA" --jq '.files[].filename'); then
FETCH_FAILED=true
break
fi
# Published: content/** except content/_system/**, plus content/_system/generated/**
PUBLISHED=$( { printf '%s\n' "$FILES" | grep '^content/' | grep -v '^content/_system/'
printf '%s\n' "$FILES" | grep '^content/_system/generated/'; } | head -1 )
if [ -n "$PUBLISHED" ]; then EXPECTED="$SHA"; break; fi
done
[ "$FETCH_FAILED" = true ] && EXPECTED=""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/content-pipeline-watchdog.yml around lines 81 - 88, Update
the per-commit fetch logic in the SHA loop so a failed `gh api` request for
commit file names is distinguished from a successful response containing no
published paths. Track the request status separately, and mark the watchdog as
degraded or otherwise stop normal expected-SHA selection when fetching fails
instead of continuing to an older SHA; preserve the existing behavior for
successful commits with no published paths.

Comment on lines 131 to 134
if [ -n "$NUM" ]; then
SHA=$(GH_TOKEN=$UI_TOKEN gh api "repos/$REPO/pulls/$NUM" --jq '.head.sha')
CI=$(GH_TOKEN=$UI_TOKEN gh api "repos/$REPO/commits/$SHA/check-runs?per_page=100" \
PR_SHA=$(GH_TOKEN=$UI_TOKEN gh api "repos/$REPO/pulls/$NUM" --jq '.head.sha' || true)
CI=$(GH_TOKEN=$UI_TOKEN gh api "repos/$REPO/commits/$PR_SHA/check-runs?per_page=100" \
--jq '[.check_runs[] | select(.name=="ci-success")] | if length == 0 then "pending" elif all(.conclusion == "success") then "green" else "RED" end')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '--- workflow excerpt ---\n'
nl -ba .github/workflows/content-pipeline-watchdog.yml | sed -n '1,170p'

printf '\n--- search for checks permission / check-runs usage ---\n'
rg -n 'checks:|check-runs|ci-success|permissions:' .github/workflows/content-pipeline-watchdog.yml

Repository: peanutprotocol/peanut-ui

Length of output: 253


🏁 Script executed:

set -euo pipefail

echo '--- workflow excerpt ---'
nl -ba .github/workflows/content-pipeline-watchdog.yml | sed -n '1,170p'

echo
echo '--- search for checks permission / check-runs usage ---'
rg -n 'checks:|check-runs|ci-success|permissions:' .github/workflows/content-pipeline-watchdog.yml

Repository: peanutprotocol/peanut-ui

Length of output: 227


🏁 Script executed:

set -euo pipefail

echo '--- workflow excerpt with line numbers ---'
awk 'NR>=1 && NR<=170 { printf "%4d  %s\n", NR, $0 }' .github/workflows/content-pipeline-watchdog.yml

echo
echo '--- search for checks permission / check-runs usage ---'
rg -n 'checks:|check-runs|ci-success|permissions:' .github/workflows/content-pipeline-watchdog.yml

Repository: peanutprotocol/peanut-ui

Length of output: 11401


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/content-pipeline-watchdog.yml')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 1 <= i <= 170:
        print(f"{i:4d}  {line}")
PY

Repository: peanutprotocol/peanut-ui

Length of output: 10921


🏁 Script executed:

set -euo pipefail

echo '--- workflow excerpt with line numbers ---'
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/content-pipeline-watchdog.yml')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 1 <= i <= 170:
        print(f"{i:4d}  {line}")
PY

echo
echo '--- search for checks permission / check-runs usage ---'
rg -n 'checks:|check-runs|ci-success|permissions:' .github/workflows/content-pipeline-watchdog.yml

Repository: peanutprotocol/peanut-ui

Length of output: 11401


Guard the check-runs lookup and add checks: read.
UI_TOKEN only has contents: read and pull-requests: read, so this call needs checks: read; without it, the lookup can fail and, because it isn’t wrapped in || true, abort the watchdog mid-step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/content-pipeline-watchdog.yml around lines 131 - 134,
Update the workflow permissions to grant checks: read alongside the existing
UI_TOKEN permissions, and guard the check-runs gh api invocation in the CI
assignment with || true so lookup failures do not abort the watchdog step.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2234 ran, 0 failed, 0 skipped, 37.4s

📊 Coverage (unit)

metric %
statements 61.7%
branches 45.1%
functions 51.0%
lines 62.1%
⏱ 10 slowest test cases
time test
3.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.5s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/utils/__tests__/demo-balance.test.ts › starts at the full balance on a fresh install and stamps a timestamp
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/demo-balance.test.ts › resetDemoBalance refills and restarts the TTL window
0.2s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant