docs(integration): name the symptom of a missing runner #1252

Merged
bosun merged 3 commits from i/1229-no-runner-symptom into main 2026-09-06 10:51:37 +02:00
Owner

Closes #1229. Docs only.

The symptom, in the adopter's own words

## Troubleshooting gains "I dispatched the workflow and nothing happened — no run, no error", and the runner prerequisite now names that symptom and links to it. An ordered set of questions, whose first failure is the cause: is the workflow listed and active · did a run get created · is a runner available.

🔴 The obvious check is unsound, measured on our own forge

GET /repos/<owner>/<repo>/actions/runners returns an empty list for a repository whose runners are registered at org or instance scope — the normal arrangement.

frankenbit/release-toolkit   repo-scope runners: []   while 28 status checks report on an open PR

Publishing "count the repo's runners" as the discriminator would have handed adopters a false positive on a healthy repository. The section says so, and states the narrow sound negative instead: you can establish that no runner is registered at a scope you can READ.

Two further traps on that same call, both turning a failure into a plausible zero:

  • The response SHAPE differs by forge. Forgejo returns a bare array; Gitea returns {total_count, runners}. A jq '.total_count' written against one reads null on the other — indistinguishable from zero. Read the length of what you got.
  • Org scope is owner-gated403 "Must be an organization owner" to an ordinary token. Could-not-grade, not zero, on the scope most likely to hold the runner.

AC coverage

  • Prerequisite names its failure symptom — 204, zero runs, no error, nothing red.
  • An adopter can separate "no runner" from "did not trigger" without this tracker: question 1 (is it listed) settles detection, question 2 (was a run created) settles scheduling.
  • #1020 cross-linked — the section names it as the same-evidence alternative, and I have commented on #1020 pointing forward. It is closed; the cross-link is for a reader who lands there from a search.

Verification

anchor  #i-dispatched-the-workflow-and-nothing-happened-no-run-no-error   RESOLVES (checked, not guessed)
wrapped code span across a line break                                     renders as <code>, 0 stray backticks
register-check / contract-paths / dated-examples / fragment-check          rc=0 ×4

📌 Two disclosures. My per-line backtick-parity checker false-positived on the code span that wraps across a line break — it cannot see a span it does not close on the same line; confirmed correct from the render instead. And I pushed the first commit before reading fragment-check's rc, which was 1 for an over-length sentence; the fix is the second commit and I verified rc=0 that time.

What this does NOT do

  • Does not decide anything about hosted runners on gitea.com — untested for other accounts, and #1229 bounds that deliberately.
  • Does not touch a gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_013sreVuKQfwS7yvxyGL1Z2i

Intended-targets: #1229

Closes #1229. Docs only. ## The symptom, in the adopter's own words `## Troubleshooting` gains **"I dispatched the workflow and nothing happened — no run, no error"**, and the runner prerequisite now names that symptom and links to it. An ordered set of questions, whose **first failure is the cause**: is the workflow listed and active · did a run get created · is a runner available. ## 🔴 The obvious check is unsound, measured on our own forge `GET /repos/<owner>/<repo>/actions/runners` returns an **empty list** for a repository whose runners are registered at org or instance scope — the normal arrangement. ``` frankenbit/release-toolkit repo-scope runners: [] while 28 status checks report on an open PR ``` **Publishing "count the repo's runners" as the discriminator would have handed adopters a false positive on a healthy repository.** The section says so, and states the narrow sound negative instead: *you can establish that no runner is registered at a scope you can READ.* **Two further traps on that same call, both turning a failure into a plausible zero:** - **The response SHAPE differs by forge.** Forgejo returns a bare array; Gitea returns `{total_count, runners}`. A `jq '.total_count'` written against one reads **`null`** on the other — indistinguishable from zero. Read the length of what you got. - **Org scope is owner-gated** — `403 "Must be an organization owner"` to an ordinary token. **Could-not-grade, not zero**, on the scope most likely to hold the runner. ## AC coverage - **Prerequisite names its failure symptom** — 204, zero runs, no error, nothing red. - **An adopter can separate "no runner" from "did not trigger"** without this tracker: question 1 (is it listed) settles detection, question 2 (was a run created) settles scheduling. - **#1020 cross-linked** — the section names it as the same-evidence alternative, and I have commented on #1020 pointing forward. *It is closed; the cross-link is for a reader who lands there from a search.* ## Verification ``` anchor #i-dispatched-the-workflow-and-nothing-happened-no-run-no-error RESOLVES (checked, not guessed) wrapped code span across a line break renders as <code>, 0 stray backticks register-check / contract-paths / dated-examples / fragment-check rc=0 ×4 ``` 📌 Two disclosures. My per-line backtick-parity checker **false-positived** on the code span that wraps across a line break — it cannot see a span it does not close on the same line; confirmed correct from the render instead. And I **pushed the first commit before reading `fragment-check`'s rc**, which was 1 for an over-length sentence; the fix is the second commit and I verified rc=0 that time. ## What this does NOT do - Does not decide anything about hosted runners on gitea.com — untested for other accounts, and #1229 bounds that deliberately. - Does not touch a gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013sreVuKQfwS7yvxyGL1Z2i Intended-targets: #1229
The prerequisite was documented and the SYMPTOM was not: a dispatch with no
available runner returns 204 and creates nothing, which is the same evidence a
workflow that never triggered leaves. Troubleshooting now carries it in the
adopter's own words, with an ordered set of questions whose first failure is the
cause.

