mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #223] [MERGED] fix(workflow-pins): judge a ref: step output on a flow WORKFLOWS_REF binding line (BE-9099) #230
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#230
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/223
Author: @mattmillerai
Created: 8/25/2026
Status: ✅ Merged
Merged: 8/25/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-9099-flow-binding-step-output-ref📝 Commits (2)
3863054fix(workflow-pins): judge aref:step output on a flow WORKFLOWS_REF binding line (BE-9099)3f0a723fix(workflow-pins): register a flow-form resolver's id so its consumer is judged (BE-9099)📊 Changes
3 files changed (+291 additions, -2 deletions)
View changed files
📝
.github/workflow-pins/README.md(+20 -0)📝
.github/workflow-pins/check_workflow_pins.py(+69 -2)📝
.github/workflow-pins/tests/test_check_workflow_pins.py(+202 -0)📄 Description
ELI-5
check_workflow_pins.pyreads a workflow one line at a time and asks that line exactly ONE question: is it aWORKFLOWS_REFbinding, or a directref:use, or a multi-lineref:opener, or aref:that reads a step's output? A step written as a single flow mapping —- {uses: actions/checkout@v4, env: {WORKFLOWS_REF: ...}, with: {ref: "${{ steps.x.outputs.ref }}"}}— answers YES to two of those at once, and the binding question was asked first and stopped there. So that checkout got no verdict at all: notdangling, not guarded, not unguarded, and not even the BE-9045 "I couldn't read this job" warning, because nothing was dropped. It just vanished from the lint. Now the binding arm answers the second question too, so theref:on that line is judged like every other step-output checkout.What changed
.github/workflow-pins/check_workflow_pins.py— insideref_checkouts'if binding:arm, after the guard-credit block and afterresolvers.add(step_id), record the line's step-output sites via_steps_output_sites/_record_steps_output, passing the BE-9045dropcollector exactly as the finalelse:arm does (PR #222 has merged — it is the tip ofmainat5f4a22b— so thedropargument is passed rather than omitted)..github/workflow-pins/tests/test_check_workflow_pins.py— four new tests (265 → 269)..github/workflow-pins/README.md— one sentence in the resolve-then-consume section.Nothing else moved: the
ref_usedemotion,_GUARD_BINDING_FLOW_RE, and #222'sdroppedplumbing are untouched beyond passingdropthrough.Why it is safe to widen the binding arm
The new call only fires on a line that genuinely carries a step-output
ref:. Every spelling of an ordinary binding line that reaches this arm — blockWORKFLOWS_REF: ${{ inputs.workflows_ref }}, its|| job.workflow_shavariant, the quoted form, and both flow forms (- env: {…}andenv: {…}) — returnsNonefrom_steps_output_sites, so the arm is a no-op for them; I probed all five directly. Direct-inputref:uses cannot reach here at all:flow_bindingis only set whenbinding is None and not ref_use, which is the existing# Reporting winsdemotion, untouched. And the walk visits each line once with exactly one arm running, so no double-record guard is needed. Empirically: all 265 pre-existing tests pass unchanged, and the real-tree CLI is byte-identical (OK — 11 workflow(s) …, exit 0, zero notices).Verification of what the ticket names
Reproduced first on
main@5f4a22bbefore touching anything: the flow-mapping fixture returnedref_checkouts == []while the identical line minus theenv:binding returned[(13, False, False, 'dangling')]— the reported defect exactly. After the change the same fixture returns[(13, False, False, 'dangling')].Corpus sweep of this repo's own
.github/workflows/, including the half NOT fixed here: 0 lines carry the newly-judged shape (aWORKFLOWS_REFbinding and a step-outputref:on one physical line), 0 whole-step flow mappings bindWORKFLOWS_REFat all, and there is 1 step-outputref:in the tree (cursor-review.yml:456, block form, already judged and covered). The onesteps: [hit in the tree is a fixture inside arun:heredoc intest-workflow-pins.yml, not a live job —check_diremits zero notices for the real tree, whichtest_this_repos_own_workflows_passasserts. So the fix is preventive for consumer repos and for future edits here; it changes no verdict on any workflow that exists today.Deviation from the ticket's test 2
The ticket specified, for
test_a_flow_step_binding_does_not_swallow_a_covered_consumer, bothref_checkouts == [(lineno, False, False, True)]andunguarded_ref_checkoutsempty. Those two cannot both hold:unguarded_ref_checkoutsfilters onnot guarded, andguardedis that tuple's third element,False. I asserted the tuple — which is the observed behaviour and the one that carries the ticket's actual point (via_step_outputisTrue, not'dangling', i.e. the resolver was credited and the binding arm no longer swallows the line) — and the site is correspondingly reported unguarded.That is not an oversight in the fix: the
if:rides INSIDE the flow mapping, and_skips_on_empty_outputreads only a step-levelif:key of its own. It is the same fail-closed stance the flow-form binding takes one arm up — a flow spelling may DEMAND a guard, never excuse one. I checked all three inline spellings (bare, quoted,${{ … }}-wrapped); none is credited. The remedy is one line, not a rewrite: promoting theif:to its own key is coverage even withenv:andwith:left as flow maps, and the test now asserts that working spelling alongside the failing one, so this can never harden into "that shape simply cannot pass".I also used the class's existing
resolve_refftypo convention rather than the ticket'ssteps.typo, and_reusable(PINNED)rather than adefault: ''input, to match the neighbouring dangling tests as the ticket asked.Review round 2 — panel findings addressed (
3f0a723)The 8-model panel raised 5 findings. Three are fixed here, two are deferred with
repros; every thread has a reply and is resolved.
Fixed — the RESOLVER-side twin of this PR's own fail-open (High). The panel
found that
resolversis populated only via_binding_step_id, which reads ablock-form
id:at the step's key column and so could not see theid:of astep written as one flow mapping — the very spelling this PR is about. Verified
before fixing: a flow-form resolver plus the identical block-form consumer
returned
ref_checkouts == [](no verdict at all), where the block-formresolver spelling correctly returns
(14, False, False, True). Writing theresolver flow-style silently switched the BE-8130/BE-8221 requirement off for
its consumer.
_binding_step_idnow reads that spelling, gated exactly as_job_step_idsgates its own flow reading — only where a flow mapping OPENS —so
with: {id: x}on a block key line still cannot register a phantom resolver(pinned by its own test).
Fixed — a sibling drop the widening surfaced.
_STEP_ID_RE's\S+kept the}that a flow CONTINUATION line leaves on it (- {env: {…},/id: r}→r}) while_job_step_idsreadr, so the resolver registered under an id noconsumer could name — the same silent drop, one spelling over. Both readers now
narrow through
_step_id_value.Fixed — site-recording now precedes registration (ordering, not reach). The
resolversarm of_record_steps_outputcarries nodeclared < consumer_startcheck of its own, only the
step_idsarm does; verified by a block-formself-referencing step returning
(13, False, False, True)rather thandangling. Sotest_a_self_referencing_flow_step_is_danglingreally waspassing for a different reason than its comment gave, and the widening above
would have changed its verdict. Recording ahead of
resolvers.add(step_id)puts that verdict on ORDER, where the widening cannot reach it. I preferred
this to adding an ordering check to the
resolversarm, which would also haveflipped the block-form self-reference from passing to a hard error — and that
shape is runtime-safe (its self-referential
if:is'' != '', so the stepsimply skips). The test comment is rewritten to state the real reason.
Fixed — the docs gap (Low). The README claim held only in the negative
direction and the BE-8130 remedy read as already satisfied for a flow-inline
if:. Both now say plainly that anif:riding inside a flow mapping is notread as coverage, and that the one-line fix is to promote it to the step's own
if:key withenv:/with:left as flow maps.Deferred — comment/quoted text decides which arm a line takes (Medium).
Confirmed, and worse than arm-flipping: it is a false FAILURE on a compliant
workflow.
ref: ${{ steps.r.outputs.ref }}under a correctif:returns[],but the same line with
# resolved from inputs.workflows_refappended returns[(15, False, False)]. Pre-existing onmain, and in a different code path(comment handling at arm selection in
is_ref_use) from the flow-mapping stepreaders this PR touches;
is_ref_usehas several callers, so normalizing wantsits own sweep. Follow-up filed with the repro and proposed fix.
Deferred —
_steps_output_sitesis not scoped towith:(Medium).Mechanism confirmed (
_ref_valuesreturns both entries, the reader returns thefirst), but it is pre-existing — the identical line without the binding takes
the old
elsearm and behaves the same — and I could not reach a fail-openthrough it. Two
ref:keys on one physical line means the whole step is a flowmapping, where the
if:is necessarily inline and therefore never coverage, sothe site is reported unguarded whichever
ref:was judged; the panel's ownfixture returns
dangling. Today the cost is a wrong message, not a missedfinding, so per this repo's follow-up bar it is a thread reply rather than a
ticket.
Also deferred (found while self-reviewing, not panel-raised). A bare
-whose flow mapping sits on the next line fails one level above
_binding_step_id:_step_boundsreturns None, so the resolver neverregisters and its consumer is silently dropped (
_job_step_idsreads{'r': 8};ref_checkoutsreturns[]). Same fail-open class, but_step_boundsis load-bearing foris_guard_stepand_consuming_step_boundstoo, so it needs its own test sweep. Follow-up filed.
Verified
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py'— 272 passed, 0 failed (269 after the first commit, 265 onmain; round 2 adds 3 more tests and flipped no existing one).python3 .github/workflow-pins/check_workflow_pins.py—OK — 11 workflow(s) declare workflows_ref, none with a default, every ref checkout guarded (0 exempt), exit 0. Unchanged by round 2, which is the load-bearing check on the_binding_step_idwidening: no false failure on any real workflow.test-workflow-pins.ymlsmoke tests re-run locally (reintroduced default fails; unguarded checkout fails; unreadablesteps:warns without failing) — the full CI job reproduced green._steps_output_sites(allNone) and of the fourif:spellings against coverage (three flow-inline uncovered, one block-key covered).Residual
Flow-form step
if:earns no coverage credit. A checkout written as one flow mapping is now correctly judged, but if itsif:is written inside that same flow mapping it can never satisfy_skips_on_empty_output, so such a step is always reported unguarded even when itsif:is semantically the exact non-empty test. Fail-closed and consistent with the flow-form binding rule. Round 2 closed the readability half the panel objected to — the BE-8130 error text and the README now both name the flow-inlineif:explicitly and give the one-line remedy — so a consumer hitting it no longer reads "add the exactif:" as advice they have already followed. Teaching_skips_on_empty_outputthe flow spelling itself remains a separate change, deliberately not made here: a flow spelling may DEMAND a guard, never excuse one.Unrelated, pre-existing, untouched:
python3 .github/agents-md-integrity/check_agents_md.py --root .fails on this repo's own tree —AGENTS.mdis 305 lines against a 200-line hard ceiling — identically onmainbefore this branch. This repo is not self-enrolled for that checker so nothing gates on it here, and it is outside this ticket's scope.Unexercised artifacts
The ticket cites its evidence as living in the findings comment of an upstream investigation ticket, which I hold no access to; I re-derived the repro from the ticket body against
main@5f4a22binstead, and it reproduced exactly as described. No attachments or sub-issues were named.Provenance
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py': 272 passed, 0 failed;python3 .github/workflow-pins/check_workflow_pins.py: OK, 11 workflows, exit 0; all threetest-workflow-pins.ymlsmoke tests re-run locally and green; groom 365 / cursor-review 194 / agents-md-integrity 46 all OKunguarded_ref_checkouts-is-empty assertion was not written, because it contradicts the tuple asserted in the same criterion — see "Deviation from the ticket's test 2" above. In review round 2, two of the five panel findings were deferred to follow-ups rather than fixed here (arm selection over un-normalized text;_steps_output_sitesnot scoped towith:) — both pre-existing onmain, with repros and rationale in the round-2 section and on their threads🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.