Skip to content

feat(doctor): runner-health check (ADR-0005 AC4) — Closes #225 - #231

Merged
dngioidev merged 1 commit into
mainfrom
feat/225-doctor-runner-health
Jul 23, 2026
Merged

feat(doctor): runner-health check (ADR-0005 AC4) — Closes #225#231
dngioidev merged 1 commit into
mainfrom
feat/225-doctor-runner-health

Conversation

@dngioidev

Copy link
Copy Markdown
Owner

What

forge:doctor now reports local self-hosted-runner health (ADR-0005 decision 1, AC4). The check is gated on runner.enabled in the loaded config — when the runner feature is off (block absent or enabled:false), doctor emits nothing for it (no noise for the majority who don't run a local runner).

Scope is only AC4 — no changes to the #224 scaffold or the #226 forge.json runner schema.

AC4 → behaviour → test

AC4 requirement Behaviour Test (tests/doctor.test.mjs)
Silent / n-a when feature off no runner/runner-secret results feature off (no runner block) → absent; runner.enabled:false → still silent
Enabled + registered + online → healthy runner = ok, probed via gh api repos/{o}/{r}/actions/runners matching configured labels enabled + a matching runner online → ok
Enabled + offline runner runner = warn enabled + matching runner OFFLINE → warn
Enabled + missing runner runner = warn enabled + NO matching runner registered → warn
sharing:"org" → org endpoint queries orgs/{owner}/actions/runners sharing:org → queries the ORG runners endpoint
Degrade gracefully on scope/403/404 runner = warn, no crash gh api lacks scope / 403 → degrades to warn
Secret store gitignored + untracked runner-secret = ok / warn (git ls-files + check-ignore) store present but runner.env NOT gitignored → warn
Secret store tracked → fail runner-secret = fail secret store TRACKED in git → FAIL
PAT in a committed file → fail runner-secret = fail (git grep, filenames only) PAT-looking secret in a committed file → FAIL
Private-only reminder: public + enabled runner = FAIL with fork-PR RCE message enabled on a PUBLIC repo → FAIL

Plus hardening from review: secret-store scan is gh-independent and always runs even when the gh repo view probe fails (secret-store scan is git-only and still runs when gh repo view fails), and case-insensitive label matching (FORGE-LOCAL vs forge-local). 12 new cases; 21 in the file; 418 repo-wide.

Security / injection

  • Every gh/git call is argv-only (shell:false via lib/exec.mjs) — no shell-string interpolation; owner/name/endpoint are single argv tokens.
  • Doctor output never echoes a secret valuegit grep -l lists filenames only; all messages are filenames/counts/fixed strings.
  • RUNNER_PAT_RE is assembled from string parts so it never self-matches doctor's own source.
  • Independent forge:reviewer + forge:security passes run over the diff. Security: pass. Reviewer majors addressed (secret-scan now unconditional; case-insensitive labels; per_page=100).

Deferred (called out honestly)

ADR-0005 decision 1 also lists a chmod-600 assertion on ~/.forge/runner.env. It is not included here: the store is a POSIX home-dir file outside the repo, a no-op on the owner's native-Windows box, and typically absent at doctor time (it lives on the runner host). The AC4 contract for this ticket scopes the secret-store assertion to gitignored + untracked + PAT-scan. Suggest a small follow-up ticket for a POSIX-only, best-effort perms check if wanted.

Verification

  • pnpm verify418 passed (local gate). ✅
  • claude plugin validate ./plugin --strictpassed. ✅
  • Git-based secret-store logic exercised end-to-end with real git (real repos/commits) on Windows in the test suite.
  • Hosted CI is red on an account Actions billing block (not a code defect) — local verify + validate are the green signal; billing untouched.

Closes #225
Refs #180

🤖 Generated with Claude Code

forge:doctor now reports local self-hosted-runner health, gated on
config runner.enabled — fully silent when the runner feature is off.

When enabled:
- private-only guard: FAIL on a public repo (fork-PR RCE), belt-and-
  suspenders with init's refusal (#224)
- registered + online probe via `gh api {repos|orgs}/.../actions/runners`
  (org endpoint when sharing:"org"), matching the configured labels
  case-insensitively; degrades to a warn (never a crash) on 403/404/scope
- secret-store assertion (decision 1): ~/.forge/runner.env gitignored +
  untracked (git ls-files / check-ignore) and no PAT-looking secret in a
  committed file (git grep); git-only, so it runs even when the gh probe
  can't (transient failure / public repo)

All gh/git calls are argv-only (no shell); only file names are ever
surfaced — a discovered secret's value is never echoed into output.

12 vitest cases map AC4: enabled+online->ok, offline/missing->warn,
feature off->absent, secret tracked/committed->fail, public+enabled->fail,
sharing:org endpoint, gh-scope degrade, case-insensitive labels.

Closes #225
Refs #180

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SATRHKa6mDHDuirhP6QuwL
@dngioidev
dngioidev merged commit 672cfd7 into main Jul 23, 2026
0 of 5 checks passed
@dngioidev
dngioidev deleted the feat/225-doctor-runner-health branch July 23, 2026 15:07
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.

Runner: doctor runner-health check (AC4)

1 participant