Skip to content

fix(queue-monitor-liveness): drop the failed-audit-step carve-out - #222

Merged
kyle-sexton merged 1 commit into
mainfrom
fix/queue-monitor-liveness-green-on-success-contract
Jul 22, 2026
Merged

fix(queue-monitor-liveness): drop the failed-audit-step carve-out#222
kyle-sexton merged 1 commit into
mainfrom
fix/queue-monitor-liveness-green-on-success-contract

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

No related issue: companion to melodic-software/ci-runner#140 contract change

Summary

melodic-software/ci-runner#140 makes queued-job-monitor.yml green-on-success: a successful execution now reports success whether or not it detected queue strain, alerting instead by upserting a marker-deduped incident issue in ci-runner rather than failing its own scheduled run.

This workflow's liveness check hard-coded the old contract: a MONITOR_AUDIT_STEP ("Inspect queued jobs") carve-out treated a monitor run whose only failed step was the audit step as healthy=true, on the theory that the failure itself WAS the queue alert. Under the new contract there is no longer a step whose failure is an expected, healthy outcome — that same failure shape now means the monitor is genuinely broken (config drift, a GitHub API error, or a failure writing the incident issue), while the watchdog carve-out would suppress its own alert exactly when both the monitor and this watchdog need to fire.

  • Removes the MONITOR_AUDIT_STEP env var and the per-step-failure classification logic (the gh_read api .../jobs call that inspected which steps failed).
  • Simplifies the health check to: any completed run whose conclusion is not success is monitor breakage, full stop.
  • Updates the recovery guidance text and the header rationale comment, which both described the old failure-is-the-alert contract.

Safe to merge under the old queued-job-monitor.yml contract too, ahead of ci-runner#140: today, the monitor's audit-step failure would newly be classified as breakage and this watchdog would fire (correct-if-noisy for the transition window) rather than silently swallowing it as healthy. Per the team lead's merge sequencing, this PR merges first; ci-runner#140 re-arms after.

Test plan

  • actionlint .github/workflows/queue-monitor-liveness.yml — clean
  • zizmor .github/workflows/queue-monitor-liveness.yml — clean, no findings
  • typos .github/workflows/queue-monitor-liveness.yml — clean
  • Confirmed no other file in the repo references MONITOR_AUDIT_STEP (fully removed, no dangling reference)

Related

🤖 Generated with Claude Code

ci-runner#140 makes queued-job-monitor.yml green-on-success: a
successful execution reports success whether or not it detected queue
strain, alerting instead by upserting an incident issue in ci-runner
rather than failing its own run. Under the old contract, this
workflow's MONITOR_AUDIT_STEP carve-out treated a run whose only
failed step was "Inspect queued jobs" as healthy (that failure WAS the
queue alert). Under the new contract that same failure is genuine
breakage - config drift or a GitHub API error - and the carve-out would
suppress this watchdog's own alert exactly when both it and the
monitor need to fire. Any non-success conclusion of queued-job-monitor.yml
is now monitor breakage, full stop.

Companion to melodic-software/ci-runner#139/#140.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 16s —— View job


PR Review

  • Gather context and understand the request
  • Read the changed file
  • Review for correctness, security, and CLAUDE.md alignment
  • Post review findings

Verdict: Approve. This is a clean, well-reasoned simplification with no correctness, security, or style issues.


What changed

The old health check treated a failure conclusion specially: it made a second GitHub API call to inspect which steps failed, and classified a run whose only failure was the MONITOR_AUDIT_STEP ("Inspect queued jobs") as healthy=true — on the theory that the failure itself was the queue alert. All other non-success conclusions (cancelled, timed_out, etc.) were already treated as breakage via a separate branch.

The new check collapses both branches into one: any conclusion != "success" is breakage, full stop. The MONITOR_AUDIT_STEP env var and the per-step API call are removed entirely.


Correctness

Logic is sound. Under the new green-on-success contract, there is no longer a step whose failure is an expected outcome. Collapsing to a single != "success" branch is strictly correct and simpler. The removed broken_steps/unexpected detection path was load-bearing only for the old contract.

The conclusion value space is handled. The previous code's two-branch approach (failure → inspect steps; other non-success → "reached no verdict") mapped cleanly onto the old contract. The new single-branch approach maps cleanly onto the new one — failure, cancelled, timed_out, action_required, stale are all now correctly classified as breakage.

MONITOR_AUDIT_STEP is fully removed. No dangling references elsewhere in the repo (verified with grep). The env var block is clean.

