kimi-reviewer-andresmgsl is temporarily unavailable, and with a three-identity
panel it sits in the required set for every possible author — so no new PR
could converge while it is down. glm-reviewer-andresmgsl takes the seat.
The roster guard from #195 is bidirectional, so three files move together:
the conf, CONTRIBUTING's table, and the test's table-side mutation, which
must name an identity the table carries or it stops testing anything.
Refs #222
All three schedule-simulation probes carried the assignment the script no
longer reads. This time the head was verified clean BEFORE the push, not
after. Refs #215
The script stopped reading GITHUB_EVENT_NAME with the input gate; the
previous commit pushed with this red because I trusted an && echo that
printed nothing. Refs #215
The venue drill caught what no hermetic test had: with the workflow_call
bridge delivering "no" perfectly, drill runs 16/17 still bootstrapped. The
script gated on GITHUB_EVENT_NAME = workflow_dispatch and never read
$BOOTSTRAP at all; the wrapper's only coupling was exporting the event name
for yes. Correct while an operator's manual dispatch was the only dispatch
there was — inert from #209 on, when the trigger job made every machine wake
a workflow_dispatch event. Runs 459/523 bootstrapped for this reason, not
for the input-delivery defect, which is real but was never the operative
cause of the observed re-upserts.
The script now gates on ${BOOTSTRAP:-no} = yes; the wrapper passes the input
through untouched; the hermetic suite pins the exact regression pair (a
dispatch event with no/unset creates and deletes nothing) alongside the
yes path's full create+delete assertions.
Refs #215
A called workflow cannot read the caller's dispatch inputs on this forge:
github.event.inputs is empty inside workflow_call even though the top-level
caller receives the value in both contexts (probe runs 6/7). The sweep's gate
read exactly that, so every dispatch-woken sweep bootstrapped — runs 459 and
523, ~20 label upserts per board event — while the trigger honestly logged
bootstrap=no.
The bridge, per the #6361 contract: labels-sweep.yml declares
workflow_call.inputs.bootstrap (string, default "no"); the dogfood caller and
the published CONSUMERS.md stub pass it via with.bootstrap with empty mapped
to "no" at the caller — kimi's edge: on schedule the top-level context is
empty, and an empty that slipped through would have turned every cron into a
bootstrap. The gate feeds the declared input to labels-reconcile unchanged,
so an invalid value meets the action's own yes|no refusal.
test/labels-bootstrap.test.sh pins every hop: the declared boundary, both
gates as the identity, no expression reading github.event.inputs (scoped to
${{ }} bodies — the file's prose names the context to explain it), the two
pass-throughs byte-exact, and the four value paths driven through the shipped
expressions into the action's real validator. Mutations: dropping the
declaration reds 4, dropping the pass-through reds 3, restoring the old gate
reds 2.
Refs #215
Plain `env` preserves the parent environment, and on the runner every step
arrives with GITHUB_API_URL set — the premise of the fix under test — so the
unset-refusal case inherited it and never exercised the refusal. It passed
only in a dev shell that lacks the variable: the environment distance
UPSTREAM-SYNC.md step 7 warns about, in the test written the same day
(@kimi-reviewer-andresmgsl, run 468).
Refs #205
The check grepped the gh workflow run line #205 removes, so it passed on
every REST implementation including one that swallows a failed POST — a
green assertion whose name claimed an invariant its implementation could not
observe. Its replacement lives in test/labels-dispatch.test.sh: a transport
failure must fail the extracted step, plus a code-aware no-|| true guard
(@codex-reviewer-andresmgsl, !213 review round 2).
Refs #205
Three corrections from @codex-reviewer-andresmgsl's review of 935a813.
1. `api="${GITHUB_API_URL:-https://api.github.com}"` guessed GitHub when the
variable was absent — driven with a recording curl, it reported success
after POSTing to api.github.com from this forge. That is the "Never
'probably github'" rule, and the same unset-environment refusal #201 just
established for docs-sync. It now refuses before any request, and the test
asserts zero calls were made: refusing after a POST is not refusing.
2. The "never silenced with || true" invariant was still asserted by grepping
the gh line this port removed, so it passed on any REST implementation
including one that swallows a failed POST. It is rebound behaviourally: a
curl that dies at the transport must fail the step. Doing that revealed the
step failed with a bare exit 7 and no sentence, so it now names the failure
— owning the diagnostic is the whole point of the surrounding code.
3. docs/CONSUMERS.md and both caller comments still described `gh workflow
run` as the mechanism. They describe the REST dispatch now, and the manual
bootstrap command carries a forge-neutral curl form beside the gh one: a
cross-forge runbook that sends this forge to a missing binary is wrong even
where the prose around it is right.
Refs #205
The action's entire gather was one GraphQL query asking GitHub for its own
parse of the closing keywords. Forgejo serves no GraphQL at all — /api/graphql
404s here and a forgejo-runner job arrives with GITHUB_GRAPHQL_URL empty — so
there was nothing to translate it to. It is re-expressed, as #188 re-expressed
its own two GraphQL sites, over two reads both backends serve plus this repo's
own parser.
The graph was called authoritative for including "closing keywords and sidebar
links". Those halves resolve differently here: Forgejo has no sidebar-link
concept, so nothing is lost there, but it DOES honour closing keywords in
commit messages. A body-only port would miss a PR that closes an issue from a
commit subject — exactly the contradiction this action exists to catch — so
the closing set unions the body and every commit message.
The hasNextPage refusal is relocated, not dropped: --paginate carries the
forgejo backend's x-total-count completeness proof, and a short gather refuses
rather than returning a partial verdict.
lib/issue_references.sh extracts the LOCAL/CROSS classifier from
issueflow-reconcile's executable. closes_references.sh's header recorded that
dependency in prose; a composite action cannot source a reconciler to borrow
one function, because sourcing a reconciler runs one.
refs-guard.yml's github-only gate is removed in the same change. A portable
action behind that gate is a guard that passes by never running.
The contract test drives the boundary on BOTH backends with stubs at the
transport. Mutations: body-only parse reds 4 cases, dropping --paginate reds
the partial-gather case, ignoring a failed read reds 9.
Refs #199
`.github/workflows/labels.yml` dispatched the sweep with `gh workflow run`,
the eighth runtime gh call site the 0.6.0 merge reintroduced and the only one
!204 did not port. On this forge the runner carries neither gh nor a GitHub
API, so the step refused and the entire event-driven reconcile path ended
there — every transition waiting up to an hour for the scheduled sweep.
The workflow-dispatch endpoint has the SAME shape on both forges:
POST {api}/repos/{owner}/{repo}/actions/workflows/{file}/dispatches
{"ref": "<branch>", "inputs": {...}} -> 204, empty body
so the step no longer decides a forge at all. The CEREMONY_FORGE_CLIENT=gh
declaration and both inline refusals are removed rather than ported, and
test/no-runtime-gh.test.sh now asserts their ABSENCE — an opt-out with no gh
behind it is a standing permission slip.
The ref is supplied explicitly and taken from the repository, never from
GITHUB_REF_NAME: on a pull_request_target run that is `<n>/merge`, which is
not a branch. A non-204 still fails the job, keeping the misconfiguration
alarm the trigger exists to be, and the diagnostic explains Forgejo's empty
500 rather than passing a bare status to a reader who will go looking for an
outage that is not there.
test/labels-dispatch.test.sh extracts the shipped step and executes it against
a recording stub, asserting the method, endpoint, ref and inputs actually
sent. Dropping the inputs or ignoring a non-204 both red the suite.
Refs #205
Found by the first post-merge sweep after the 0.6.0 merge — #198's own
acceptance probe — not by review. Three PRs in one run:
labels: #208: could not read the head commit's date:
forge_api: HTTP 404 from 'GET repos/heavy-duty/ceremony/commits/f3a1336…'
— blocker:unrequested not judged this pass
Measured against this instance:
forgejo repos/{o}/{r}/commits/{sha} -> 404
forgejo repos/{o}/{r}/git/commits/{sha} -> 200, date under `.created`
github repos/{o}/{r}/commits/{sha} -> 200, date nested
A fourth asymmetry, alongside the three lib/forge-forgejo.sh's header already
records. #198 ported this call site onto the shim with GitHub's path unchanged
— correct against GitHub, and the block it lives in (#236 D2) arrived WITH the
merge, so nothing here had ever executed it.
So it becomes a verb rather than a path at the call site: the caller wants one
timestamp and should not have to know either shape.
Cost while it stood was bounded and loud rather than silent — guarded_read
refused and the sweep said so — but blocker:unrequested could never be judged
on this forge.
The tests pin each backend's PATH and FIELD, because a stubbed forge_api cannot
catch a wrong path; that is exactly how this shipped and why it took a live
sweep to find. Swapping the paths reds the forgejo pair; swapping the fields
reds the github one.
test/run.sh 28/28 under jq 1.7 and jq 1.6; forge-backends 124/124; shellcheck
0.10.0 and actionlint clean.
Refs #209
@codex-reviewer-andresmgsl found the subtlety my board fixture could not reach:
BOARD_RECORDS filters an object-valued row out of the LIST before the per-issue
guard ever sees it, so no board fixture alone can prove the scalar stand-down.
My #64 case proved the gather excluded it, not that reconcile_issue_pass did.
The fixture that isolates the site is a deliberate mismatch: the LIST row is
null-valued, so the board gather admits #65 — and the INDIVIDUAL payload the
sweep then fetches is object-valued. Only reconcile_issue_pass's own guard can
stand that down.
Three rows over the same number, so the guard cannot pass by standing
everything down or by admitting everything:
payload object-valued -> NOT reconciled
payload null-valued -> reconciled
payload key absent -> reconciled (the GitHub shape)
Mutating ONLY the scalar predicate now reds three BEHAVIOURAL rows plus the
pin, where before it red only the pin and a neighbour. pass_disc is gone: it
repeated the predicate inside the test helper and never called production —
which is the same isolated-expression trap, one layer down, in the fix for it.
issueflow 512/512; test/run.sh 28/28; shellcheck 0.10.0 clean.
Refs #210
@codex-reviewer-andresmgsl's three, and items 1 and 2 were still open after
bada4ff — his review predates that push, but only item 3 (release bodies) was
actually answered by it.
TRAVERSAL, not a non-empty gather. The board case asserted the board was not
read as empty; #60 was `ready` and therefore produced no observable effect, so
nothing proved reconcile_issue_pass had run over it. #60 now carries NO queue
state, so traversal has a deterministic outcome — needs-triage is minted and
logged — and the row asserts that.
THE SCALAR SITE, through the real path. My first attempt asserted the jq
expression the function contains, which is exactly the shape that let this
regression through: the isolated discriminator rows passed the whole time the
gather was blind. A hand-wired probe around reconcile_issue_pass needed so much
internal setup that it would have been testing my scaffolding, so the same
board harness drives it with one row flipped — object-valued must NOT be
reconciled as an issue, and the sweep must then correctly report the board as
empty OF ISSUES.
Per-site mutation, all three now behavioural rather than pin-only:
revert BOARD_RECORDS -> 4 red
revert release_bodies -> 2 red
revert reconcile_issue_pass -> 3 red
test/run.sh 28/28; issueflow 510/510; shellcheck 0.10.0 clean.
Refs #210
@codex-reviewer-andresmgsl's two scope items, applied before the first review
round rather than after.
1. THE GUARD IS COMMENT-AWARE, WITH CONTROLS. It already stripped comments —
it has to, because the #188 warning that explains why has("pull_request") is
wrong contains the string. Without controls that was an untested property,
and the pressure it creates is real: a raw grep would push a builder into
deleting the very warning that prevents recurrence. Two fixtures now prove
it: the explanatory comment is allowed, an executable jq filter is rejected.
2. ALL THREE SITES ARE DRIVEN BY BEHAVIOUR. The pin makes any revert red, but a
pin proves a string is absent, not that each replacement means the intended
thing:
BOARD_RECORDS the forgejo-shaped board is not read as empty
release_bodies an open `release` issue whose gate holds an open
member makes a claimable NON-member draw a window
flag — empty carriers, no flag, so the row
discriminates the site instead of merely reaching it
reconcile_issue_pass the scalar payload: key-present-null is an issue,
object-valued is a PR, key-absent is still an issue
The release_bodies row did NOT discriminate on its first write — it asserted
an issue number that BOARD_RECORDS also produces, so reverting the site left
it green. Caught by mutating each site separately rather than trusting the
suite total.
Mutation, per site: BOARD_RECORDS 3 red, release_bodies 2 red,
reconcile_issue_pass 2 red.
test/run.sh 28/28; issueflow 510/510; shellcheck 0.10.0 clean.
Refs #210