ci-queue-wait.sh classifier cannot block: python3 - heredoc consumes stdin, every payload classifies unknown, and unknown is an exit-0 arm (gate 6 is vacuous) #1198

Closed
opened 2026-08-13 10:48:33 +00:00 by coder-mos1 · 1 comment
Collaborator

Summary

ci-queue-wait.sh cannot block. Its classifier always returns unknown, and unknown is an
exit-0 arm. The pending arm is unreachable. The guard has never delayed a push or a merge, on
either platform, for any seat, since the classifier was written.

Constitution gate 6 — "Before any push or merge, run the CI queue guard" — is therefore
currently satisfied by a control that cannot fail. pr-merge.sh calls the same guard internally,
so every merge the fleet has made passed a vacuous check.

Root cause

get_state_from_status_json() is invoked as a pipeline stage:

STATE=$(printf '%s' "$STATUS_JSON" | get_state_from_status_json)

but the function body runs the interpreter with the program supplied on stdin:

python3 - <<'PY'
...
payload = json.load(sys.stdin)

python3 - reads its program from stdin, and the heredoc is stdin. The heredoc wins; the piped
STATUS_JSON is discarded and never reaches the process. json.load(sys.stdin) then reads a
stream already consumed to EOF, raises, and the handler does exactly what it was written to do:

except Exception:
    print("unknown")
    raise SystemExit(0)

unknown is then grouped with the terminal states:

terminal-success|terminal-failure|unknown)
    # Queue guard only blocks on pending/running/queued states.
    exit 0

Two correct-looking pieces with an unbound seam between them: the fetch works, the classifier
works, and nothing carries the payload from one to the other.

print_pending_contexts() has the identical construction and the same defect, which is why no
pending-context diagnostic has ever printed either.

Proved by construction against main, not read from the source

Re-verified 2026-08-13. The installed copy on web1 and the copy in main are byte-identical
(sha256 19cda2f7009c…), so this is a main defect and not install drift:

$ sed -n '/^get_state_from_status_json() {/,/^}/p' ci-queue-wait.sh > fn.sh && source fn.sh

$ echo '{"state":"pending","statuses":[{"status":"running","context":"ci/woodpecker/pr/ci"}]}' | get_state_from_status_json
unknown
$ echo '{"state":"failure","statuses":[{"status":"failure","context":"ci"}]}' | get_state_from_status_json
unknown
$ echo '{"state":"success","statuses":[{"status":"success","context":"ci"}]}' | get_state_from_status_json
unknown

A live running pipeline classifies as unknown and the guard proceeds.

Observed end to end against mosaicstack/stack, three consecutive runs, deterministic:

run1 rc=0 -> state=unknown
run2 rc=0 -> state=unknown
run3 rc=0 -> state=unknown

while the guard's own status URL, fetched with the guard's own curl invocation, token and
User-Agent, returns valid JSON with "state":"success" — which the classifier would have
correctly called terminal-success had it ever received it.

Why this matters beyond the one-line fix

The failure is invisible at the call site. The guard prints a confident, well-formed line —
[ci-queue-wait] state=unknown purpose=merge branch=main — and exits 0. Nothing in that output
distinguishes "I checked and the queue is clear" from "I could not read anything at all." The
word unknown is doing the work of both, and the exit code is identical either way.

This is the same class as the attribution defect (#1196) and the mosaic-worktree.sh SIGPIPE
abort: rc=0 was never a reading, and a control's own success line is not a reading either. Only
a provider readback is.

Proposed fix

  1. Feed the payload to the interpreter properly — pass the program as an argument (python3 -c)
    or via a process substitution / temp program file, so stdin stays free to carry the JSON.
    Apply to both get_state_from_status_json and print_pending_contexts.
  2. Do not leave unknown in the exit-0 arm. An unreadable status is not a clear queue.
    unknown should either block or exit non-zero for --purpose merge; the current grouping with
    terminal-success is what converted a parse failure into a green light.
  3. Distinguish the two roads to unknown in the output — "classifier saw states it does not
    recognise" and "payload did not parse" must not print the same line.
  4. Add a regression that would fail against the current tree: a constructed pending payload
    must classify as pending and the guard must not exit 0. Any test that only asserts the guard
    exits 0 on a clear queue passes on the broken tree and proves nothing — that is precisely how
    this survived.

Scope note

Split out of #1194. That issue was filed carrying this body under a title about the 38-tool
installed-framework drift; the two are unrelated defects and #1194's merged PR #1195 (drift
detection) does not touch this one. #1194 keeps the framework-refresh scope; this issue keeps the
classifier defect so it is not lost when #1194 closes.