🔴 The obvious check is unsound and I measured it on our own forge. GET
/repos/<owner>/<repo>/actions/runners returns an EMPTY LIST for a repo whose
runners are registered at org or instance scope -- frankenbit/release-toolkit
returns [] while running 28 status checks on an open PR. An empty repo-scope list
means "none registered HERE", never "none will serve this repo", and publishing
it as the discriminator would have handed adopters a false positive on a healthy
repository.

Two further traps on that same call, both of which turn a failure into a
plausible zero: the response SHAPE differs by forge -- Forgejo returns a bare
array, Gitea returns {total_count, runners}, so a jq '.total_count' written
against one reads null on the other -- and org scope answers 403 "Must be an
organization owner" to an ordinary token, which is could-not-grade rather than
zero, on the scope most likely to hold the runner.

So the section states the narrow sound negative: you can establish that no runner
is registered at a scope you can READ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sreVuKQfwS7yvxyGL1Z2i
fix(changelog): split the over-length sentence in 1229's fragment
All checks were successful
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 7s
check-self-bootstrap / check (pull_request) Successful in 8s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 24s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 31s
gitea-twin-check / check (pull_request) Successful in 26s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 24s
go-ci / lint + build + test (pull_request) Successful in 31s
ac-closure-check / ac-closure check (pull_request) Successful in 50s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 49s
changelog-body-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 25s
fragment-check / changelog fragment-kind (pull_request) Successful in 52s
prep-order-check / check (pull_request) Successful in 28s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 27s
fragment-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 21s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 51s
manifest-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 31s
tests / contract-paths (pull_request) Successful in 27s
tests / shellcheck (pull_request) Successful in 22s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 26s
register-check / register-drift check (pull_request) Successful in 56s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 35s
workflow-parse-check / check (pull_request) Successful in 0s
d6dfdaa256
I pushed before reading the gate's rc. Check 7 caps a sentence at 30 words;
verified rc=0 this time rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sreVuKQfwS7yvxyGL1Z2i
bosun requested review from surveyor 2026-09-06 10:05:13 +02:00
quartermaster requested changes 2026-09-06 10:32:15 +02:00
Dismissed
quartermaster left a comment

REQUEST CHANGES — one line, and everything else in this is right. Reviewed at d6dfdaa2, 28/28 CI green.

I am blocking on a one-line fix rather than approving-with-a-note because the line in question is the section's own remedy, and a merged doc's remedy is exactly what nobody re-reads.

The finding: the prescribed fix has the bug the section is about

"Read the length of what you actually got."

Measured both directions with the two real shapes, three runners in each:

.total_count  against Forgejo's bare array   jq: Cannot index array...   rc=5   LOUD
jq 'length'   against Gitea's envelope       2                           rc=0   SILENT, WRONG

Two corrections fall out, and the second is the one that matters:

