mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #193] [MERGED] fix(workflows): resolve asset checkouts from job.workflow_sha, not the empty github.job_workflow_sha (BE-8077) #202
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#202
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/193
Author: @mattmillerai
Created: 8/20/2026
Status: ✅ Merged
Merged: 8/20/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-8077-job-workflow-sha-fallback📝 Commits (5)
3cf1621fix(workflows): resolve asset checkouts from job.workflow_sha, not the empty github.job_workflow_sha (BE-8077)8a506b5fix(workflow-pins): make the job.workflow_sha guards load-bearing, and fail closed on Unicode whitespace (BE-8077)32e8159fix(workflow-pins): track guard STRENGTH, and anchor the self-pin at both ends (BE-8077)302ff5efix(workflow-pins): anchor the self-pin to the whole ref value, and carry alias strength (BE-8077)8e78bbbfix(workflow-pins): drop alias strength as unsound, judge the leading operand (BE-8077)📊 Changes
12 files changed (+1652 additions, -192 deletions)
View changed files
📝
.github/groom/README.md(+22 -8)📝
.github/workflow-pins/README.md(+120 -11)📝
.github/workflow-pins/check_workflow_pins.py(+295 -46)📝
.github/workflow-pins/tests/test_check_workflow_pins.py(+601 -18)📝
.github/workflows/cursor-review.yml(+65 -11)📝
.github/workflows/groom.yml(+438 -36)📝
.github/workflows/pr-risk.yml(+27 -19)📝
AGENTS.md(+13 -5)📝
CONTRIBUTING.md(+5 -3)📝
README.md(+17 -8)📝
docs/callers/README.md(+15 -10)📝
docs/callers/groom.md(+34 -17)📄 Description
ELI-5
Groom and cursor-review load their prompts and scripts out of this repo at run time. When a caller didn't pin
workflows_ref, they were supposed to fall back to "whatever commit the caller'suses:line pointed at". They asked for that commit by the wrong name —github.job_workflow_sha, which isn't a thing GitHub Actions knows about — and Actions quietly hands back an empty string instead of complaining.actions/checkoutreads an emptyrefas "give me the default branch", so those jobs were checking out whatever happened to be onmainat that moment, inside jobs holdingANTHROPIC_API_KEYand a GitHub App token. This PR asks for the commit by its real name (job.workflow_sha) and adds a guard so that if it ever is empty, the run stops loudly instead of silently grabbingmain.What changed
groom.yml— 7 asset checkouts (gate,audit_find,audit_verify,build_select,file,build,build_pr) now readref: ${{ inputs.workflows_ref || job.workflow_sha }}. Each is preceded by a fail-closedRequire a resolvable workflows_refstep that binds the resolved value throughenv:, then rejects two ways: an emptiness test on the whitespace-stripped value (actions/checkoutreadsrefthroughcore.getInput, which trims — so a whitespace-only value is empty to it while sailing past a bare-z), and a positive shape test on the raw value (case "$WORKFLOWS_REF" in *[!A-Za-z0-9._/@+-]*)). The second was added in review: POSIX[:space:]is ASCII-only while the JStrim()behindcore.getInputalso strips U+00A0/U+FEFF, so a ref made purely of those cleared the emptiness test and still reached checkout as''. The checkout consumes the raw expression, so the value has to be rejected rather than sanitized. Either branch emits an::error::naming the input andjob.workflow_sha, says a runner older than v2.334.0 is the only way it is empty, andexit 1. The comparison runs underexport LC_ALL=C: bracket ranges match by collation order, so under a glibc locale[A-Za-z]also matches accented letters and the negated class quietly stops rejecting them — and the stale self-hosted runner this guard exists for has an unknown locale. The rejection is worded as a policy restriction, not a claim that the ref is invalid to git: the class is deliberately narrower than git's own rules, which permit(,),,,%and UTF-8. Shape copied from the BE-5546 guard incursor-review.yml. The guard is inlined per job rather than factored into a composite action for the reason.github/workflow-pins/README.mdalready gives: a composite would have to be loaded from the very ref being validated.cursor-review.yml— thePrior-review ledgerjob. This job must never fail, because the review matrixneeds:it, so it gets noexit 1. A newresolve_refstep validates the ref's shape and emits it untouched into a step output — resolving anything non-ref-shaped to the empty string — and::warning::s when the result is empty. (It emitted the stripped value until review:tr -dremoves interior whitespace too, soa bwas silently rewritten into the different-but-real refabinstead of being rejected.)Load cursor-review assetsis gated onif: steps.resolve_ref.outputs.ref != ''and readsref: ${{ steps.resolve_ref.outputs.ref }}. With the checkout skipped,Build prior-review ledgerfails under its existingcontinue-on-error: trueand the existingEnsure ledger artifact existsfallback (id: fallback,if: always()) publishesstatus=unknownplus its own warning — no new fallback logic was needed. The new step carriestimeout-minutes: 2so it stays under the job timeout, per that job's stated design ("a hung step must trip its OWN timeout, not the job's").check_workflow_pins.py— the BE-4169 exemption now keys onjob.workflow_sha, and the oldgithub.job_workflow_shaspelling is no longer exempt in either place it was honoured (the checkout carve-out and thedefault: ''carve-out), so the mistake cannot come back with the lint green.Review hardened three things about that exemption:
}}, and matched against the comment-stripped line. Unanchored it read "contains the fallback" rather than "IS the fallback", so${{ inputs.workflows_ref || job.workflow_sha || 'main' }}was exempted by both callers of the regex — and that resolves to the mutable default branch in precisely the pre-v2.334.0 case the fallback exists for.check_dir's file-wideany(...)was looser still: a comment merely naming the expression bought thedefault: ''carve-out._GUARD_BINDING_REwidened to accept theWORKFLOWS_REF: ${{ inputs.workflows_ref || job.workflow_sha }}binding groom's seven guards use. Matching only the bare form meant none of the seven was ever consulted byis_guard_step, and falsely reported a checkout that dropped the fallback while keeping its guard.default: ''carve-out); the guard answers emptiness. Exempting the fallback from both meant deleting all seven of groom's guard steps left the lint and its whole suite green — measured, not theorised — while the file's own comments already leaned on them. Unguarded fallback checkouts get their own BE-8077 message rather than the BE-5546 one.A second round tightened three more edges on that same exemption:
${{ inputs.override || inputs.workflows_ref || job.workflow_sha }}resolves to whatever the leading operand names, and no runtime guard catches it, because a guard proves non-emptiness rather than immutability.default: ''carve-out is scoped to ref-checkout lines. Asking "does any line self-pin?" of the whole file granted it to any file merely mentioning the expression in code — most sharply the guard steps' ownenv:binding._ENV_ALIAS_REalso learned the fallback spelling, so hoisting that binding to a job-levelenv:keeps coverage instead of silently dropping to zero, andcheck_dirnow reads the fallback flag the parser determined rather than re-reading the reported line (a block-scalarref:key never holds the expression, so that path always emitted the wrong message).Suite is 80 -> 90 tests: both directions of the interlock (delete the guards -> 7 errors; fallback guard + bare checkout -> reported; bare guard + fallback checkout -> clean), the leading- and trailing-operand holes, the alias refactor, a per-step check that all seven groom guards carry their shape test and are byte-identical (deleting all seven
caseblocks previously left everything green), and a backstop for the ledger checkout, which left the lint's coverage when it moved to a step output.Docs —
AGENTS.md,README.md,CONTRIBUTING.md,docs/callers/README.md,docs/callers/groom.md,.github/groom/README.md,.github/workflow-pins/README.md, plus theworkflows_refinput description and header caller example ingroom.yml. The "leave it unset" advice is now true rather than aspirational, and the "pin it until groom's checkouts move tojob.workflow_sha" instructions are flipped. Wherever the accessor is documented there is now a note thatactionlint≤ 1.7.12 false-positives onjob.workflow_sha(itsjob-context schema predates runner v2.334.0); nothing in this repo's CI runs actionlint, so nothing gates on it.Sweep
job_workflow_shaappeared on 54 lines across 11 files onmain. After this PR: 38 lines across 11 files, and zero of them are live${{ … }}expressions outside the two lint test fixtures that deliberately assert the old spelling is rejected. Every remaining mention is prose that names the old spelling to contrast it with the correct one, or to explain what a caller pinned at a pre-BE-8077 commit still gets.That sweep also turned up three stale prose claims in
pr-risk.yml(and the matchingREADME.mdcatalog row) asserting that "the runner does not expose [the commituses:resolved to] to the workflow" and that the answering value "exists only as an OIDC token claim". That is no longer true —job.workflow_shaanswers it and needs noid-token: write. The prose is corrected, but thepr-risk.ymlcheck itself is deliberately NOT wired up: assertingworkflows_ref == job.workflow_shathere would fail red on every caller whose two pins have already drifted apart, which is a caller-contract change well outside this ticket. It is now recorded as a wiring gap rather than a runner limitation.Verification
workflows_refset (every current fleet caller) is byte-identical:||short-circuits to the same string, the checkout inputs are unchanged, and the only new work is a guard step that passes.The core premise was confirmed against a real public run rather than asserted —
Comfy-Org/comfy-cligroom run32240568032,groom / Gatejob96029967905(public repo, read-only log fetch):WORKFLOW_SHA: eb4b26c9bc13feea5ae76c4334370158713fb23a—job.workflow_sharesolves to a real 40-hex SHA onubuntu-latest.workflows_refthe caller passed, which equals the commit itsuses:line pins — i.e.job.workflow_shareally is "the commit the caller'suses:resolved to", which is the whole basis of the fallback.job.workflow_sha is emptywarning did not fire (no##[warning]annotations in that job at all).Judgment calls
inputs.workflows_ref || job.workflow_shanon-empty says nothing about a later bareref: ${{ inputs.workflows_ref }}checkout in the same job — is correct. Round one widened the binding regex anyway, on the mistaken argument that the fallback made the later checkout safe too; it does not, because that checkout has no fallback of its own and receives''directly. Round two restored the original insight without giving up what the widening bought:_GUARD_BINDING_RErecords which expression each guard validated, and coverage is granted by strength — a bare guard covers every checkout in its job, a fallback guard covers fallback checkouts only. So groom's seven guards are consulted (they were not before, which made them deletable with CI green) and a de-fallbacked checkout under one of them is still correctly reported.docs/callers/groom.md's caller snippet keeps its explicitworkflows_ref:(now annotated as optional) even though the row and prose say leaving it unset is safe.bump-callers.shmoves that line and theuses:pin in one pass for roster-enrolled callers, andAGENTS.mdrecords double-pinning as the groom convention; changing the recommended snippet's shape is not something this ticket's evidence covers.groom.yml's own header example keeps its# No workflows_refform, per the ticket.ubuntu-latestby construction.Not fixed here (why this says
Refs, notCloses)actions/checkout@v7andactions/upload-artifact@v7in the ledger job are now pinned by full SHA: they sit in the very job this PR rewrites, and a mutable action tag inside the job whose point is loading assets from an immutable commit undercuts the guarantee. Still bare and left alone: twoactions/download-artifact@v8elsewhere incursor-review.yml(lines 1310, 1556) andactions/checkout@v7/actions/setup-python@v7intest-refresh-reviewers.yml. All six are a regression from the Dependabot major-group bump in #182, which replaced full-SHA pins with floating tags — againstAGENTS.md's "pin everything by full commit SHA". The remaining four are untouched by this PR and unrelated to the asset-checkout path; they want a repo-wide sweep that also fixes the Dependabot config, not a rider on this diff.AGENTS.mdis 273 lines against its own 200-line hard ceiling (python3 .github/agents-md-integrity/check_agents_md.py --root .fails on it today). Pre-existing; this repo does not self-enrolagents-md-integrity.yml, so nothing gates on it. My edits there are net +9 lines and I did not trim the file, because deciding what to cut from a repo's own conventions doc is not this ticket's call.Both of those sit on artifacts the ticket names (
cursor-review.yml's ledger checkout,AGENTS.md), so the closure claim is downgraded: the ticket stays open to carry them.Unexercised artifacts
The upstream investigation ticket this was written from (BE-8069) and its findings comment are not reachable from this environment, so the evidence in it was taken as given and re-derived independently from the run log cited above rather than read.
Post-merge
No manual dispatch needed:
bump-groom-callers.ymlandbump-cursor-review-callers.ymlboth carrypush: branches: [main]withpaths:covering their own reusable workflow file, so both fleets re-bump themselves on merge. Confirmed by reading both trigger blocks.Refs BE-8077
Provenance
check_workflow_pins.py --workflows-dir .github/workflows: OK, 10 workflows declareworkflows_ref, none with a default, every ref checkout guarded, 0 problems. Unittest: workflow-pins 102 passed (80 on the first push; 90, 98, 102 across three review rounds), groom 365 passed (1 skipped), cursor-review 194 passed, agents-md-integrity 46 passed.yaml.safe_loadparses both edited workflows;py_compileclean on both edited Python files.Round 4 reverted a round-3 change of mine as unsound, on evidence rather than assertion. Carrying an
env:binding's strength toref: ${{ env.NAME }}was reproduced failing in both directions before the revert: a fallback binding in a guard step'senv:scored the sibling checkout(14, True, True)— a guarded self-pin — although step-levelenv:never reaches a sibling step, so it expands to''and takes the default branch; and a step-localWORKFLOWS_REF: ${{ inputs.workflows_ref || 'main' }}scored(16, True, True)by inheriting strength from another step's strict binding.cursor-review.ymlbindsWORKFLOWS_REFboth ways today (line 420 with the fallback, six more without), so the cross-talk was live. An alias is judged bare now — fail-closed.The refactor that change existed to serve turned out to be impossible, which is why the revert is total rather than a scope fix:
jobis not available injobs.<job_id>.env(actionlint 1.7.12 on${{ job.status }}, chosen to isolate context availability from thejob.workflow_shaschema staleness — rejected at job level, accepted at step level), and a step-levelenv:does not reach a sibling step.Also corrected: a round-3 deferral whose stated reason was wrong.
ref: ${{ 'main' || inputs.workflows_ref }}linted clean behind a bare guard with no second input declared —||returns the first truthy operand — so the hole was reachable, and the leading-operand check closes it rather than re-wording the excuse.Every fix across rounds 3 and 4 is mutation-tested: reverting any one of them fails a named test — value anchoring, quote-aware flow matching, comment-stripped alias binding, the leading-operand check, the block-scalar carve-out, comment stripping, alias strength (now the reverse assertion), and the two
cursor-review.ymlguards (deletingexport LC_ALL=Cfromresolve_refalone, and swapping its binding for|| 'main'). The quadratic carve-out was measured, not inferred: restoring the per-line comprehension takes the suite from 0.08s to 4.04s.Not run:
.github/coderabbit-config/tests— fails to import on a missingjsonschemamodule locally, pre-existing and unrelated (path-filtered to a directory this PR does not touch; its CI job is green). The glibc collation behaviour behindLC_ALL=Ccannot be reproduced on macOS libc, which rejects the accented case either way.[A-Za-z0-9._/@+-], deliberately narrower than git's own ref rules — a policy restriction that keeps every shell and::workflow command::metacharacter out. It covers a 40-hex SHA and any branch/tag this input is documented to carry; a legal-but-excluded ref such asfeat(groom)/xfails closed loudly, and the message says it is policy rather than validity.pr-risk.yml's current-pin equality check is documented as now-possible but deliberately left unwired (caller-contract change, out of scope). Droppingworkflows_reffromci-groom.ymlto give the fallback CI coverage was considered and declined: that caller documents itsuses:andworkflows_refpins as byte-identical andbump-callers.shrewrites both in one pass. No live groom/cursor-review run was triggered — both are credit-spending and state-mutating.Three reviewer findings are deferred to follow-up tickets, each with a full plan rather than a note: teaching the lint to follow a
ref:through a step output (a new detection capability, not a correction —cursor-review.yml:456is covered only by a per-site grep today); three spellings that leave a checkout invisible (flow-formenv:, bracket access, env-to-env chains — none used in this tree, and the_CONSUMES_*backstop cannot catch them because it only fires when the input declaration is unparseable); and block-scalar blindness, where arun:script can forge a guard binding and mark its whole job guarded. All three are pre-existing and unreached by anything in the tree; closing the last one means teaching the parser block-scalar extents, which touches every scan in the module.Two bare
actions/download-artifact@v8refs incursor-review.ymlwere pinned to the SHA already written out elsewhere in that file, finishing the sweep this PR started there. The two intest-refresh-reviewers.ymlare left: a different file, untouched here, wanting the repo-wide sweep that also fixes the Dependabot config behind the #182 regression.AGENTS.mdis over its own 200-line ceiling. Pre-existing (273 onmain, and this repo does not self-enrolagents-md-integrity.yml, so nothing gates on it); not trimmed here, because deciding what to cut from a repo's conventions doc is not this ticket's call.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
ref:whose id cannot resolve, and read the||fallback spelling (BE-8220) #210ref:expression the step-output reader cannot parse (BE-8253) #211