mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #202] [MERGED] fix(workflow-pins): report a step-output ref: whose id cannot resolve, and read the || fallback spelling (BE-8220) #210
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#210
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/202
Author: @mattmillerai
Created: 8/20/2026
Status: ✅ Merged
Merged: 8/21/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-8220-dangling-step-id-and-or-fallback📝 Commits (5)
f481ca5fix(workflow-pins): stop the step-output reader failing compliant spellings (BE-8130)61cbc05fix(workflow-pins): report dangling step-output refs and read the||fallback spelling (BE-8220)b68df60fix(workflow-pins): keep the dangling check to real checkouts, and judge any leading operand (BE-8220)1c66f7ffix(workflow-pins): keep the scope gate ahead of operand order, and read awith:the YAML nests differently (BE-8220)793bfaeMerge branch main into matt/be-8220-dangling-step-id-and-or-fallback📊 Changes
3 files changed (+852 additions, -62 deletions)
View changed files
📝
.github/workflow-pins/README.md(+51 -3)📝
.github/workflow-pins/check_workflow_pins.py(+417 -41)📝
.github/workflow-pins/tests/test_check_workflow_pins.py(+384 -18)📄 Description
STACKED — merging lands on
matt/be-8130-step-output-reader-spellings(owned by #197's author, mattmillerai), NOTmain. GitHub retargets this PR tomainautomatically when #197 merges; do not merge it ahead of #197.ELI-5
check_workflow_pins.pyfollows aref:through a step output (BE-8130, #195/#197) so the resolve-then-consume checkout idiom is linted. But two spellings still fell out of the lint entirely, and both were verified empirically by the BE-8215 spike: aref:naming a step id that does not exist before the checkout (a typo, a step in another job, or a resolver declared below its consumer) was silently dropped — even though at runtime that expression is''andactions/checkoutreads''as the default branch, running the checkout unconditionally at a mutable ref — andref: ${{ steps.x.outputs.ref || 'main' }}matched none of the step-output patterns, recording nothing. This closes both drops: the first becomes an error in its own right with its own message, and the second is read and judged by operand order exactly as the input side already is.What changed
_job_step_ids). The walk-orderresolversmap cannot tell "an earlier step this lint has no claim on" from "no such step at all", and the two demand opposite answers. The pre-scan collects every step id in the job — block form with the same- id:list-marker normalization_binding_step_idmakes, plus a flow-form pattern so- {id: x, …}steps are seen — because an id the scan misses becomes a false FAILURE under the new check, while over-collection merely reproduces the old fail-open drop for that one site._record_steps_outputis split. An id that exists and is declared before the consuming step stays out of scope (an unrelatedgit rev-parse, a release lookup). An id that is absent, in another job, or declared at/after the consumer is recorded unguarded withvia_step_output='dangling'— whose truthiness keepsunguarded_ref_checkoutsunchanged. The boundary is the consuming STEP's first line, not theref:line, so a step's ownid:cannot excuse a ref consuming it (its output does not exist during its ownwith:evaluation).with:INPUT is judged (_is_ref_input). The walk asks every line of the job about step-output refs, soref:keys that are not action inputs reach the same code path: a job-leveloutputs:mapping (which conventionally sits abovesteps:, and therefore above every step id) and aref:line inside arun:heredoc. Both used to be dropped harmlessly; under the dangling check they instead hard-failed a compliant workflow with an error naming a mutable checkout ref where there is no checkout at all. The gate is the one thing every real site has and neither of those does — a checkout'sref:is an entry of its step'swith:mapping — restoring the old drop for the rest.check_dir, referencing BE-8215, naming the actual id and output rather than<id>/<out>placeholders. The BE-8130 remedies are deliberately not reused — neither fixes a nonexistent id; the message says to fix the id or move the resolving step above the checkout, and a test asserts the BE-8130 remedy text is absent from it._STEPS_OUTPUT_BODYreads||continuations, before and after the output name, keeping the namedid/outgroups and #197's_outside_quotesdecoy loop. The block and continuation bodies are bounded by[^}];[^,}]is the FLOW form's entry boundary and is kept only there, since carrying it into the other two left a fallback containing a comma (|| 'a,b') matching nothing at all.[^}]still cannot cross a}}, so a match can never span two interpolations._leading_operand_reaches_input. When the expression STARTS with the step output, the site is covered by the usual routes (under the exactif:the fallback arm is unreachable dead code, sosteps.x.outputs.ref || 'main'passes). Otherwise it is a'non-leading'site with its own message:'main' || steps.x.outputs.refresolves to the literal on every runner, andA && steps.x.outputs.refresolves to the output or to'', so noif:and no resolver change can cover either — the message is worded for any leading operator, not just||. A leading operand||falls through (false,'',"",0,null) still reaches the output and is judged normally, rather than being failed with an error no edit can satisfy. Judged from theleadgroup of the same match rather than the line's first interpolation, so a quoted decoy sibling cannot skew the verdict, and judged before step existence, since a leading operand that wins makes "no such step" a red herring.ref: >-continuation forms, a flow-style- {id: …}resolver's consumer not being falsely reported dangling, the job-leveloutputs:andrun:-heredoc non-checkouts, a comma-bearing fallback, every falsey leading operand, and the&&wording. The earlier-unrelated-step and repo-real-workflows tests still pass unchanged.||fallbacks are read here with leading-operand judgment, whichref:lines are in scope at all — and keeps (narrowed to the BINDING precondition) the warning that an unrecognized resolver is silent, plus the ref spellings this reader still does not recognize.Verification
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py'— 139 passed, 0 failed (123 on the #197 base).shellcheck -xclean.python3 .github/workflow-pins/check_workflow_pins.py— green on the repo's real workflows: 10 workflows, every ref checkout guarded, the 16-site count unchanged, 0 exempt.'main' ||leading), andsteps.x.outputs.ref || 'main'under the exactif:produces none — each pinned by a test.with:gate, the[^}]widening, the falsey-lead judgment, the non-leading state, the id interpolation, and the operator-agnostic wording — each fails exactly the tests that name it and nothing else.Review round
The panel's High finding was real and reproduced before fixing: a compliant job declaring
outputs: {ref: ${{ steps.pick.outputs.ref }}}hard-failed with the dangling error, on a workflow containing no checkout. Fixed by_is_ref_inputabove, gating all of_record_steps_outputrather than only the dangling verdict —_consuming_step_bounds is not None(the suggested gate) does not separate the two over-matches, since a heredoc line resolves its-list item perfectly well. Also fixed from the panel: the unactionable message on a non-leading operand, the falsey leading operand, the block form's inherited[^,}]bound, the placeholder-only dangling text, and the over-deleted README warning.Not fixed here (pre-existing or deferred, with follow-ups filed)
_GUARD_BINDING_REbinding match, and a hard-guard resolver's consumer exemption never proves the value written to$GITHUB_OUTPUTis non-empty. The panel raised the second again in its new combination (steps.r.outputs.ref || 'main'passing via a hard-guard resolver that writes a differently-named output); the outcome there is unchanged from before this branch — that spelling previously matched nothing and passed silently — so nothing regressed, and the fix belongs with the rest of that residual.|| format('refs/heads/{0}', 'main')), a parenthesized operand,&&rather than||, and any line holding more than one interpolation or more than one step-output operand (only one match is judged). Documented in the README; a follow-up proposes a two-tier reader so an unknown spelling fails LOUD rather than vanishing. Widening the regex further inside this PR is the wrong trade — a bad widening turns a silent miss into a false CI failure in every consumer repo, which this PR already had to fix once.id:underwith:, orecho "id: x"in arun:, registers as a declared step and silences the dangling check. Deliberate here (an id the scan MISSES is a false failure; one it over-collects is the old drop), with a follow-up to scope it to the step key column paired with a fail-loud escape.Refs BE-8220 — kept open rather than closed only because this body names unfixed (BE-8130-tracked) residuals in the same file the ticket names; BE-8220's own two reported drops are both fixed and verified, so it can be closed once a human confirms the residuals stay BE-8130's.
Unexercised artifact: the BE-8215 spike ticket itself (unreachable from this sandbox) — its verified findings are reproduced in BE-8220's body, which is what this change was built and verified against. The precondition on PR #197 was handled by stacking on its branch rather than waiting for it to merge.
Provenance
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py': 139 passed, 0 failed; cursor-review 194, groom 365, agents-md-integrity 46, all green;shellcheck -xclean;python3 .github/workflow-pins/check_workflow_pins.py: green (10 workflows, every ref checkout guarded, 16 sites, 0 exempt); 11 mutation checks, each failing exactly the tests that name itsteps.<id>.outputs.<out>placeholders (as the ticket's own wording does); the review round replaced them with the actual id and output. Two panel findings were deferred to follow-ups rather than fixed here (unrecognized ref expressions; step-id pre-scan scope), both because widening this reader carries a false-CI-failure risk across every consumer repo that a separate change can prove out.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.