Skip to content

fix(workflows): resolve asset checkouts from job.workflow_sha, not the empty github.job_workflow_sha (BE-8077) - #193

Merged
mattmillerai merged 5 commits into
mainfrom
matt/be-8077-job-workflow-sha-fallback
Aug 20, 2026
Merged

fix(workflows): resolve asset checkouts from job.workflow_sha, not the empty github.job_workflow_sha (BE-8077)#193
mattmillerai merged 5 commits into
mainfrom
matt/be-8077-job-workflow-sha-fallback

Conversation

@mattmillerai

@mattmillerai mattmillerai commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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's uses: 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/checkout reads an empty ref as "give me the default branch", so those jobs were checking out whatever happened to be on main at that moment, inside jobs holding ANTHROPIC_API_KEY and 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 grabbing main.

What changed

groom.yml — 7 asset checkouts (gate, audit_find, audit_verify, build_select, file, build, build_pr) now read ref: ${{ inputs.workflows_ref || job.workflow_sha }}. Each is preceded by a fail-closed Require a resolvable workflows_ref step that binds the resolved value through env:, then rejects two ways: an emptiness test on the whitespace-stripped value (actions/checkout reads ref through core.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 JS trim() behind core.getInput also 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 and job.workflow_sha, says a runner older than v2.334.0 is the only way it is empty, and exit 1. The comparison runs under export 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 in cursor-review.yml. The guard is inlined per job rather than factored into a composite action for the reason .github/workflow-pins/README.md already gives: a composite would have to be loaded from the very ref being validated.

cursor-review.yml — the Prior-review ledger job. This job must never fail, because the review matrix needs: it, so it gets no exit 1. A new resolve_ref step 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 -d removes interior whitespace too, so a b was silently rewritten into the different-but-real ref ab instead of being rejected.) Load cursor-review assets is gated on if: steps.resolve_ref.outputs.ref != '' and reads ref: ${{ steps.resolve_ref.outputs.ref }}. With the checkout skipped, Build prior-review ledger fails under its existing continue-on-error: true and the existing Ensure ledger artifact exists fallback (id: fallback, if: always()) publishes status=unknown plus its own warning — no new fallback logic was needed. The new step carries timeout-minutes: 2 so 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 on job.workflow_sha, and the old github.job_workflow_sha spelling is no longer exempt in either place it was honoured (the checkout carve-out and the default: '' carve-out), so the mistake cannot come back with the lint green.

