mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #201] [MERGED] fix(workflow-pins): drop the hard-guard resolver exemption and scan the marker line in is_guard_step (BE-8221) #209
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#209
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/201
Author: @mattmillerai
Created: 8/20/2026
Status: ✅ Merged
Merged: 8/21/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-8221-guard-marker-line-resolver-exemption📝 Commits (7)
7603faafix(workflow-pins): drop the hard-guard resolver exemption and scan the marker line in is_guard_step (BE-8221)c0e3689fix(workflow-pins): normalize any list marker, and see the marker line in _skips_on_empty_output and _step_bounds (BE-8221)caf36e4fix(workflow-pins): anchor step bounds to steps:, gate marker-line bounds, and see the flow binding (BE-8221)b0268c1Merge origin/main into matt/be-8221-guard-marker-line-resolver-exemption67261c9Merge remote-tracking branch 'origin/main' into matt/be-8221-guard-marker-line-resolver-exemption872fed5Merge remote-tracking branch 'origin/main' into matt/be-8221-guard-marker-line-resolver-exemptionc7906e3fix(workflow-pins): don't let a flow-form guard excuse a direct checkout (BE-8221)📊 Changes
3 files changed (+616 additions, -104 deletions)
View changed files
📝
.github/workflow-pins/README.md(+21 -14)📝
.github/workflow-pins/check_workflow_pins.py(+237 -80)📝
.github/workflow-pins/tests/test_check_workflow_pins.py(+358 -10)📄 Description
ELI-5
Some of our workflow jobs figure out which version of our scripts to download in one step ("the resolver") and do the actual download in a later step. The lint that polices this used to say: if the resolver step checks its own input carefully, the download step doesn't need its own safety check. But checking the input tells you nothing about what the resolver actually hands over — it can validate its input and still pass along an empty value (for example, its clean-up branch blanks a malformed value, or its hand-over line was deleted). Now the download step must always carry its own "skip if empty" check. The lint also used to go blind whenever a step's first key was written on the same line as its
-bullet — which let a step that can never fail masquerade as a safety guard, and, in the other direction, made the lint fail a download step that did carry the right check. That blind spot is fixed in every spelling YAML allows for that bullet.What changed
The hard-guard resolver exemption is gone.
_record_steps_outputno longer accepts "the producing step is a fail-closed guard" as coverage for a step-output checkout — that proves the step rejects an empty input, nothing about the value written to$GITHUB_OUTPUT. Verified holes the old rule passed: a resolver that guards the input then sanitizes a malformed ref to''(the exactcase-pattern shapecursor-review.yml's real resolver uses), a resolver whose output write was dropped or renamed, and a consumer naming an output the step never sets. A step-output site is now covered only by the consumer's exactif: steps.<id>.outputs.<name> != ''.resolversbecomes membership-only (a set), and the remedy text, docstrings, and README now name the one accepted remedy.The
-list marker no longer hides a step's first key — anywhere. A step's first key can be written on the marker line itself, and_indentreads the marker's physical column, so every_indent(line) != key_indentscan skipped it. Four sites, three of which fail open:is_guard_step—- continue-on-error: true(or- if: …) as the first key let a never-fail step score as a hard guard. This affected the classic direct-binding path too, not just the step-output role._binding_step_id/is_guard_stepmarker width — both stripped-as a fixed two-column slice, but YAML allows any run of separation spaces after the-. At dash+3 (- id: resolve) the slice left one space behind and the rewritten key landed one column deep, reopening the hole to anyone who types an extra space._marker_widthnow measures the marker;_unmarkedapplies it at every site._skips_on_empty_output— never taught the marker line at all. With the resolver exemption gone thisif:is the only coverage route, so a consumer written- if: steps.<id>.outputs.ref != ''was reported unguarded and told to add the line it already carried. (Loud, not silent — but it fails the one remedy the lint accepts.)_step_bounds— read the step's key column off the marker's physical indent, so a step whose first key is the binding block itself (- env:withid:/run:below) resolved to no step at all:is_guard_stepfailed closed, but_binding_step_idnever registered the resolver and its consumer'sref: ${{ steps.<id>.outputs.ref }}passed the lint unreported. It now reads the column from where the key actually starts. The step-start scan also accepts a bare-marker (every key on the lines below it) — the same fail-open in its third spelling.endis extracted as_step_endso both_step_boundsexits share one definition.Round 2 (
caf36e4): the marker/step machinery held to the same fail-closed bar. The round-1 fixes taught four scans the marker line; the second panel round found the STEP-RESOLUTION machinery under them could still be steered by things that are not steps, plus two registration gaps that became silent passes once the resolver exemption was gone:_step_boundsnow refuses any list item that is not an entry of asteps:sequence (_in_steps_sequence): astrategy.matrix.includeitem, or step-shaped text inside arun: |block scalar, could carry the binding's shape plus a guard-shaped script and score as a job-wide hard guard — blessing every later unguarded checkout in the job.- with: {ref: …}) resolves to ITS OWN step: the shifted retry used to stop at the PRECEDING step's marker and credit a marker-first flow consumer with its neighbour'sif:— the normal shape of the never-fail idiom's first checkout, so the second checkout inherited a guard it does not carry._env_bindingsnormalizes the marker, so a marker-first- env:block binds aliases too — alias discovery is the one path deciding whether a laterref: ${{ env.<name> }}is SEEN at all. Its block boundary is measured at the key column, so the step's other keys cannot bind false aliases._GUARD_BINDING_FLOW_REregisters a flow-form binding (env: {WORKFLOWS_REF: …}): the old "flow reads as ABSENT, which fails loudly" rationale died with the exemption — absence on the resolver path now means the consumer is never judged at all. A line that is ALSO a ref use stays reported rather than swallowed.is_guard_stepnormalizes the marker line ONCE for both of its scans, so a one-line guard written marker-first (- run: [ -z "$WORKFLOWS_REF" ] && exit 1) is recognized instead of reported with the remedy the author already has._marker_widthanswers 0 for a dash with only trailing whitespace, matching its documented bare-marker contract;_consuming_step_bounds' docstring restates the retry's actual (backstop) role.Verification
if:, the marker-lineenv:resolver, and the bare-marker resolver and consumer).if:still passes), and that normalizing the marker line and the spacing did not relax what is matched there — an OR-widenedif:stays refused in the marker-first spelling too.python3 .github/workflow-pins/check_workflow_pins.py: OK — 10 workflows checked, every ref checkout guarded, 0 exempt.ref_checkoutsover the repo's own 10 workflows is byte-identical before and after — all 25 sites keep their(fallback, guarded, via_step_output)tuple. The tree's only step-output site (cursor-review.yml's ledger job) already carries the exactif:on its consumer, which is also the empirical proof that the one remedy the tightened lint accepts is practical — it is what the real workflow already does.Coordination with #197
#197 (open) now overlaps this PR inside
_skips_on_empty_output, so whichever merges second will hit a real conflict there — this is a change from this PR's original note, which predated the review round below._skips_on_empty_outputthe marker line. #197 uses the fixedline.lstrip()[2:]slice; this PR uses_unmarked, which measures the marker instead and so also covers the dash+3 spelling. Resolution: keep_unmarked.!=(so…outputs.ref!=''passes) and hardenssteps_output_ref's flow-form walk against a quoted-comma decoy. Neither is touched here — keep both.if:-only rule.if:test under different names; either name is fine, keeping one is enough.Judgment calls / notes
continue-on-errorrationale it carried lives on in the new exemption-rationale sentence and inis_guard_step's own comments._step_boundshole was raised in review as pre-existing and Low, with "fail closed in_record_steps_output" as the suggested remedy. Fixed at the root instead: failing closed there would have caught the legitimate not-this-lint's-subject case its docstring describes (agit rev-parseresolver), failing workflows the lint has no claim on.test_the_ledger_resolver_carries_the_shape_check'scontinue-on-error: trueassertion is kept but re-justified — the lint has no opinion on it either way now, which is precisely why the assertion has to stay.Provenance
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py': 143 passed, 0 failed (8 of round 1's new/flipped tests and 10 of round 2's 11 fail against the unmodified checker);python3 .github/workflow-pins/check_workflow_pins.py: OK — 10 workflows checked, every ref checkout guarded, 0 exempt;ref_checkoutsdiffed over all 10 workflows before/after: identical, 25 sites.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.