mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #230] [MERGED] ci(rosters): add a leak-safe roster shape probe (TEMPORARY) #238
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#238
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/230
Author: @mattmillerai
Created: 8/26/2026
Status: ✅ Merged
Merged: 8/26/2026
Merged by: @mattmillerai
Base:
main← Head:matt/roster-shape-probe📝 Commits (6)
d39c282ci(rosters): add a leak-safe roster shape probe (TEMPORARY)d5c901dfix(rosters): harden the roster shape probe per the review paneld7f71c2fix(rosters): close the round-2 review findings on the roster shape probefd5ab65fix(rosters): close the round-3 review findings on the roster shape probe1865e75docs(rosters): drop the stale 'digest already public' premise from the probe header5931d8cfix(rosters): the probe now CONFIRMS a digest instead of publishing one📊 Changes
1 file changed (+511 additions, -0 deletions)
View changed files
➕
.github/workflows/roster-shape-probe.yml(+511 -0)📄 Description
ELI5
We keep a list of which repos get automatic code reviews. That list lives in a
safe. You can put a new list in, but you can never open the safe to read the
current one.
Someone wants one repo added. To add one, you have to put in a whole new list —
and since we can't read the old one, we'd be writing it from memory. Guess wrong
and repos silently drop off. Nobody finds out until their automation has quietly
been dead for weeks.
The safe does shout one thing every time it's used: "I'm holding 19 things, and
here's a fingerprint of them." That's enough to check a guess, but not
enough to make one.
This PR adds a button that asks the safe a few harmless questions about the
shape of what's inside — how many items, which optional fields each item has,
what type of value sits in each field. Never the names. Never the label text. Not
even how long they are.
Shape + fingerprint together let us rebuild the list exactly and prove it
matches before writing anything. Then we add the new repo, and the safe's next
fingerprint confirms it landed.
Then we delete the button. It also expires on its own on 2026-09-30, in case we
forget.
Why
A request came in to enrol another caller in
CURSOR_REVIEW_CALLERS. That turnsout to be blocked, and this PR is the unblock.
The rosters live only inside write-only Actions secrets:
gh secret setoverwrites wholesale and there is no
gh secret get. So adding one caller meansrewriting all of them, and we cannot currently prove what "all of them" is.
Live
CURSOR_REVIEW_CALLERSis 19 callers,ad58945c…(confirmed on run32929348118, 2026-08-26). The private ops repo has recovered membership,order and filenames from the fleet's own bump PRs — but not the entry shape;
~116 candidate serializations have failed to match that digest. Writing an
unverified roster is how you silently unenroll 19 repos.
What this does
A
workflow_dispatch-only, self-expiring probe that prints the missingstructural facts and nothing else. One run should close the digest.
For a roster with fewer than 8 entries only
count+roster_sha256print(both already public via the bumper's audit line) — see "The disclosure
argument".
The disclosure argument — this is the bit to review
This repo is public and so are its run logs. Actions masks a secret's exact
value but not anything derived from it, so "it came from a secret" buys nothing
here. Each line is therefore non-disclosing by construction:
keys<unlisted-key>, re-sorted after substitution so its position doesn't hint at the namewire_bot,labelnull,"absent"and type markers ("<string>"); no string content and no lengths — over a""/ci/Corevocabulary a length is the labelfiles(ci-)?<stem>.ya?ml, anchored); an off-vocabulary string prints<unlisted>; absent / null / non-string use the same four-state encoding as the other fieldsroster_sha256bump-callers.shalready logs on every run; verifies a reconstruction we hold. Its residual (a one-entry roster collapses to "is it this repo?") is pre-existing and stated in the headerMIN_SHAPE_COUNTthe operator does the inverse search locally over the tiny per-entry shape space insteadDeliberately not printed: any repo name, any label text or length, any
repo name length, any key name outside the documented four, any filename
outside the public vocabulary, and — after review — no per-field digests:
sha256(map(.repo))with count and filenames beside it is an unsaltedmembership oracle over an enumerable
Comfy-Org/*namespace, andsha256(map(.label))over a three-word vocabulary is just the labels in a wrapper.If you add a line here, re-answer that question for it. If you can't state why it
discloses nothing, it doesn't go in.
One subtlety worth flagging
The per-position vectors use
has(), not// null. In jq onlynullandfalseare falsy, so.wire_bot // nullreports a present-and-falsewire_botidentically to an absent one — and those two serialize differentlyunder
jq -cS, which would leave the reconstruction unfalsifiable against thevery digest we're trying to match. Absent (
"absent"), explicitnull, theboolean, and
"<type>"for anything else are four distinct outputs on everyfield,
fileincluded.Blast radius
workflow_dispatchonly;permissions: {}; 5-minute timeout. A first,secret-free step fails (rather than silently skipping) on a non-main ref,
re-checks the roster name against a literal allowlist before it is used to
index
secrets, and refuses to run afterEXPIRES: 2026-09-30so a forgottencopy on
maingoes dead on its own — the date is shape-validated so a typofails closed, not open. The header says plainly that the ref check is not a
security boundary (write access is secrets access, as for every
bump-*-callers.yml). The roster gate is slurped and pinned to exactly one JSONarray of objects, so a concatenated paste is a red run rather than a green one
with a digest nothing can match. It touches no roster, opens no PR, and writes
nothing. The live bumper is not modified — this is a separate file
specifically so it cannot affect the fleet, and so it can't trip
test-bump-callers.yml's staticbump-*-callers.ymlguard.Verification
actionlint .github/workflows/roster-shape-probe.yml— passesshellcheckon both extracted step bodies — passespython3 .github/workflow-pins/check_workflow_pins.py— passesbash .github/bump-callers/tests/test_bump_callers.sh— 437 passed, 0 failedmixing every field state (absent / null / false / non-string per field,
listed and unlisted filenames, trailing newline, unanchored prefix, unknown
keys) whose printed digest matches
jq -cS . | sha256sum; a 3-entry roster(shape withheld) and the 7→8 threshold boundary; an array of strings, a bare
object, two concatenated documents, empty, non-JSON; no
sha256sumon PATH(exits non-zero, nothing printed); non-main ref, unknown roster name,
malformed
EXPIRES, expired, expires-todayAfter this merges
CURSOR_REVIEW_CALLERSjq -cS . | sha256sum == ad58945c…locallywith the new caller added (20)
rosters (all 12 fleets are selectable) — before it expires on 2026-09-30
(tracked: BE-9651)
Provenance
Verified: actionlint, shellcheck, check_workflow_pins.py and test_bump_callers.sh (437/0) green locally at
d7f71c2; both probe step bodies exercised against synthetic rosters and every guard path (see Verification).Deviations: round-1 panel fixes (
d5c901d) dropped the two per-field digests, allowlisted the printed filenames, and replaced the job-level refif:with a failing, self-expiring guard step; round-2 fixes (d7f71c2) allowlist key names, drop label lengths for presence/type, givefilethe four-state encoding, withhold the shape lines below 8 entries, validateEXPIRES, slurp the JSON gate, and setpermissions: {}. A protectedenvironment:gate was not adopted (repo-settings change, not warranted for a self-expiring throwaway);FILE_ALLOWstays a static snapshot rather than a run-time derivation (that would need a checkout or token in a job meant to hold only the secret) — the output flags possible staleness instead.🤖 Generated with Claude Code
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.