Review hardened three things about that exemption:

  • Anchored to the closing }}, 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-wide any(...) was looser still: a comment merely naming the expression bought the default: '' carve-out.
  • _GUARD_BINDING_RE widened to accept the WORKFLOWS_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 by is_guard_step, and falsely reported a checkout that dropped the fallback while keeping its guard.
  • A guard is now required even when the fallback is present. The fallback answers mutability (and still earns the 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:

  • Guard strength. Recognizing the fallback binding is not the same as treating it as blanket job-wide coverage — see the judgment call below. A guard on the fallback covers fallback checkouts only.
  • Anchored at the head as well as the tail. ${{ 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.
  • The 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' own env: binding.

_ENV_ALIAS_RE also learned the fallback spelling, so hoisting that binding to a job-level env: keeps coverage instead of silently dropping to zero, and check_dir now reads the fallback flag the parser determined rather than re-reading the reported line (a block-scalar ref: 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 case blocks previously left everything green), and a backstop for the ledger checkout, which left the lint's coverage when it moved to a step output.

DocsAGENTS.md, README.md, CONTRIBUTING.md, docs/callers/README.md, docs/callers/groom.md, .github/groom/README.md, .github/workflow-pins/README.md, plus the workflows_ref input description and header caller example in groom.yml. The "leave it unset" advice is now true rather than aspirational, and the "pin it until groom's checkouts move to job.workflow_sha" instructions are flipped. Wherever the accessor is documented there is now a note that actionlint ≤ 1.7.12 false-positives on job.workflow_sha (its job-context schema predates runner v2.334.0); nothing in this repo's CI runs actionlint, so nothing gates on it.

Sweep

job_workflow_sha appeared on 54 lines across 11 files on main. 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 matching README.md catalog row) asserting that "the runner does not expose [the commit uses: resolved to] to the workflow" and that the answering value "exists only as an OIDC token claim". That is no longer true — job.workflow_sha answers it and needs no id-token: write. The prose is corrected, but the pr-risk.yml check itself is deliberately NOT wired up: asserting workflows_ref == job.workflow_sha there 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_ref set (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-cli groom run 32240568032, groom / Gate job 96029967905 (public repo, read-only log fetch):

  • WORKFLOW_SHA: eb4b26c9bc13feea5ae76c4334370158713fb23ajob.workflow_sha resolves to a real 40-hex SHA on ubuntu-latest.
  • That SHA equals the workflows_ref the caller passed, which equals the commit its uses: line pins — i.e. job.workflow_sha really is "the commit the caller's uses: resolved to", which is the whole basis of the fallback.
  • The existing job.workflow_sha is empty warning did not fire (no ##[warning] annotations in that job at all).

Judgment calls

  • The pin lint recognizes the new guard steps, but only for checkouts of matching strength. This took two review rounds and one wrong turn, recorded because the wrong turn is the instructive part. The original reasoning here — that a guard proving inputs.workflows_ref || job.workflow_sha non-empty says nothing about a later bare ref: ${{ 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_RE records 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 explicit workflows_ref: (now annotated as optional) even though the row and prose say leaving it unset is safe. bump-callers.sh moves that line and the uses: pin in one pass for roster-enrolled callers, and AGENTS.md records 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_ref form, per the ticket.
  • No live groom or cursor-review run was triggered. Both spend model credits and mutate GitHub state (issues, PRs, review comments), so they are out of bounds for non-mutating acceptance. The runner-side premise was verified from the existing run log above; the two new failure branches are unreachable on ubuntu-latest by construction.

Not fixed here (why this says Refs, not Closes)

  • Bare action pins — two of six fixed in review, four left. actions/checkout@v7 and actions/upload-artifact@v7 in 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: two actions/download-artifact@v8 elsewhere in cursor-review.yml (lines 1310, 1556) and actions/checkout@v7 / actions/setup-python@v7 in test-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 — against AGENTS.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.md is 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-enrol agents-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.yml and bump-cursor-review-callers.yml both carry push: branches: [main] with paths: covering their own reusable workflow file, so both fleets re-bump themselves on merge. Confirmed by reading both trigger blocks.

Refs BE-8077

Provenance

  • Authored by: agent-work loop
  • Verified: check_workflow_pins.py --workflows-dir .github/workflows: OK, 10 workflows declare workflows_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_load parses both edited workflows; py_compile clean 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 to ref: ${{ env.NAME }} was reproduced failing in both directions before the revert: a fallback binding in a guard step's env: scored the sibling checkout (14, True, True) — a guarded self-pin — although step-level env: never reaches a sibling step, so it expands to '' and takes the default branch; and a step-local WORKFLOWS_REF: ${{ inputs.workflows_ref || 'main' }} scored (16, True, True) by inheriting strength from another step's strict binding. cursor-review.yml binds WORKFLOWS_REF both 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: job is not available in jobs.<job_id>.env (actionlint 1.7.12 on ${{ job.status }}, chosen to isolate context availability from the job.workflow_sha schema staleness — rejected at job level, accepted at step level), and a step-level env: 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.yml guards (deleting export LC_ALL=C from resolve_ref alone, 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 missing jsonschema module locally, pre-existing and unrelated (path-filtered to a directory this PR does not touch; its CI job is green). The glibc collation behaviour behind LC_ALL=C cannot be reproduced on macOS libc, which rejects the accented case either way.
  • Deviations: The charset accepted by the shape checks is [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 as feat(groom)/x fails 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). Dropping workflows_ref from ci-groom.yml to give the fallback CI coverage was considered and declined: that caller documents its uses: and workflows_ref pins as byte-identical and bump-callers.sh rewrites 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:456 is covered only by a per-site grep today); three spellings that leave a checkout invisible (flow-form env:, 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 a run: 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@v8 refs in cursor-review.yml were pinned to the SHA already written out elsewhere in that file, finishing the sweep this PR started there. The two in test-refresh-reviewers.yml are 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.md is over its own 200-line ceiling. Pre-existing (273 on main, and this repo does not self-enrol agents-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.

…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.
@mattmillerai mattmillerai added the agent-coded Authored by the agent-work loop label Aug 20, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review August 20, 2026 05:33
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

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.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54e81624-4edf-4c3d-aa38-18caa1038102

📥 Commits

Reviewing files that changed from the base of the PR and between b09780e and 8e78bbb.

📒 Files selected for processing (12)
  • .github/groom/README.md
  • .github/workflow-pins/README.md
  • .github/workflow-pins/check_workflow_pins.py
  • .github/workflow-pins/tests/test_check_workflow_pins.py
  • .github/workflows/cursor-review.yml
  • .github/workflows/groom.yml
  • .github/workflows/pr-risk.yml
  • AGENTS.md
  • CONTRIBUTING.md
  • README.md
  • docs/callers/README.md
  • docs/callers/groom.md

Comment @coderabbitai help to get the list of available commands.

@mattmillerai mattmillerai added the cursor-review Multi-model cursor review label Aug 20, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/groom.yml
Comment thread .github/workflows/groom.yml
Comment thread .github/workflows/groom.yml
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml Outdated
…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.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Aug 20, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflows/groom.yml
Comment thread .github/workflows/groom.yml
Comment thread .github/workflow-pins/tests/test_check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py
…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.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Aug 20, 2026
@mattmillerai

Copy link
Copy Markdown
Contributor Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

  • BE-8127 — workflow-pins: follow a checkout ref: through a step output so resolve-then-consume jobs stay linted — filed as agent-spike (premise unverified)

The following carry agent-spike instead of agent-ok because their reachability claim was not backed by evidence (BE-5378) — the claim is investigated before any code is written, and "the premise does not hold" is a valid, successful outcome:

  • workflow-pins: follow a checkout ref: through a step output so resolve-then-consume jobs stay linted — no reachability block in the proposal

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

Comment thread .github/workflow-pins/check_workflow_pins.py
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/tests/test_check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/tests/test_check_workflow_pins.py Outdated
Comment thread docs/callers/groom.md Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
…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.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Aug 20, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflow-pins/check_workflow_pins.py
Comment thread .github/workflow-pins/check_workflow_pins.py
Comment thread .github/workflow-pins/check_workflow_pins.py Outdated
Comment thread .github/workflow-pins/check_workflow_pins.py
Comment thread .github/workflow-pins/check_workflow_pins.py
Comment thread AGENTS.md Outdated
Comment thread README.md Outdated
Comment thread .github/workflows/cursor-review.yml
… 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.
@mattmillerai

Copy link
Copy Markdown
Contributor Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

  • BE-8140 — workflow-pins: follow a ref: through a step output so resolve-then-consume jobs are linted — filed as agent-spike (premise unverified)
  • BE-8141 — workflow-pins: close three spellings that make a ref checkout invisible to the lint — filed as agent-spike (premise unverified)
  • BE-8142 — workflow-pins: skip block-scalar bodies so a run: script cannot forge a guard or an alias — filed as agent-spike (premise unverified)

The following carry agent-spike instead of agent-ok because their reachability claim was not backed by evidence (BE-5378) — the claim is investigated before any code is written, and "the premise does not hold" is a valid, successful outcome:

  • workflow-pins: follow a ref: through a step output so resolve-then-consume jobs are linted — no reachability block in the proposal
  • workflow-pins: close three spellings that make a ref checkout invisible to the lint — no reachability block in the proposal
  • workflow-pins: skip block-scalar bodies so a run: script cannot forge a guard or an alias — no reachability block in the proposal

@mattmillerai
mattmillerai merged commit f8eec04 into main Aug 20, 2026
9 checks passed
@mattmillerai
mattmillerai deleted the matt/be-8077-job-workflow-sha-fallback branch August 20, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants