mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #222] [MERGED] feat(workflow-pins): warn when a job's steps: escapes the step-id pre-scan (BE-9064) #229
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#229
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/222
Author: @mattmillerai
Created: 8/25/2026
Status: ✅ Merged
Merged: 8/25/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-9064-workflow-pins-escaped-steps-warning📝 Commits (2)
daab3c3feat(workflow-pins): warn when a job'ssteps:escapes the step-id pre-scan (BE-9064)952d1b9fix(workflow-pins): tighten the escaped-steps:warning after review (BE-9064)📊 Changes
4 files changed (+545 additions, -49 deletions)
View changed files
📝
.github/workflow-pins/README.md(+21 -2)📝
.github/workflow-pins/check_workflow_pins.py(+223 -19)📝
.github/workflow-pins/tests/test_check_workflow_pins.py(+263 -28)📝
.github/workflows/test-workflow-pins.yml(+38 -0)📄 Description
ELI-5
The pin lint has one job: make sure no reusable workflow here can check out at a mutable ref. One part of it — the dangling-ref check — quietly gives up on a job whose
steps:is written in a shape it can't read (steps: [ … ]on the key line, an anchor, or no-item below it). Giving up is the right call: guessing there would fail perfectly good workflows. The problem was that it gave up silently — the lint printed "every ref checkout guarded" and exited 0, so rewriting a job'ssteps:into flow style was an in-band way to switch that job's check off with nothing in the output to say so. This PR makes the lint say it out loud, in a::warningthat shows up in the PR annotations and does not fail the run.What changed
check_dirnow returns a fourth value,notices— annotation-ready strings that never touch the exit status — andmainprints them after thechecked …lines and before any errors. One::warningper job, naming the job, pointing at itssteps:line, and counting theref:sites that were passed unjudged. When any warning fired, the OK summary drops its "every ref checkout guarded" claim for "every JUDGED ref checkout guarded (…); N job(s) skipped the dangling-ref check".return 1 if errors else 0is unchanged, and the drop itself is unchanged — a fail-closed escape would manufacture false CI failures out of a pre-scan that could not run, which is the failure mode this deliberately does not trade for.It fires only where the escape actually cost a site its verdict. A flow-
steps:job that never reads asteps.<id>.outputs.<name>never reaches the drop branch and stays silent, and neither does a site that still came away with a verdict because a sibling operand named a tracked resolver — so oneref:line can never carry both an::errorand a::warningsaying it went unjudged. A warning marks lost coverage rather than a shape, and says so: the message reads "this is lost coverage, not a finding", because with the pre-scan defeated a dangling id and a real earlier out-of-scope step are indistinguishable and the readable path drops the second one silently too.Collection rides on the walk
check_diralready makes for the self-pin carve-out, as an optionaldropped=out-parameter onref_checkouts— no third walk, and no return-shape change toref_checkouts/unguarded_ref_checkouts/find_unguarded_ref_checkouts(49 call sites read those)._record_steps_outputnotes the escape at the existingif step_ids is None:branch and appends(job_start, job_indent, idx)once, after the loop, only for a site it leaves with nofoundentry; nothing else in the verdict logic moved. The out-parameter is one bundle,drop=(job_start, job_indent, out_list), so the list cannot travel without the job coordinates the annotation needs.Every annotation
check_dirbuilds — the nine pre-existing::errorlines as well as the new::warning— now runs itspath/namethrough_ann_prop/_ann_msg. Those values come from a directory listing, and git permits newlines and commas in a filename; unescaped, a crafted name walks out offile=or starts a log line with::stop-commands::and suppresses every annotation after it — which, now that notices print ahead of the errors, means the error annotations. A no-op for every name a real workflow file has..github/workflows/test-workflow-pins.ymlgains a third smoke step alongside the two that already pin the failing paths, because either half of this is a silent regression on its own: a warning that starts failing the run, or a drop that goes quiet again. It asserts exit 0 and::warningin the captured stdout.Judgment calls / deviations from the plan
ref:line —_record_steps_outputappends at most onefoundentry per call, keyed on the line — so two flow-style checkouts written on one physical line count once, which is exactly the shape that triggers this warning. Rather than print a number a reader can falsify against the job's step count, the message says "%d `ref:` site(s) in this job — one per `ref:` LINE, this lint's unit throughout — and the checkout(s) they feed were passed unjudged". Documented in the README and pinned bytest_a_warning_counts_the_sites_the_job_lost(a flow sequence broken over two physical lines counts 2). This is a documented property of the annotation, not an unfixed defect: no site is lost, only merged in the count.lines[job_start].strip().rstrip(':'). Two things are wrong with that trim.build: # the mapperis legal YAML and it prints the comment inside the job's name; andbuild: &common— an anchor on the key line, which is one of the three escape shapes this very message enumerates, so a routine way to reach the annotation — prints "job `build: &common`". The name now goes through the module's quote-aware_strip_commentand is then cut at the key's own mapping colon, found on the shared_quote_maskscan so a quoted key that contains one ("deploy: prod":) is cut at its delimiter rather than inside its name. Output is identical for every ordinary key.ref:line, the unit_record_steps_outputitself works in — and only for a site left with nofoundentry;check_dirstill groups by job and counts distinctidx, so the count stays a count of lines if a line is ever walked twice.test_this_repos_own_workflows_passnow bindsnoticesand asserts== [], turning "zero warnings on the real tree" from a one-off manual check into a regression gate."$dir/out.txt"rather thanout.txtin the checkout root.Verification
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py'→ 265 passed, 0 failed (255 before this change, +10). The other three suites in this repo (cursor-review194,groom365,agents-md-integrity) pass unchanged, andshellcheck -xon the bump-callers scripts is clean.python3 .github/workflow-pins/check_workflow_pins.pyon the real tree → exit 0, zero warnings, summary unchanged verbatim. Swept everysteps:key in.github/workflows/: 68 total, 67 bare block sequences, 1 value-on-key-line — and that one is the fixture inside the new smoke step's heredoc, not a real job. So nothing in this tree is currently escaping the pre-scan, and the not-fixed portion of that sweep is zero.steps: [{id: resolve, run: echo hi}, {uses: actions/checkout@abc, with: {ref: "${{ steps.typo.outputs.ref }}"}}]) now prints the::warningand still exits 0. The identical typo'd id under a block-sequencesteps:still produces the existing dangling error and no warning (test_a_readable_steps_job_emits_no_notice) — so the message's claim "this job was not judged" is falsifiable, and it checks out in the direction that proves the message right rather than merely asserting it.:strip; making it a naive first-colon cut (caught by the quoted-key test); making the drop per-operand and unconditional again; removing the annotation escaping.::warningpresent); the workflow file parses as YAML.Not exercised
The investigation ticket this cites (BE-9045) and its findings comment live in Linear, which this run has no access to. The reproduction above was rebuilt from the description alone and confirmed here against the real checker, so the behavior is verified first-hand; the original evidence comment is not. The precondition — that #221 (
fix(workflow-pins): scope the step-id pre-scan to the step key column) is merged — was exercised: it is onmainas9efb339, this branch is based on it, and theNoneescape it introduces is what the new tests fire on.AGENTS.mdfailscheck_agents_md.py --root .at 305 lines against a 200-line ceiling. That is pre-existing onmain(identical there), this repo is not self-enrolled in that check, and nothing in this diff touches it.Review round (post-panel)
Four of the five panel findings are fixed in
952d1b9(job-name cut at the mapping colon; record once per unjudged site rather than per operand; bundle the out-parameter so it cannot travel without its job coordinates; escape annotation values). The fifth — asteps:line carrying both aWORKFLOWS_REFbinding and a step-outputref:— reproduced as an upstream site-detection gap, not a dodge of this notice: the site is recorded neither infoundnor indroppedeven under a fully readable blocksteps:, so the notice is silent because nothing was dropped, because nothing was ever found. Closing it changes verdicts, which this PR's contract excludes; deferred to a follow-up with the reproduction.One thing a human should settle: the branch, PR title and commit subjects say BE-9064, while every in-code and README reference — including the user-facing "See BE-9045." in the warning itself — says BE-9045. One of the two is wrong and this run holds no Linear access to tell which, so nothing was renamed.
Provenance
python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py': 265 passed, 0 failed; the repo's other suites (cursor-review194,groom365,agents-md-integrity) pass andshellcheck -xon the bump-callers scripts is clean;python3 .github/workflow-pins/check_workflow_pins.pyon the real tree: exit 0, 11 workflows checked, 0 warnings, 0 errors, summary line unchanged verbatim; four mutation checks each caught by exactly the intended test and no other; the CI smoke step run locally: exit 0 with::warningpresent.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.