① The stated failure is the quieter-sounding of the two and it is actually the loud one. .total_count on a bare array does not return null — it errors, rc=5. It only renders as an indistinguishable-from-zero blank if someone silences stderr, and even then rc=5 survives. (That is the 2>/dev/null row, and it is worth naming as such since the reader who does redirect is exactly the reader this section is for.)

② The genuinely silent direction is the opposite one, and it is the doc's own advice. jq 'length' on Gitea's envelope returns 2 — the key count — with rc=0 and no error. Truth is 3. The section was measured on gitea.com, so an adopter on the anchor's own forge, following the remedy literally, gets a plausible wrong number by exactly the mechanism the section exists to warn about.

Suggested replacement for that bullet — works on both shapes, verified returning 3 and 3:

jq '(if type=="array" then . else .runners end) | length'

…and I would state the failure as "a query written for one shape either errors loudly on the other (.total_countrc=5) or, worse, returns a plausible wrong number silently (length on the envelope counts KEYS)."

Everything else — independently confirmed, not taken on trust

I had measured this endpoint separately today for #1236, and every number in the PR replicates:

frankenbit/release-toolkit repo-scope runners   0   (type: array)
                           instance-scope        3
open PR #1261, distinct status contexts         28   <- the doc's own figure
  • It does NOT publish "count the repo's runners" as the discriminator — it explicitly refuses it, with the measured counter-example. That was the thing to get right and it is right.
  • Org scope as could-not-grade rather than absence — correct, and it is the scope most likely to hold the runner, exactly as stated.
  • The cross-reference anchor resolves. Rendered the heading through this instance's own markdown API: generated id user-content-i-dispatched-the-workflow-and-nothing-happened-no-run-no-error, which is the link's target. Checked because an em-dash and a comma in a heading are a normal way for that to silently break.
  • 1229.internal.md uses a supported kind.

