fix(workflows): resolve asset checkouts from job.workflow_sha, not the empty github.job_workflow_sha (BE-8077) - #193
Conversation
…e empty github.job_workflow_sha (BE-8077)
Seven groom.yml asset checkouts and cursor-review.yml's `Prior-review ledger`
checkout read `ref: ${{ inputs.workflows_ref || github.job_workflow_sha }}`.
`github.job_workflow_sha` is not a property of the `github` context — it exists
only as an OIDC token claim — so Actions expanded it to '' and actions/checkout
read `ref: ''` as this repo's default branch. A caller that omitted
`workflows_ref`, which groom.yml's own input description ("LEAVE UNSET") and
header example instruct, therefore loaded the briefs, ledger.py and interval.py
from a MUTABLE branch into jobs holding ANTHROPIC_API_KEY and the App token.
The populated accessor is `job.workflow_sha` (runner v2.334.0+), which
groom.yml already uses successfully for the agent CLI pin.
- groom.yml: all 7 checkouts move to `inputs.workflows_ref || job.workflow_sha`,
each preceded by a fail-closed `Require a resolvable workflows_ref` guard
copying the BE-5546 pattern — whitespace-stripped (actions/checkout trims
`ref` via core.getInput), `::error::` naming the input, the accessor and the
stale-runner cause, then exit 1.
- cursor-review.yml: the ledger job must never fail (the review matrix needs
it), so a `resolve_ref` step computes the trimmed ref, `::warning::`s when it
is empty, and the checkout is gated on `steps.resolve_ref.outputs.ref != ''`.
The existing continue-on-error build step and `always()` fallback already
publish status=unknown from there — no new fallback logic.
- check_workflow_pins.py: the BE-4169 exemption regex now matches
`job.workflow_sha` only; the old spelling is FLAGGED, with tests covering both
the checkout exemption and the `default: ''` carve-out in each direction.
- Docs swept repo-wide (AGENTS.md, README.md, CONTRIBUTING.md, docs/callers/*,
.github/groom/README.md, .github/workflow-pins/README.md): "leave it unset" is
now true, and pr-risk.yml's prose no longer claims the runner cannot expose
the current-pin commit — it can, via job.workflow_sha; wiring that assertion
in is a caller-contract change and stays deliberately out of scope.
Behaviour with `workflows_ref` set (every current fleet caller) is unchanged:
the `||` short-circuits to the same value and the new guard passes.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 24 minutes Limit details: You’ve used the included review currently available. Your 95 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 8 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 1 |
| 🟡 Medium | 4 |
| 🟢 Low | 3 |
Panel: 8/8 reviewers contributed findings.
…d fail closed on Unicode whitespace (BE-8077)
Review follow-ups on the BE-8077 asset-checkout change.
check_workflow_pins.py
- Anchor `_JOB_WORKFLOW_SHA_FALLBACK_RE` to the closing `}}` and match it
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 users of the regex — and that resolves to the MUTABLE default branch in
exactly the pre-v2.334.0 case the fallback exists for. A comment merely
naming the expression also bought `check_dir`'s `default: ''` carve-out.
- Widen `_GUARD_BINDING_RE` to accept the
`WORKFLOWS_REF: ${{ inputs.workflows_ref || job.workflow_sha }}` binding
groom.yml's seven guards use. Matching only the bare form meant none of the
seven was ever consulted, and falsely reported a checkout that dropped the
fallback while keeping its guard.
- Require a guard even when the fallback is present. The fallback answers
MUTABILITY; the guard answers EMPTINESS (`job.workflow_sha` is '' below
runner v2.334.0). Exempting it from both meant deleting all seven guard
steps left the lint and its whole suite green, while the comments already
leaned on them. Unguarded fallback checkouts now get their own message.
groom.yml / cursor-review.yml
- Validate the ref's SHAPE, not just its emptiness. POSIX `[:space:]` is
ASCII-only while the `trim()` behind `core.getInput` also strips U+00A0 and
U+FEFF, so a ref made purely of those passed all seven guards and still
reached checkout as '' — the default branch, in the jobs holding
ANTHROPIC_API_KEY.
- cursor-review's `resolve_ref` now emits the value UNTOUCHED. `tr -d` strips
INTERIOR whitespace too, so `a b` was silently rewritten into the different
ref `ab` instead of being rejected.
- Pin `actions/checkout` and `actions/upload-artifact` in the ledger job by
full SHA, per AGENTS.md.
Tests: +6 cases, including both directions of the guard interlock and a
backstop for the ledger checkout, which left the lint's coverage when it moved
to a step output.
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Round 2 — ledger: 8 prior finding(s) across 1 round(s) (0 never answered).
Found 8 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 1 |
| 🟡 Medium | 3 |
| 🟢 Low | 4 |
Panel: 8/8 reviewers contributed findings.
…both ends (BE-8077)
Second review round. The previous commit widened `_GUARD_BINDING_RE` to
recognize groom.yml's seven guards and, in doing so, opened a hole the original
PR body had correctly predicted — 7 of 8 panel reviewers caught it.
- **Guard strength is now tracked (High).** A guard binding
`${{ inputs.workflows_ref || job.workflow_sha }}` proves only that the OR
EXPRESSION is non-empty: with the input omitted it passes on
`job.workflow_sha` while a sibling `ref: ${{ inputs.workflows_ref }}` in the
same job still receives '' and checkout takes the default branch. Treating any
recognized binding as blanket job-wide coverage put that behind a green lint,
and the previous commit's
`test_the_fallback_guard_also_covers_a_bare_input_checkout` asserted the
silence. `_GUARD_BINDING_RE` now captures which expression was validated; a
bare guard covers every checkout, a fallback guard covers fallback checkouts
only. That test is inverted, and the bare-guard-covers-fallback direction is
pinned alongside it.
- **The self-pin pattern is anchored at both ends.** Anchoring only the tail
still admitted a LEADING operand —
`${{ inputs.override || inputs.workflows_ref || job.workflow_sha }}` resolves
to whatever `inputs.override` names, and no runtime guard catches it because a
guard proves non-emptiness, not immutability.
- **The `default: ''` carve-out is scoped to ref-checkout lines.** Asking it of
every line granted it to any file merely mentioning the expression in code —
most sharply the guard steps' own `env:` binding — so a file whose checkouts
are all bare bought an empty default it does not self-pin against.
- **`_ENV_ALIAS_RE` learns the fallback spelling**, so hoisting the binding to a
job-level `env:` keeps coverage instead of silently dropping to zero.
- **`check_dir` reads the fallback flag the parser determined**, not the
reported line — a block-scalar `ref:` key never holds the expression, so that
path always emitted the wrong (BE-5546) message.
Shell guards
- `export LC_ALL=C` before the shape test. 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.
- Reword the rejection as a POLICY restriction rather than a claim that the ref
is invalid to git. The class is deliberately narrower than git's rules, which
permit `(`, `)`, `,`, `%` and UTF-8; saying otherwise was untrue.
Tests: 86 -> 90. The seven groom `case` blocks were unverified — deleting all
seven left the lint and the whole suite green — so they are now pinned
individually and required byte-identical. The ledger backstop is scoped to the
consuming step and matches the condition exactly, instead of scanning a fixed
12-line window that a neighbour's `if:` could satisfy and that
`!= '' || always()` would have passed.
|
🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:
The following carry
|
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Round 3 — ledger: 16 prior finding(s) across 2 round(s) (0 never answered).
Found 10 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 3 |
| 🟢 Low | 6 |
| ⚪ Nit | 1 |
Panel: 8/8 reviewers contributed findings.
…arry alias strength (BE-8077)
Round-3 review findings on the BE-8077 lint changes.
- The fallback matcher was anchored to the `${{ … }}` interpolation, not to the
YAML value, so `refs/heads/${{ … }}`, `${{ inputs.override }}${{ … }}` and a
flow mapping whose SIBLING entry carried the fallback all scored as self-pins
— earning the weaker fallback-guard requirement and the `default: ''`
carve-out while resolving to a mutable ref. Replaced with a block/flow/
continuation trio mirroring `_REF_USE_*`, the flow form bounded at the entry
boundary exactly as `_REF_USE_FLOW_RE` bounds its own value.
- An `env:` alias of the fallback binding was registered but scored BARE,
because strength was re-read off the literal `ref:` line — which
`ref: ${{ env.WORKFLOWS_REF }}` can never satisfy. The blessed hoist-to-`env:`
refactor was therefore reported as an unguarded bare checkout, with the wrong
(BE-5546) message on it. `fallback_env_aliases` now carries the binding's
strength to the checkout.
- `_ENV_ALIAS_RE` enumerated blessed spellings, so an unrecognized binding failed
OPEN: `WORKFLOWS_REF: ${{ inputs.workflows_ref || 'main' }}` registered no
alias, `ref: ${{ env.WORKFLOWS_REF }}` read as no ref use at all, and that
checkout left the lint entirely carrying the exact mutable fallback the lint
exists to catch. It now matches any `env:` value mentioning the input and lets
the guard/mutability checks decide. The strict half moved to
`fallback_env_aliases`, which grants an exemption and so still fails closed.
- `check_dir`'s `default: ''` carve-out re-derived the self-pin per line, which
cannot see the block-scalar spelling the parser already handles: for `ref: >-`
with the expression below it, no single line carries both the key and the
expression, so a file that genuinely self-pins lost the carve-out and got
BE-5546's "delete the default" while its checkouts got BE-8077's "the fallback
IS recognized". It now asks the parser (`ref_checkouts`), which also drops the
per-line rebuild of `env_aliases` + `_ref_use_res` — quadratic on a 3,000-line
groom.yml, and 50x slower on this suite alone.
Tests: the three `assertIn`s pinning cursor-review.yml's `resolve_ref` matched
the whole file, so any of its seven guards satisfied them and a consistency
sweep could have deleted `export LC_ALL=C` from the resolver with them green;
they are now scoped to `_enclosing_step` and additionally assert the step
resolves from the `job.workflow_sha` fallback. The comment fixture named the
expression without its opening `${{` and so passed vacuously; it now carries a
complete interpolation, and a second fixture pins the direction that stripping
actually buys. Every fix is covered by a test that fails when the fix is
reverted.
Docs: the README blessed hoisting the FALLBACK binding to a job-level `env:`.
The `job` context is not available in `jobs.<job_id>.env` — verified with
actionlint 1.7.12 on `${{ job.status }}`, rejected at job level and accepted at
step level — so that is an invalid workflow, not a mislinted one. All seven of
groom.yml's bindings are step-level. docs/callers/groom.md claimed every groom
job fails closed on an empty ref; the agent CLI pin path is deliberately
warn-only, and now says so.
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Round 4 — ledger: 18 prior finding(s) across 2 round(s) (0 never answered).
Found 10 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 1 |
| 🟡 Medium | 4 |
| 🟢 Low | 5 |
Panel: 8/8 reviewers contributed findings.
… operand (BE-8077)
Round-4 review findings. The headline is a revert of my own round-3 change.
- REVERTED: carrying an `env:` binding's strength to `ref: ${{ env.NAME }}`.
`env:` is scoped per step and per job and it SHADOWS, while these scans are
file-wide, so a file-wide "names bound to the fallback" set granted the
exemption at checkouts the binding never reaches — in both directions. The
round-3 test fixture was itself the counterexample: its binding lives in the
GUARD step's `env:`, invisible to the sibling checkout step at run time, so
`${{ env.WORKFLOWS_REF }}` there expands to '' and takes the default branch
while scoring as a guarded self-pin. Symmetrically, a step-local
`WORKFLOWS_REF: ${{ inputs.workflows_ref || 'main' }}` inherited fallback
strength from any other step binding that name strictly — and cursor-review.yml
binds `WORKFLOWS_REF` both ways today, so the cross-talk was live, not
hypothetical. An alias is judged BARE now and needs a bare guard: fail-closed.
There is also no valid refactor left for it to serve. The fallback cannot be
hoisted to a shared `env:` at all — `job` is not available in
`jobs.<job_id>.env`, and a step-level `env:` does not reach a sibling step —
so groom.yml's seven duplicated bindings are duplicated of necessity. The
README said otherwise and now says this.
- The leading operand of the ref expression must reach the input. GitHub's `||`
returns the first TRUTHY operand, so `ref: ${{ 'main' || inputs.workflows_ref }}`
mentions the input — making it a ref use that clears the guard — while
resolving to a mutable branch on every runner. This also corrects the round-3
deferral of the same hole, whose stated reason ("needs a second ref-bearing
input, and none exists") was simply wrong: a literal leading operand needs no
input declaration at all.
- The flow-form self-pin matcher is quote-aware. It `search`es mid-line, so its
`[{,]` entry boundary could be met by a comma INSIDE a quoted sibling scalar,
planting a decoy `ref:` that scored the line a self-pin while its real `ref:`
was bare — buying the weaker fallback-guard requirement and the `default: ''`
carve-out.
- `_ENV_ALIAS_RE` matches the comment-STRIPPED child. Widening it to any value
mentioning the input made it the one place in the module reading a comment as
code, so `ASSETS: _dir # checked out at inputs.workflows_ref` bound `ASSETS`
and failed a compliant workflow.
Docs: README.md carried the same unscoped "every one of those jobs fails closed"
claim that docs/callers/groom.md was corrected for last round — the agent CLI
pin path is deliberately warn-only. AGENTS.md described the self-pin as an
exception to the guard requirement, which is the pre-BE-8077 behaviour this PR
reverses; it is an exemption from the `default:` half only. Both swept.
Also finishes the SHA-pin sweep in cursor-review.yml: the two remaining bare
`actions/download-artifact@v8` refs now carry the full SHA already written out
elsewhere in the same file, per AGENTS.md's pin-everything rule.
Every fix is covered by a test that fails when the fix is reverted.
|
🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:
The following carry
|
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 chore(deps): bump the github-actions-major group with 3 updates #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 chore(deps): bump the github-actions-major group with 3 updates #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.