Distinct from #1177 (ci-queue-wait.sh guards main by default, not the branch being pushed) —
that is a target-selection defect; this is a payload-delivery defect. Both are live.

Deliberately not folded into #1174 (workspace hygiene / tool enforcement) — that PR is under
adversarial review with open blockers, and expanding a reviewed PR's scope to carry an unrelated
fix is how review coverage gets lost.

## Summary `ci-queue-wait.sh` **cannot block**. Its classifier always returns `unknown`, and `unknown` is an exit-0 arm. The pending arm is unreachable. The guard has never delayed a push or a merge, on either platform, for any seat, since the classifier was written. Constitution gate 6 — *"Before any push or merge, run the CI queue guard"* — is therefore currently satisfied by a control that cannot fail. `pr-merge.sh` calls the same guard internally, so every merge the fleet has made passed a vacuous check. ## Root cause `get_state_from_status_json()` is invoked as a pipeline stage: STATE=$(printf '%s' "$STATUS_JSON" | get_state_from_status_json) but the function body runs the interpreter with the program supplied **on stdin**: python3 - <<'PY' ... payload = json.load(sys.stdin) `python3 -` reads its program from stdin, and the heredoc *is* stdin. The heredoc wins; the piped `STATUS_JSON` is discarded and never reaches the process. `json.load(sys.stdin)` then reads a stream already consumed to EOF, raises, and the handler does exactly what it was written to do: except Exception: print("unknown") raise SystemExit(0) `unknown` is then grouped with the terminal states: terminal-success|terminal-failure|unknown) # Queue guard only blocks on pending/running/queued states. exit 0 Two correct-looking pieces with an unbound seam between them: the fetch works, the classifier works, and nothing carries the payload from one to the other. `print_pending_contexts()` has the identical construction and the same defect, which is why no pending-context diagnostic has ever printed either. ## Proved by construction against `main`, not read from the source Re-verified 2026-08-13. The installed copy on web1 and the copy in `main` are byte-identical (sha256 `19cda2f7009c…`), so this is a `main` defect and not install drift: $ sed -n '/^get_state_from_status_json() {/,/^}/p' ci-queue-wait.sh > fn.sh && source fn.sh $ echo '{"state":"pending","statuses":[{"status":"running","context":"ci/woodpecker/pr/ci"}]}' | get_state_from_status_json unknown $ echo '{"state":"failure","statuses":[{"status":"failure","context":"ci"}]}' | get_state_from_status_json unknown $ echo '{"state":"success","statuses":[{"status":"success","context":"ci"}]}' | get_state_from_status_json unknown A live running pipeline classifies as `unknown` and the guard proceeds. Observed end to end against `mosaicstack/stack`, three consecutive runs, deterministic: run1 rc=0 -> state=unknown run2 rc=0 -> state=unknown run3 rc=0 -> state=unknown while the guard's own status URL, fetched with the guard's own `curl` invocation, token and User-Agent, returns valid JSON with `"state":"success"` — which the classifier would have correctly called `terminal-success` had it ever received it. ## Why this matters beyond the one-line fix The failure is invisible at the call site. The guard prints a confident, well-formed line — `[ci-queue-wait] state=unknown purpose=merge branch=main` — and exits 0. Nothing in that output distinguishes *"I checked and the queue is clear"* from *"I could not read anything at all."* The word `unknown` is doing the work of both, and the exit code is identical either way. This is the same class as the attribution defect (#1196) and the `mosaic-worktree.sh` SIGPIPE abort: `rc=0` was never a reading, and a control's own success line is not a reading either. Only a provider readback is. ## Proposed fix 1. Feed the payload to the interpreter properly — pass the program as an argument (`python3 -c`) or via a process substitution / temp program file, so stdin stays free to carry the JSON. Apply to **both** `get_state_from_status_json` and `print_pending_contexts`. 2. **Do not leave `unknown` in the exit-0 arm.** An unreadable status is not a clear queue. `unknown` should either block or exit non-zero for `--purpose merge`; the current grouping with `terminal-success` is what converted a parse failure into a green light. 3. Distinguish the two roads to `unknown` in the output — "classifier saw states it does not recognise" and "payload did not parse" must not print the same line. 4. **Add a regression that would fail against the current tree:** a constructed pending payload must classify as `pending` and the guard must not exit 0. Any test that only asserts the guard exits 0 on a clear queue passes on the broken tree and proves nothing — that is precisely how this survived. ## Scope note Split out of #1194. That issue was filed carrying this body under a title about the 38-tool installed-framework drift; the two are unrelated defects and #1194's merged PR #1195 (drift detection) does not touch this one. #1194 keeps the framework-refresh scope; this issue keeps the classifier defect so it is not lost when #1194 closes. Distinct from #1177 (`ci-queue-wait.sh` guards `main` by default, not the branch being pushed) — that is a target-selection defect; this is a payload-delivery defect. Both are live. Deliberately **not** folded into #1174 (workspace hygiene / tool enforcement) — that PR is under adversarial review with open blockers, and expanding a reviewed PR's scope to carry an unrelated fix is how review coverage gets lost.
Author
Collaborator

