Skip to content

fix(bump-callers): target the github-workflows pin token and assert both pins moved (BE-4662) - #79

Open
mattmillerai wants to merge 8 commits into
mainfrom
matt/be-4662-bump-callers-precise-pin
Open

fix(bump-callers): target the github-workflows pin token and assert both pins moved (BE-4662)#79
mattmillerai wants to merge 8 commits into
mainfrom
matt/be-4662-bump-callers-precise-pin

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

STACKED — merging lands on matt/groom-max-prs-reusable (owned by @mattmillerai, PR #77), NOT main. This PR's base is #77's branch because #77 rewrites the same sed block; basing on main would conflict with it and silently drop its # main @ pin-comment rules. GitHub retargets this PR to main once #77 merges. Review the net diff, not the merge button.

ELI-5

When one of this repo's reusable workflows changes, a bot opens a "bump the pin" PR in every repo that uses it. A repo pins us in two places: the uses: line and a workflows_ref: input that loads our prompts/briefs/scripts at run time. Both have to point at the same commit, or the run uses one version's workflow with another version's briefs.

The bot used to find those pins by looking for "any 40-character hex string on a line that mentions github-workflows." That's the wrong thing to look for, and it never checked its own work. So if a repo pinned workflows_ref: v1 (a tag, not 40 hex), the bot bumped the uses: line, left the tag alone, and happily opened a green-looking PR that splits the caller. And if some unrelated 40-hex value happened to sit on a line that mentioned github-workflows, the bot overwrote it.

Now the bot looks for the pin tokenComfy-Org/github-workflows…@ and the workflows_ref: key — and takes whatever comes right after it, whatever shape it is. Then, before it stages anything, it re-reads the file and checks that every pin really did move. If one didn't, it says so out loud and fails that repo instead of opening a half-bumped PR.

What changed

  • The rewrite targets the pin token, not 40-hex-ness. Two patterns matched by position: Comfy-Org/github-workflows[^@\s]*@<ref> and workflows_ref: <ref> (optionally quoted). Whatever sits right after the token is the ref, so a full sha, a short sha, or a tag all move — and an unrelated SHA sharing the line is unreachable.
  • The workflows_ref rule is ^-anchored to a block-mapping key, so a prose comment mentioning the input (# workflows_ref: keep in sync with uses:) is not rewritten into a sentence with a SHA spliced through it.
  • A post-rewrite assertion gates staging. The file is re-read with a deliberately broader reader (any non-whitespace value sitting where a ref belongs, comments excluded); if anything but the new SHA is still there, it emits a ::warning:: naming the file and the stale value and fails that repo. Same posture line 203 already takes for a transient fetch error — a partial bump is worse than no bump (BE-3896). Nothing is written to the caller repo before this point (all API writes are in Pass 2), so the failure is genuinely all-or-nothing.
  • A dedicated rule for the full-sha # main @ <40hex> pin comment. That comment used to be corrected as collateral of the old line-scoped 40-hex substitution. With the substitution now precise, it needs its own rule or it would be left naming the old commit — a regression against feat(groom): own the max_prs dispatch override in the reusable + add the groom caller fleet (BE-4346) #77's intent. Verified by deleting the rule and watching feat(groom): own the max_prs dispatch override in the reusable + add the groom caller fleet (BE-4346) #77's groom case go red (2 failures).
  • Doc sync: .github/bump-callers/README.md gains a "How the pin rewrite is scoped" section; bump-groom-callers.yml's header no longer describes the old line-keying.

Tests

.github/bump-callers/tests/test_bump_callers.sh99 → 130 cases, all green, plus shellcheck -x clean on the script and the suite. Five new cases:

Case Asserts
reftag a workflows_ref: v1 moves in lock-step with uses:
refshort a quoted short sha '2222222' moves, and the closing quote survives
coloc an unrelated 40-hex on a github-workflows line and one on a workflows_ref line are both untouched; exactly one pin is rewritten
assertfire a workflows_ref: ${{ inputs.workflows_ref }} fails the repo — no blob, no commit, no PR, bump failed for 1 repo, and the warning names both the file and the stale value
prosecomment a # workflows_ref: … prose note neither trips the assertion nor gets mangled, while the real pin below it still bumps

Ran: shellcheck -x .github/bump-callers/bump-callers.sh .github/bump-callers/tests/test_bump_callers.sh and bash .github/bump-callers/tests/test_bump_callers.sh.

Judgment calls

  1. Stacked on feat(groom): own the max_prs dispatch override in the reusable + add the groom caller fleet (BE-4346) #77 rather than main. feat(groom): own the max_prs dispatch override in the reusable + add the groom caller fleet (BE-4346) #77 is open and rewrites this exact sed block (it added the # main @ comment rules). Building on main would have conflicted and effectively reverted them. Banner at the top; base is matt/groom-max-prs-reusable.
  2. A uses: pin at a tag is now rewritten to the SHA too. The ticket specified shape-agnostic matching for workflows_ref only, but applying it to uses: as well is what makes the pair coherent — and it is required, because the assertion demands every uses: pin equal the new SHA, so a @v1 pin would otherwise fail the repo forever. It also matches the repo standard ("pin everything by full commit SHA"; bare @v1 fails consumers' pinact/zizmor).
  3. The assertion is intentionally broader than the rewrite, and that asymmetry is the design. Anything the rewrite cannot move surfaces as a loud per-repo failure rather than a silent half-bump. The known consequence: a caller whose workflows_ref is a ${{ … }} expression, or in flow style (with: {workflows_ref: v1}), will fail its repo on every run until a human fixes the config. That is deliberate — there is no literal ref for the bumper to move in those shapes, and rewriting them would break the caller. Comments are stripped before the assertion scans, so prose can never cause that failure.
  4. Falsification of the new deny path. The only outcome this diff denies is "bump a caller whose pin the rewrite cannot move." I checked every workflows_ref usage in the repo: the ${{ inputs.workflows_ref }} forms all live inside the reusable workflows themselves (never in a caller list), and every documented caller pattern (workflows_ref: <sha>, workflows_ref: main, workflows_ref: <same-sha>) is a literal the new rewrite moves. So no caller shape that works today is newly denied — the shapes that fail are the ones that previously produced a silently half-bumped PR.

Risk

The change is confined to one script that backs all five bump fleets, so blast radius is "every fleet" — but every write to a caller repo happens in Pass 2, after the assertion, and the suite covers each fleet's fixture shape. The riskiest line is the workflows_ref substitution, which replaces whatever token follows the key; that is safe because workflows_ref is this repo's own input and its only meaning is "the github-workflows ref to load assets from."

Closes BE-4662.

mattmillerai and others added 4 commits July 26, 2026 00:23
…the groom caller fleet (BE-4346)

Comfy-Org/cloud#5572 added a `max_prs` workflow_dispatch input to ONE caller, and
it cost ~40 lines of expression gymnastics to do it:

  max_prs: ${{ fromJSON(contains(fromJSON('["1","2","3","5"]'), github.event.inputs.max_prs) && github.event.inputs.max_prs || '1') }}

All of that exists because the reusable declared `max_prs` as `type: number`. A
workflow_dispatch input is always a string, GitHub rejects a string expression
assigned to a `type: number` reusable input at startup, so the caller must cast
with `fromJSON()` — and `fromJSON()` on a non-numeric string throws during
expression evaluation, killing the run before any job starts. Hence the mirrored
`contains()` allowlist guarding the cast. Every caller that wanted the knob would
have carried its own copy of that, which is exactly the drift this repo exists to
prevent: the logic belongs here, once.

So `max_prs` becomes `type: string` and the reusable does the parse. Verified
with actionlint that the asymmetry is real: a string expression into a
`type: number` input is rejected, while a plain `max_prs: 1` into a `type: string`
input is accepted — so existing callers need no change. The caller collapses to:

  max_prs: ${{ github.event.inputs.max_prs || '1' }}

Parsing lives in build_select, and none of its cases can abort a run: empty (what
a SCHEDULE yields, carrying no inputs) takes the default; a number is clamped to
>= 0; anything else opens ZERO PRs with a loud warning. That last case is the one
judgement call — falling back to the DEFAULT would be actively wrong, since a
caller piloting at `max_prs: 1` would silently get 5, and exiting non-zero would
throw away a finder+verifier run already paid for while the findings still file
as issues on the 0-PR path. No upper bound is enforced: a caller's `options:`
list is a UI convention and a typo guard, not a security boundary — anyone who
can API-dispatch already has write access and could edit `options:` anyway.

Also adds the groom caller fleet, which did not exist. It is the fleet that most
needs one: a groom caller pins the reusable TWICE (`uses:` and the
`workflows_ref:` that loads the briefs + ledger), and those must move in
lock-step or a run executes one version's workflow against another version's
briefs. bump-callers.sh already rewrote both; it now also re-points the
`# main @ <short>` pin comment those callers carry, because a comment still
naming the old commit after the pin moved is worse than no comment. Anchored to
the github-workflows line and bounded to {7,12} hex so an unrelated note and a
deliberate full-SHA comment are both left alone.

- .github/workflows/bump-groom-callers.yml — thin entrypoint, GROOM_CALLERS,
  ALLOW_EMPTY=true (the fleet grows as callers land), no WIRE_BOT_SCRIPT.
- test_bump_callers.sh — a groom case covering both pins, the comment rewrite,
  an untouched actions/checkout pin, and an intact max_prs forward expression.

Verified: actionlint clean; shellcheck clean; 97/97 bump-callers tests (9 new);
59/59 groom ledger tests; agents-md-integrity passes. Parse exercised over
'', '   ', '5.0', '-2', 'abc', '1e3', 'inf', 'nan', '01', ' 2 '.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pin-comment rewrite (BE-4346)

Addresses the cursor-review panel on #77.

- bump-groom-callers.yml checked out with the mutable tag `actions/checkout@v6`
  while holding an org-scoped bot token. AGENTS.md mandates a full-SHA pin for
  every third-party action; pinned to the same SHA groom.yml already carries.

- The minted token requested no permissions, so it carried everything Cloud Code
  Bot holds on every repo it is installed on. Narrowed to the trio this bumper
  actually uses — contents (Git Data commit), pull-requests (open/update the bump
  PR), issues (`gh pr create --label`) — matching groom.yml's own PR job. `owner:`
  with no `repositories:` stays: the caller list is a runtime variable, and naming
  those repos here would leak private names into a public file.

- The `# main @ <short>` comment rewrite was unanchored at its right edge, so
  `[0-9a-f]{7,12}` would match the first 12 characters of a longer hex run. On a
  deliberate `# main @ <40hex>` comment — which rule 1 has just rewritten to
  NEW_SHA — it swapped those 12 for the 7-char SHORT and stranded the other 28,
  mangling the exact full-form comment the {7,12} bound was there to protect.
  Split into two rules requiring a non-hex character or EOL after the run, so the
  match is a whole token; a 13+ hex run now matches neither and is left intact.
  Portable ERE rather than a `\b`/`[[:>:]]` assertion, which spells differently in
  GNU and BSD sed.

99/99 bump-callers functional tests (2 new; the full-form case fails against the
old rewrite), shellcheck -x clean, actionlint clean, 59/59 groom ledger tests,
agents-md-integrity passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…o (BE-4346)

The 40-hex pin rewrite fires on `/github-workflows|workflows_ref/`, but the two
`# main @ <short>` comment rewrites were anchored to `/github-workflows/` alone.
A groom caller that annotates its `workflows_ref:` pin — the second of the two
pins groom callers carry — got that line's SHA bumped while the comment kept
naming the old commit: the stale "confident lie" pin comment these rules exist
to kill, reintroduced on the pin the groom fleet was built to keep in lock-step.

Widen the comment anchors to match rule 1's exactly, and cover both directions
in the groom fixture: the `workflows_ref:` line now carries its own `# main @`
note (must move), plus an unanchored note on a line naming neither pin context
(must NOT move, proving the anchor still bounds the rewrite). The first check
fails against the pre-fix script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oth pins moved (BE-4662)

The caller pin rewrite keyed on "any 40-hex on a line that mentions
github-workflows or workflows_ref" and never checked its own outcome, which
failed silently in both directions:

* Under-rewrite. A caller pins this repo TWICE — the `uses:` sha and the
  `workflows_ref:` input that loads the briefs/prompts/scripts at run time. A
  `workflows_ref` pinned to a tag (`v1`) or a short sha is not 40 hex, so it was
  left behind while `uses:` moved. The content-equality check still saw a
  difference, so the file was staged and a green-looking bump PR opened on a
  caller now running one version's workflow against another version's assets —
  the exact split this fleet exists to prevent.
* Over-rewrite. An unrelated 40-hex value sharing such a line was clobbered.

Anchor the substitution to the pin TOKEN instead — `Comfy-Org/github-workflows…@`
and the `workflows_ref:` key — and take whatever ref follows by position, so any
literal shape (full sha, short sha, tag) moves and a co-located SHA is
unreachable. The `workflows_ref` rule is `^`-anchored to a block-mapping key so
prose that merely mentions the input is left alone.

Precision cuts both ways, so add the guard it needs: before a rewritten file can
be staged, re-read it with a deliberately broader reader and assert every
github-workflows pin now equals the new SHA. If one does not (today: a
`workflows_ref` fed by a `${{ … }}` expression, which is never rewritten), warn
with the file + the stale value and fail that repo — the same posture a transient
fetch error already takes, because a partial bump is worse than no bump (BE-3896).

The full-sha `# main @ <40hex>` comment form gets its own rule now that rule 1 no
longer sprays every 40-hex on the line; it used to be corrected as collateral.

Tests: +31 cases (130 total) — tag-pinned and short-sha `workflows_ref`, an
unrelated 40-hex sharing a github-workflows line, the assertion firing (no
commit, no PR, non-zero for that repo), and a prose comment not tripping it.
shellcheck -x clean.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review agent-coded Authored by the agent-work loop labels Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 58 minutes

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?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9b2e4816-3af9-443f-a814-936605dec4ab

📥 Commits

Reviewing files that changed from the base of the PR and between 50ad29c and a2bf511.

📒 Files selected for processing (4)
  • .github/bump-callers/README.md
  • .github/bump-callers/bump-callers.sh
  • .github/bump-callers/tests/test_bump_callers.sh
  • .github/workflows/bump-groom-callers.yml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-4662-bump-callers-precise-pin
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-4662-bump-callers-precise-pin

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

@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 9 finding(s).

Severity Count
🟠 High 1
🟡 Medium 4
🟢 Low 3
⚪ Nit 1

Panel: 8/8 reviewers contributed findings.

Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh
Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
mattmillerai and others added 2 commits July 26, 2026 04:49
…case, key boundary (BE-4662)

Review follow-ups on the pin-token rewrite + assertion. All five are edges of
the same idea: the token has to end where the ref begins, and the assertion has
to read exactly what the rewrite writes.

* the `uses:` pattern now requires a DELIMITER after the repo name (`/` or `@`),
  so a sibling repo whose name merely starts the same
  (`Comfy-Org/github-workflows-tools/action@v1`) is no longer swallowed and
  repinned to this repo's SHA — and, because the assertion reuses the pattern,
  no longer read back as NEW_SHA and staged silently;
* the owner/repo is matched case-INSENSITIVELY, because GitHub resolves `uses:`
  that way. A `comfy-org/…` caller previously had its (repo-agnostic)
  `workflows_ref` half bumped while `uses:` stayed stale, and the assertion —
  reading `uses:` with the same pattern — missed the stale half too: precisely
  the split half-bump this guard exists to prevent;
* the assertion's `workflows_ref` reader gained a left boundary, so a longer key
  like `upstream_workflows_ref: v1` (which the rewrite correctly leaves alone) is
  no longer read as an un-bumped pin and no longer hard-fails a clean caller's
  bump on every run;
* comments are dropped by YAML's own rule (a `#` preceded by whitespace) instead
  of at the first `#`, so a `#` INSIDE a ref cannot fake its way past: REF_RE
  half-moves `'feature#1'` to `'<NEW_SHA>#1'`, which now compares unequal and
  fails the repo rather than being read back as a clean NEW_SHA;
* an empty pin (`workflows_ref: ""`) is named `(empty)` rather than filtered out
  as a blank — the rewrite cannot move it either, so dropping it let a silent
  half-bump through.

The failure warning is also sanitized before it reaches this public repo's run
logs (carriage return stripped, `::` neutralized) so a caller-supplied value
cannot inject a workflow command, and the value/prose spacing is now explicit
rather than relying on the here-string's trailing newline.

Five regression cases added; each fails against the pre-fix script (16 checks).
Two conflicts, one textual and one semantic. Both sides' intents preserved.

TEXTUAL — .github/bump-callers/bump-callers.sh, the pin-rewrite block. Main's
BE-4523 (#75) added the multi-reusable attribution guard (GW_USES / SHA_ADDR
tightening / the `main (<short>)` marker refresh); this branch added the groom
callers' `# main @ <short>` rewrite. Resolved by keeping main's structure whole
and placing the `# main @` rules in the SAME sed as the 40-hex rule, sharing
`${SHA_ADDR}` verbatim.

That placement — rather than folding them under the `GW_ONLY_OURS` guard — is
what preserves both intents at once. `SHA_ADDR` is exactly the set of lines
whose pin the 40-hex rule rewrites, and the `# main @` annotation rides ON that
line, so the address is honest attribution for it: in the sibling regime the
tightened address skips a sibling's `uses:` line, so neither its pin nor its
annotation is stamped with our SHA. The `main (<short>)` form cannot use the
same trick — it also appears in prose ABOVE the `uses:` line, naming no pin
context — which is why it keeps the caller-level guard. Gating `# main @` on
that guard instead would have manufactured the exact stale comment BE-4346
removed: a `workflows_ref:` pin is bumped even in a multi-reusable file (that
context stays broad by main's own reasoning), so its annotation must move with
it.

Three assertions added for the merged interaction, which neither branch's suite
covered. Mutation-checked both ways: dropping the `# main @` rules fails the
groom fixture; widening them back to the pre-BE-4523 broad address fails the new
sibling assertion.

SEMANTIC — .github/workflows/bump-groom-callers.yml. No textual conflict (a new
file against modified siblings), but main's BE-4058 (#62) hardened all four
existing bump entrypoints while this branch adds a fifth written from the
pre-hardening template, so the merged result shipped one unguarded fleet. Brought
to parity: the main-only ref guard, the stale re-run tip check, the decommission
guard (checking `.github/groom/` as well as `groom.yml`, since a groom caller
pins both), and the SHA-pinned checkout the sweep moved to v6.0.3.

The ref guard here is main's accidental-stale-dispatch guard, not a security
control — the workflow_dispatch token-escalation path stays open and stays
tracked as BE-4661, which needs the server-side environment gate.

123 bump-callers assertions pass, shellcheck clean, groom/agents-md/cursor-review
suites green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from matt/groom-max-prs-reusable to main July 26, 2026 22:18
…rs-precise-pin

Reconciles BE-4662's token-anchored pin rewrite with BE-4523's
sibling-reusable address tightening: rule 1 (the `uses:` pin) is now
address-restricted to OUR workflow file when a caller pins two
github-workflows reusables, the `# main @` comment rules ride the same
address, and the post-rewrite assertion scans only the same
address-eligible lines so a deliberately untouched sibling pin can't
false-fail the repo. Both test suites (BE-4662 precise-pin + BE-4523
sibling-marker) now live together and pass, 175/175.
PR #77 (the groom-max-prs-reusable branch) squash-merged to main while
this branch was mid-review, retargeting this PR from that branch to
main and reproducing the squash double-diff: main's squashed content in
bump-callers.sh/README.md is identical to what this branch already
reconciled against pre-squash, so the already-reconciled side wins.
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.

1 participant