Transition-window behavior is acceptable. The PR description is honest: during the window between this merge and ci-runner#140, if the old monitor fails at "Inspect queued jobs" (a real queue alert), this liveness check will now also fire. That's a brief noisy period, not a silent failure — strictly preferable to having this watchdog suppress its alert when both systems are trying to fire.

Error message is improved. The new message (completed as \${conclusion}`, not `success``) includes the actual conclusion value and doesn't rely on knowledge of what the audit step was, making incidents easier to triage.

Recovery guidance updated correctly. Adding "and the incident-issue write in ci-runner" to step 3 is accurate — under the new contract, a write failure is now a legitimate failure mode this liveness check covers.


Security

No security concerns:

  • Triggers unchanged (schedule, push, workflow_dispatch). No pull_request_target or workflow_run with secrets — the CLAUDE.md tripwire is not implicated.
  • Permissions unchanged: contents: read at workflow level; contents: read + issues: write at job level.
  • peter-evans/create-issue-from-file remains SHA-pinned at fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0 — unchanged.
  • The removed second gh_read api .../jobs call is gone, which is a mild security improvement: the job no longer needs read access to the per-run job list. (The token already had it, but dead code that makes API calls is better gone.)
  • No new credentials, no new echoes of tokens, no untrusted checkouts.

CLAUDE.md alignment

  • SHA pins: no new uses: introduced; existing pin is intact.
  • The new inline comment at lines 95–102 is a good "why" comment — it explains a non-obvious external contract, exactly the use case CLAUDE.md calls out.

No findings. Safe to merge.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 49s —— View job


Security Review

  • Gather context and read PR diff
  • Read changed file in full
  • Analyze for security vulnerabilities
  • Post findings

No security vulnerabilities found in this PR. This is the security-relevant analysis of what changed:

Attack-surface reduction (net positive)

The old code extracted step names from the ci-runner API and interpolated them into the problems string that became a GitHub issue body:

broken_steps="$(gh_read api "repos/${MONITOR_REPOSITORY}/actions/runs/${run_id}/jobs" \
  --jq '[.jobs[].steps[] | select(.conclusion == "failure") | .name] | unique | .[]')"
unexpected="$(grep -Fxv "$MONITOR_AUDIT_STEP" <<<"$broken_steps" || true)"
problems+="...(failed: $(tr '\n' ',' <<<"$unexpected" | sed 's/,$//')); ..."

Step names are developer-authored strings from workflow YAML files. They were flowing — without sanitization — into an issue body (markdown injection surface). The risk was minimal (org-controlled repo, issue body only, not a code-execution context), but it was the most user-influenced content in the pipeline.

The new code removes that entire branch. The only values now interpolated into problems are:

  • run_id — a GitHub-generated numeric integer; no injection potential
  • conclusion — a fixed GitHub enum (success / failure / cancelled / timed_out / action_required / stale / neutral); no special characters possible

Unchanged surfaces (no regression)

  • Trigger set is unchanged (schedule, push, workflow_dispatch). No pull_request_target or workflow_run introduced.
  • permissions: block unchanged (contents: read, issues: write).
  • The only ${{ }} expressions in run: blocks are secrets.GITHUB_TOKEN (line 55) and github.repository (line 151) — both passed as env: vars, not inline script interpolation. No change here.
  • peter-evans/create-issue-from-file remains SHA-pinned at fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0 — no regression.

Logic correctness note (not a security issue, but relevant to alert contract)

The PR description correctly identifies the transition-window behavior: under the old contract, merging this first means a legitimate failure-conclusion monitor run (the old alert shape) would now be classified as breakage and fire the watchdog. That is the acknowledged correct-if-noisy behavior; there is no security consequence to it.


Verdict: Clean. The simplification removes the most complex data-flow path (step-name extraction and interpolation) and replaces it with values that are structurally impossible to contain injection payloads.