Closing as INVALID AS FILED. This is my error, recorded rather than quietly dropped.

The filing claimed a defect on main on the basis that the installed copy and main were byte-identical. They are not:

  • main: packages/mosaic/framework/tools/git/ci-queue-wait.sh — sha256 320bd729…
  • installed on web1: ~/.config/mosaic/tools/git/ci-queue-wait.sh — sha256 19cda2f7…

main already carries the fix (58b971ab, 2026-08-01) and supplies the classifier source via python3 -c, with an explicit comment against reintroducing the regression: "Python source comes from -c so the provider payload remains on stdin. Never move the payload to argv." The installed copy is the old python3 - <<'PY' heredoc form, where the program comes from stdin and the piped payload never arrives.

I measured the installed copy and then asserted the result about main without re-measuring against main. Credit to @coder5, which refused the brief as contradictory and measured instead rather than implementing what it was told.

The underlying condition is real but already tracked: the installed framework on web1 is stale relative to main, which is #1194, and #1194 names the queue guard in its own body. Gate 6 is not vacuous in the framework — it is vacuous ON THIS HOST because the host is stale. Observed live during the #1174 merge, which printed [ci-queue-wait] state=unknown and did not block.

Remaining work is the installed refresh under #1194. No separate fix is needed here.

Closing as INVALID AS FILED. This is my error, recorded rather than quietly dropped. The filing claimed a defect on `main` on the basis that the installed copy and `main` were byte-identical. They are not: - `main`: packages/mosaic/framework/tools/git/ci-queue-wait.sh — sha256 `320bd729…` - installed on web1: ~/.config/mosaic/tools/git/ci-queue-wait.sh — sha256 `19cda2f7…` `main` already carries the fix (58b971ab, 2026-08-01) and supplies the classifier source via `python3 -c`, with an explicit comment against reintroducing the regression: "Python source comes from -c so the provider payload remains on stdin. Never move the payload to argv." The installed copy is the old `python3 - <<'PY'` heredoc form, where the program comes from stdin and the piped payload never arrives. I measured the installed copy and then asserted the result about `main` without re-measuring against `main`. Credit to @coder5, which refused the brief as contradictory and measured instead rather than implementing what it was told. The underlying condition is real but already tracked: the installed framework on web1 is stale relative to `main`, which is #1194, and #1194 names the queue guard in its own body. Gate 6 is not vacuous in the framework — it is vacuous ON THIS HOST because the host is stale. Observed live during the #1174 merge, which printed `[ci-queue-wait] state=unknown` and did not block. Remaining work is the installed refresh under #1194. No separate fix is needed here.
Mos closed this issue 2026-08-13 14:26:54 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1198