Emergency operator-authorized hotfix, sibling of crew 36c6745: engine and
sweep panel requests fail on the pre-rename -reviewer names. ceremony#232
records the debt and verifies. Authorized by @andres in-session.
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
'By decision rather than omission' published a back-tagging ruling that
belongs to the operator and has not been made. The fragment's authorized
scope is the measured gap, which the first two entries state
(@codex-reviewer-andresmgsl, !221 review).
Refs #220
Three Changed entries naming what a reader comparing the two forges' tags
would otherwise reconstruct: 0.5.0/0.6.0 arrived by merge and were never
released here; the carried ## 0.6.0 section is upstream's record; and the
absent 0.6.0 tag is a decision, not an omission. Verified: eleven fragments
assemble cleanly with these entries present, shape/cite guards green.
Refs #220
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
The evidence shows the called workflow did not see the caller's event inputs
on THIS instance while the top level received them (runs 459/523 vs probe
6/7); it does not show what GitHub or a declared-and-passed input does, so
every prose site now states declare-and-pass as the measured-reliable channel
rather than attributing a drop to Forgejo (@codex-reviewer-andresmgsl, !218
blocker 2).
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
Off-by-one in the evidence anchor; a record that links the wrong comment is
a record that does not resolve (@codex-reviewer-andresmgsl, !216 round 2).
Refs #202
Four corrections from @codex-reviewer-andresmgsl on 9c17a9e:
- changelog.d/202.md keeps !207's merged Added section (my cat > had deleted
64 lines of unreleased release notes) with the drills appended under Changed;
- run 1 and run 4 link their own probe issues — run 4 is the clean repeat
after the redaction incident and deserves its own citation;
- the #205 record links the evidence per identity and drops the pseudo-JSON,
claiming only what the cited runs measured;
- the security lesson states the real invariant: report content must never
contain a credential expression OR value — variables are not laundering.
Refs #202
The owed-probes list keeps delivered probes with their probe-issue URLs: a
claim like 'the asymmetry reproduces' should carry a link a reader can open.
Also corrects the #205 line's premise (the 500 was a bad-ref/unknown-workflow
diagnostic, not a broken route) and adds the two venue lessons the first
drills taught.
Refs #202
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
The manifest target-validation added in 6874e76 checked owner and sha per
kind but never bound the kind to the path class, so a consistently swapped
caller layer passed: `<fork>/actions/x@<armed>` declared workflow_caller
satisfies the owner rebuild and the armed-sha test
(@codex-reviewer-andresmgsl, #202 review).
Decompose the coordinate once, then let the kind fix BOTH the path class
and the sha. Driven with manifest and tree mutated together, so
tree-vs-manifest equality cannot hide the swap.
Also finish the rename codex asked for: the generator's first parameter is
the pre-arming candidate checkout, and the variable is now named for it.
Refs #202
@codex-reviewer-andresmgsl, and he linted the published snippets DIRECTLY,
which my "parse, lint clean" claim had never meant.
1. THE CHECKER DID NOT CHECK THE TARGET. It accepted <fork> <code-sha>
<armed-sha> and used none of them — SC2034 on all three, which is the same
defect the linter and the reviewer found independently. It proved only
"tree equals manifest", so a manifest generated with the ARMED sha where the
candidate belonged, against a tree rewritten to that same wrong value,
passed. Wrong-but-consistent is exactly what this gate exists to reject.
Each manifest `want` is now validated against the independently supplied
target before the tree is compared to it.
2. ONE ORDER, NOT TWO. Step 2 said "commit the arming AND write the manifest"
while the prose below correctly said to generate from the PRE-arming tree.
The manifest enumerates the carriers that must CHANGE, so it has to see them
before they do — generating afterwards enumerates rewritten rows and loses
the canonical internal-checkout ones entirely. The generator's first
parameter is <candidate-checkout> now, and says so.
3. THE SNIPPETS LINT CLEAN STANDALONE. SC2016 needed a scoped directive — and
the first placement was itself invalid: SC1124, a directive may precede a
complete command, not an individual case branch. The checker's mktemp gets
a trap.
Driven, the new controls:
correct manifest + tree + target args passes
wrong fork, manifest AND tree consistent refuses
wrong candidate sha, consistent refuses
armed sha where the candidate belongs refuses
plus every earlier class still red, and both snippets ShellCheck-clean when
extracted as an operator would copy them.
test/run.sh 28/28; repository shellcheck 0.10.0 and changelog-armed clean.
Refs #202
Found by combining all five open PRs and running the suite on the result —
which is the check this PR's own runbook adds, catching a real break the first
time it was applied at scale.
!203 (#201) makes actions/docs-sync/docs-sync.sh decide the forge from
GITHUB_SERVER_URL, because it was fetching the doctrine mirror from a
hard-coded github.com. This PR's guard requires every forge-deciding file to be
named in the inventory. Both are individually green; together the tree is red:
forge-specific but not in docs/UPSTREAM-SYNC.md:
actions/docs-sync/docs-sync.sh
The entry belongs here rather than in !203: the inventory is this PR's artifact,
and !203 is a bug fix that should not have to know about a guard absent from
its base. Adding it early is harmless — the guard checks that deciding files
ARE listed, not that listed files decide — and correct the moment both land.
Five-way combined tree after this: 28 test files 0 failed under the runner's
jq 1.6, shellcheck 0.10.0, actionlint, self-ref, marker, vendored and
changelog-armed all clean.
Refs #200
@codex-reviewer-andresmgsl drove the published commands again and found three.
1. THE GENERATOR ABORTED ON AN ABSENT CALLER CLASS — the same `set -e` +
`git grep` no-match bug I had just fixed in the CHECKER, in the generator I
wrote in the same commit and did not apply the lesson to. A probe that
exercises one layer produced no manifest and no diagnostic. `|| true` on
every extraction, plus an explicit count so ZERO ceremony callers refuses by
name while workflow-only and action-only probes generate valid manifests.
That count check was itself broken on its first write: `grep -E '\t…'` reads
a literal `t`, not a tab, so it counted zero on a perfectly good manifest
and refused it. Found by running it.
2. CALLERS CARRY THE COMPLETE COORDINATE. The manifest stored only the sha and
the checker compared owner and suffix separately, so
`<fork>/actions/WRONG-ONE@<right-sha>` passed. The manifest now records
`<fork>/<path>@<sha>` and every kind is one exact comparison — which also
removes the per-kind branch that made the omission possible.
3. GENERATOR AND CHECKER SHARE ONE DOMAIN. `actual` extracted every `uses:`
while the generator manifested only ceremony patterns, so a legitimate
`actions/checkout` was always an unrecognised carrier. Both are restricted
to ceremony callers; a wrong OWNER is still caught because
`wrong-owner/ceremony/...` is still a ceremony caller.
And the stale fragment wording, which glm flagged and codex re-flagged:
"both CEREMONY_SELF_REF values" -> "every".
DRIVEN, all of it:
generator: both / workflow-only / action-only -> valid manifests
generator: zero ceremony callers -> refuses by name
deletion, role swap x2, wrong owner, wrong sha,
wrong path, deleted caller class, extra carrier -> all refuse
armed control, third-party actions/checkout present -> passes
test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean.
Refs #202
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's four holes and @glm-reviewer-andresmgsl's prose
staleness. Every weaker shape I had written has a hole, and each was found in a
published draft of this file:
"the old literal is absent" a carrier rewritten to the wrong fork
"every extracted value equals X" a carrier that VANISHED
"each value is one of {fork,dynamic}" a ROLE SWAP either direction
"the SHA suffix matches" wrong-owner/ceremony/actions/foo@right-sha
"known callers match" an unrecognised caller, or none
So the arming step generates a MANIFEST — path, kind, full expected value —
from the tree it is arming, and the gate compares actual carriers against it as
a set. All six become one kind of failure: the sets differ. Generated rather
than written into this document, because the carrier set changes whenever a
workflow is added — which is exactly how "both CEREMONY_SELF_REF values" went
stale while main grew a third.
The prose went stale with the snippet, as glm noted: step 2 said "both", and
said "every workflow carrier -> repository:" without excepting the consumer
checkouts. Both corrected.
DRIVEN, not asserted. I built an armed/probe pair and ran every class:
deletion, role swap x2, wrong fork, wrong SHA, extra carrier -> all refuse
the armed control -> passes
Doing that found two defects the snippets would otherwise have shipped with:
* the manifest generator's consumer-checkout line used `\$` inside SINGLE
quotes — an escaped dollar, not the end anchor — so it silently produced a
manifest row with no kind and no value;
* `git grep` exits 1 on no-match, and under `set -e` inside the collecting
group that killed the script BEFORE the comparison. A carrier class that
vanished entirely produced SILENCE rather than a refusal, which is worse
than the hole it was meant to close.
test/run.sh 28/28; shellcheck 0.10.0 and changelog-armed clean.
Refs #202
@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