@kyle-sexton
kyle-sexton merged commit 2b5331e into main Jul 22, 2026
41 checks passed
@kyle-sexton
kyle-sexton deleted the fix/queue-monitor-liveness-green-on-success-contract branch July 22, 2026 19:51
kyle-sexton added a commit to melodic-software/ci-runner that referenced this pull request Jul 22, 2026
…ting (#140)

Closes #139

## Summary

- `queued-job-monitor.yml` no longer alerts by failing its own scheduled
run. A successful execution is green regardless of what it detects;
detection upserts a marker-deduped incident issue per target owner in
this repository instead (`[Alert] Managed runner queue capacity —
<owner>`, with a hidden `<!--
ci-runner:queued-job-monitor:incident:<owner> -->` marker in the body).
Matching is marker-only, preserving the fleet precedent's deliberate "a
marker survives a retitle" property (`link-check.yml`,
`queue-monitor-liveness.yml`, `standards-sync-stuck-automerge-alert.yml`
in `ci-workflows`).
- **Spoofing hardening (`48b3a6a`):** candidate issues are restricted to
ones authored by this workflow's own `GITHUB_TOKEN` identity
(`github-actions[bot]`, verified live), and more than one own-authored
issue carrying the marker fails the run closed. Mirrors
`ci-workflows`#213.
- **Data-integrity hardening (`4dc449e`):** `upsertIncident` requires
`STUCK_JSON` to be explicit/valid/array-decoding. `findOpenIncident`
filters server-side to `labels: 'automated'` (load-bearing for dedup —
documented).
- **Cross-owner marker-injection — CRITICAL, root-caused
(`4c7adff`/`ad6c79c`):** `escapeMarkdownTableCell` HTML-entity-encodes
`<`/`>` so no `<!--`/`-->` sequence from untrusted monitored-repo
content can survive into the raw body text `findOpenIncident`
substring-searches. End-to-end tests prove all three failure modes
(adopt-overwrite, false-close, false-ambiguity) are prevented.
Independently confirmed by a hard-bounded micro-review.
- **Incident body size cap (`c02681d`, from Codex review):**
`renderStuckMarkdownTable` caps at 50 rows with a "...and N more, see
the workflow run" remainder note (linked via
`GITHUB_SERVER_URL`/`GITHUB_RUN_ID`). `boundBodyLength` is a
defense-in-depth backstop truncating the assembled body under GitHub's
empirically-observed 65536-character write limit while always preserving
the trailing marker intact.
- Two further Codex findings addressed by rebuttal, not code change
(both resolved on the PR with reasoning): the `automated` label is never
applied to a monitored repo — `upsertIncident` creates issues
exclusively in `homeOwner`/`homeRepo`, derived from `GITHUB_REPOSITORY`
(this repo, ci-runner), never a monitored target; and the
marker-injection finding was already fixed by the escape-at-source
commit above (reply cites the commit, the tests, and the micro-review
verdict).
- A genuine execution error (bad configuration, GitHub API failure,
ambiguous marker match, malformed/missing `STUCK_JSON`) still fails the
run. The "Upsert queue capacity incident" step runs only on the default
`success()` condition.
- Writing the incident issue uses the job's own default `GITHUB_TOKEN`
(job-level `issues: write`), separate from the read-only, target-scoped
observer App token used to inspect queued jobs.
- `docs/queue-monitor.md` updated throughout to describe the current
design.

**Config-integrity note (documented in-file):** two
`CI_RUNNER_MONITOR_TARGETS_JSON` entries sharing an owner would race the
incident-issue upsert intra-run (the concurrency group only serializes
across scheduled runs, not the matrix within one).

**Companion PR:** melodic-software/ci-workflows#222 — **MERGED**.

**Environment note:** `ci-runner` is public, so its `ubuntu-24.04`
runner is free/unmetered, unaffected by the org's $0 spending cap. This
monitor is deliberately kept hosted, off the managed self-hosted fleet,
so it still reports when both self-hosted hosts are down.

## Test plan

- `node --test .github/scripts/queue-monitor.test.cjs` — 37/37 pass.
- `typos`, `npx markdownlint-cli2`, `ec` (editorconfig-checker) run
clean against every changed file.
- `node --test .github/scripts/*.test.cjs` (full suite): all other
suites pass; 5 pre-existing failures in
`verify-existing-release.test.cjs` are unrelated Windows-only temp-dir
`EPERM` cleanup issues, reproduced identically on `main`.
- Full fleet CI green through `ad6c79c`; `c02681d` re-running the same
gates now.
- All three Codex review threads replied to with evidence and resolved;
zero unresolved threads remain.

## Related

- Closes #139
- References: medley `docs/topics/cicd-fleet-audit/REPORT.md` (Theme 1
F6 + Status-quo BP4)
- Precedent mirrored: `melodic-software/ci-workflows`
`queue-monitor-liveness.yml`, `link-check.yml`,
`standards-sync-stuck-automerge-alert.yml` / ci-workflows#213
- Companion: melodic-software/ci-workflows#222 (merged)
- ci-runner#101 (off-GitHub dead-man's-switch — complementary)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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