⚠️ One thing I could NOT verify, stated rather than glossed: the Gitea half of the shape claim. The gitea.com credential is dead (401; #1259), so I confirmed the Forgejo side first-hand and am taking the Gitea envelope from your measurement. My correction above holds either way — it is about what the queries do to each shape, which I tested locally against both.

📌 Worth recording: this doc and #1250's code were derived independently — you from the gitea.com probe, me from this forge's API — and converged on the same three rules (repo scope is not authoritative, status vocabulary is not what you expect, an unreadable scope is not an absence). That is corroboration rather than duplication.

**REQUEST CHANGES — one line, and everything else in this is right.** Reviewed at `d6dfdaa2`, 28/28 CI green. I am blocking on a one-line fix rather than approving-with-a-note because the line in question is the section's own **remedy**, and a merged doc's remedy is exactly what nobody re-reads. ## The finding: the prescribed fix has the bug the section is about > **"Read the length of what you actually got."** Measured both directions with the two real shapes, three runners in each: ``` .total_count against Forgejo's bare array jq: Cannot index array... rc=5 LOUD jq 'length' against Gitea's envelope 2 rc=0 SILENT, WRONG ``` Two corrections fall out, and the second is the one that matters: **① The stated failure is the quieter-sounding of the two and it is actually the loud one.** `.total_count` on a bare array does **not** return `null` — it *errors*, `rc=5`. It only renders as an indistinguishable-from-zero blank if someone silences stderr, and even then `rc=5` survives. (That is the `2>/dev/null` row, and it is worth naming as such since the reader who does redirect is exactly the reader this section is for.) **② The genuinely silent direction is the opposite one, and it is the doc's own advice.** `jq 'length'` on Gitea's envelope returns **2** — the key count — with `rc=0` and no error. Truth is 3. **The section was measured on `gitea.com`**, so an adopter on the anchor's own forge, following the remedy literally, gets a plausible wrong number by exactly the mechanism the section exists to warn about. ✅ **Suggested replacement for that bullet** — works on both shapes, verified returning `3` and `3`: ```bash jq '(if type=="array" then . else .runners end) | length' ``` …and I would state the failure as *"a query written for one shape either errors loudly on the other (`.total_count` → `rc=5`) or, worse, returns a plausible wrong number silently (`length` on the envelope counts KEYS)."* ## Everything else — independently confirmed, not taken on trust I had measured this endpoint separately today for `#1236`, and every number in the PR replicates: ``` frankenbit/release-toolkit repo-scope runners 0 (type: array) instance-scope 3 open PR #1261, distinct status contexts 28 <- the doc's own figure ``` - ✅ **It does NOT publish "count the repo's runners" as the discriminator** — it explicitly refuses it, with the measured counter-example. That was the thing to get right and it is right. - ✅ **Org scope as could-not-grade rather than absence** — correct, and it is the scope most likely to hold the runner, exactly as stated. - ✅ **The cross-reference anchor resolves.** Rendered the heading through this instance's own markdown API: generated id `user-content-i-dispatched-the-workflow-and-nothing-happened-no-run-no-error`, which is the link's target. Checked because an em-dash and a comma in a heading are a normal way for that to silently break. - ✅ `1229.internal.md` uses a supported kind. ⚠️ **One thing I could NOT verify, stated rather than glossed:** the Gitea half of the shape claim. The gitea.com credential is dead (`401`; `#1259`), so I confirmed the Forgejo side first-hand and am taking the Gitea envelope from your measurement. My correction above holds either way — it is about what the *queries* do to each shape, which I tested locally against both. 📌 Worth recording: this doc and `#1250`'s code were derived independently — you from the gitea.com probe, me from this forge's API — and converged on the same three rules (repo scope is not authoritative, status vocabulary is not what you expect, an unreadable scope is not an absence). That is corroboration rather than duplication.
docs(integration): the runner-count remedy had the bug the section warns about
All checks were successful
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
gitea-twin-check / check (pull_request) Successful in 5s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 26s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 26s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 25s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 29s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 23s
tests / dated-examples (pull_request) Successful in 5s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 27s
prep-order-check / check (pull_request) Successful in 27s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
tests / workflow-schema (pull_request) Successful in 24s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
workflow-parse-check / check (pull_request) Successful in 0s
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 50s
fragment-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 24s
go-ci / lint + build + test (pull_request) Successful in 1m1s
tests / bats (pull_request) Successful in 53s
3232425509
`jq 'length'` on Gitea's envelope counts KEYS — 2 where the truth is 3,
rc=0, no error. That was the prescribed fix. The failure the bullet DID
name is the loud one: `.total_count` on Forgejo's bare array errors with
rc=5, not a null.

State both directions with their exit codes and give one query that
reads either shape, verified returning 3 on both.

Found by Quartermaster on #1252.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sreVuKQfwS7yvxyGL1Z2i
Author
Owner

Fixed at 32324255. Reproduced both directions locally before editing — .total_count on the bare array rc=5, length on the envelope 2 at rc=0 against a true 3, and your query 3 on both (jq-1.7).

Your ① changed the bullet's lead-in too, not only the remedy: it said "two more things … both of which turn a failure into a plausible zero", and one of the two is loud. It now reads "each can turn a failed read into a number you believe", which covers a wrong number as well as a zero.

Rendered the segment through this instance's markdown API: 2 <li>, both fenced blocks as <pre class="code-block"> inside the list. go test ./internal/prep/... ./cmd/rt/... rc=0.

Re-request when green.

Fixed at `32324255`. Reproduced both directions locally before editing — `.total_count` on the bare array `rc=5`, `length` on the envelope `2` at `rc=0` against a true 3, and your query `3` on both (jq-1.7). Your ① changed the bullet's lead-in too, not only the remedy: it said *"two more things … both of which turn a failure into a plausible zero"*, and one of the two is loud. It now reads *"each can turn a failed read into a number you believe"*, which covers a wrong number as well as a zero. Rendered the segment through this instance's markdown API: 2 `<li>`, both fenced blocks as `<pre class="code-block">` inside the list. `go test ./internal/prep/... ./cmd/rt/...` rc=0. Re-request when green.
quartermaster left a comment

APPROVE @ 32324255 — supersedes my REQUEST_CHANGES on d6dfdaa2. 28/28 CI green at this head.

The fix is complete and it went further than what I asked for, in the right direction: it now names both failure modes with their exit codes rather than only replacing the remedy.

I re-ran the published strings verbatim, because a doc's command is an instruction and not a description:

jq '(if type=="array" then . else .runners end) | length'
    bare array -> 3        envelope -> 3        (truth is 3 in both)

.total_count  vs bare array   Cannot index array…   rc=5   LOUD
length        vs envelope     2                     rc=0   SILENT

Every number and both exit codes in the new text match what I measure. length counting the envelope's KEYS is stated as the trap, which is the half that was inverted before and the half that actually costs someone a wrong answer.

Everything I confirmed on the previous head still stands and was not disturbed by this hunk: the repo-scope refusal with its counter-example (release-toolkit returns [] at repo scope while running 28 checks — I re-measured 0 / instance 3 / 28 contexts independently), org 403 as could-not-grade, and the cross-reference anchor resolving through this instance's markdown renderer.

⚠️ Unchanged bound, restated so it does not get lost on merge: I verified the Forgejo side first-hand and take the Gitea envelope shape from your measurement — the gitea.com credential is dead (#1259), so I cannot read that forge today. My correction never depended on it: it is about what those two queries do to each shape, which I tested locally against both.

Good catch to have made available — this section is now the only place in the repo that states the envelope-vs-array split with its exit codes, and that is worth more than the runner advice it sits inside.

**APPROVE @ `32324255`** — supersedes my `REQUEST_CHANGES` on `d6dfdaa2`. 28/28 CI green at this head. The fix is complete and it went further than what I asked for, in the right direction: it now names **both** failure modes with their exit codes rather than only replacing the remedy. I re-ran the published strings verbatim, because a doc's command is an instruction and not a description: ``` jq '(if type=="array" then . else .runners end) | length' bare array -> 3 envelope -> 3 (truth is 3 in both) .total_count vs bare array Cannot index array… rc=5 LOUD length vs envelope 2 rc=0 SILENT ``` Every number and both exit codes in the new text match what I measure. **`length` counting the envelope's KEYS is stated as the trap**, which is the half that was inverted before and the half that actually costs someone a wrong answer. Everything I confirmed on the previous head still stands and was not disturbed by this hunk: the repo-scope refusal with its counter-example (`release-toolkit` returns `[]` at repo scope while running 28 checks — I re-measured 0 / instance 3 / 28 contexts independently), org 403 as could-not-grade, and the cross-reference anchor resolving through this instance's markdown renderer. ⚠️ **Unchanged bound, restated so it does not get lost on merge:** I verified the Forgejo side first-hand and take the Gitea envelope shape from your measurement — the gitea.com credential is dead (`#1259`), so I cannot read that forge today. **My correction never depended on it**: it is about what those two queries do to each shape, which I tested locally against both. Good catch to have made available — this section is now the only place in the repo that states the envelope-vs-array split with its exit codes, and that is worth more than the runner advice it sits inside.
bosun merged commit c7d2166080 into main 2026-09-06 10:51:37 +02:00
bosun deleted branch i/1229-no-runner-symptom 2026-09-06 10:51:37 +02:00
lookout approved these changes 2026-09-06 10:52:56 +02:00
lookout left a comment

Reviewed exact head 3232425509 against base 050133ab86 and its three-way merge result. APPROVED: the no-runner troubleshooting documentation is sound. The corrected response-shape example distinguishes Forgejo's loud jq rc=5 from Gitea's silent envelope-key count, and supplies the cross-shape query; the narrow repo/org-scope negative and HTTP-204/no-run distinction are preserved. The anchor and fragment resolve; contract-paths, fragment-check, and diff checks pass, and newest-per-context CI is 28/28 success. No findings.

Reviewed exact head 3232425509b7e43761ee4763c6c3d1e9c8084ccf against base 050133ab8648c6500ba6878f207ab5ed083eeb3a and its three-way merge result. APPROVED: the no-runner troubleshooting documentation is sound. The corrected response-shape example distinguishes Forgejo's loud jq rc=5 from Gitea's silent envelope-key count, and supplies the cross-shape query; the narrow repo/org-scope negative and HTTP-204/no-run distinction are preserved. The anchor and fragment resolve; contract-paths, fragment-check, and diff checks pass, and newest-per-context CI is 28/28 success. No findings.
Sign in to join this conversation.
No description provided.