Skip to content

fix(bump-callers): fetch the roster at run time and mask it before the public log ever sees it (BE-6482) - #138

Open
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-6482-fetch-roster-at-runtime
Open

fix(bump-callers): fetch the roster at run time and mask it before the public log ever sees it (BE-6482)#138
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-6482-fetch-roster-at-runtime

Conversation

@mattmillerai

@mattmillerai mattmillerai commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

ELI-5

The bumper's list of which repos to bump is private. It used to be handed to the script through the workflow step's env: block — and GitHub prints a step's env block into the run log before the step runs. This repo is public, so the private repo names showed up in a world-readable log a moment before the script's masking could hide them. Now the script goes and fetches the list itself and masks it before anything is printed, so there is nothing to print early.

⚠️ DEPLOY PRECONDITION — needs an org admin before this works

The Cloud Code Bot GitHub App must be granted the repository permission Variables: read, and that permission update must be approved on the Comfy-Org installation. An agent cannot verify or grant this. Until it is granted, every bump-*-callers run fails with an explicit error naming exactly that grant:

::error::App token cannot read Actions variables — grant the Cloud Code Bot App repository permission 'Variables: read' and approve it on the Comfy-Org installation (BE-6477).

That is a loud, named failure rather than a silent no-op, on purpose.

What changed

.github/bump-callers/bump-callers.sh — the roster is now read at run time from the Actions variable VAR_NAME names, keyed on GITHUB_REPOSITORY, instead of arriving via the step env.

  • The fetch runs only when CALLERS_JSON is genuinely unset ([ -z "${CALLERS_JSON+x}" ], not -z "$CALLERS_JSON"). A set-but-empty value still means "empty roster" — so the suite's empty-variable cases keep their meaning, and an operator's deliberate CALLERS_JSON='' can never fall through into bumping the live fleet.
  • gh api …/actions/variables/<VAR> rather than gh variable get, because only the REST path surfaces the HTTP status. 404 → CALLERS_JSON="", which hands straight to the existing ALLOW_EMPTY block, so its semantics are bit-for-bit preserved (seeded-empty fleet → clean no-op; must-have-callers fleet → the loud clobber-detecting error). 403 → the Variables-grant error above. Anything else → an ::error:: carrying the sanitized reason only.
  • No error path ever echoes the fetched value. The ::add-mask:: loop remains the first place the roster's repo names are used, and carries a comment stating that invariant.

All 8 bump-*-callers.yml entrypointsCALLERS_JSON: deleted from the step env: block; VAR_NAME kept and its comment updated to say the script reads the roster from it.

Docs — the "Known gap" blockquote in .github/bump-callers/README.md, the KNOWN GAP header paragraphs in the auto-label / pr-risk / detect-unreviewed-merge entrypoints, and the AGENTS.md line that referenced the gap. The gh variable set update-flow instructions are unchanged.

Tests — the gh stub gained an api repos/*/actions/variables/* handler (configurable roster fixture, 404 mode, 403 mode, 500 mode, and it logs every invocation). Seven new cases; every existing case is untouched. bash .github/bump-callers/tests/test_bump_callers.sh207 passed, 0 failed; shellcheck -x clean on both files.

Least privilege — kept, via a second token

The roster read needs a token that can read Actions variables, and a downscoped App token cannot: GitHub's app-permissions schema (the installation-token request body) has no variables key, so there is no permission-variables input to ask for. I verified that against the action's own action.yml at both refs:

ref permission-* inputs occurrences of variab
d72941d (the pinned v1.12.0) 48 0
default branch (v3.x) 54 0

The first cut resolved that by dropping permission-contents/pull-requests/issues from four entrypoints, widening the write token — the one that commits, branches, opens PRs and stamps labels across every caller repo — to full installation permissions. Every reviewer on the panel flagged it, and they were right: it is avoidable.

Now there are two tokens. Each entrypoint mints a second create-github-app-token with repositories: github-workflows, and the script uses it (VAR_TOKEN) for the roster read alone. GH_TOKEN stays on its narrow trio, restored on all four entrypoints that had it. The read token is still un-downscoped — that part is forced — but repositories: confines it to this one public repo rather than every repo the App is installed on. VAR_TOKEN is optional: unset, the read falls back to GH_TOKEN, so a manual invocation still works. A test asserts the read runs under VAR_TOKEN and not the write token, so collapsing back to one token fails CI.

The action pin itself is untouched (v2+ breaks the cross-repo owner: scoping the bumper depends on — see the in-file comment and the dependabot ignore); repositories: is confirmed present at the pinned ref.

Read failure modes — hardened after review

  • A 404 is never silent. It still folds into the empty roster (so ALLOW_EMPTY keeps its exact semantics), but always logs a ::warning:: naming the variable and both scopes tried. A 404 is also how GitHub answers "this token cannot see the repository at all" — App uninstalled, repo-selection drift, a rename, a future VAR_NAME typo — which would otherwise stop a whole ALLOW_EMPTY=true fleet from bumping in total silence.
  • Org-level variables resolve again. repos/…/actions/variables/… is narrower than the ${{ vars.* }} binding it replaced, which also resolved org-level variables. The read now falls back to orgs/{owner}/… after a repo-scope 404, repo-level winning on a name clash. Best-effort: a failed org probe stays an empty roster, never a hard error.
  • A 403 is not automatically "missing grant". Rate limiting is matched first and reported as a throttle; the grant error names the other 403 causes (IP allow-list, SAML, suspended installation) and carries the API's own sanitized reason.
  • mktemp is checked and the temp file is trapped on EXIT.
  • The :: squash loops to a fixpoint — a single s|::|:|g pass is not one, since ::::x:::: collapses back into ::x::. An adversarial-error-body case asserts no workflow command survives into the log.

Judgment calls

  1. Scope: 8 entrypoints, not 6. The ticket was written against the fleet as it stood then and named 6 entrypoints for the CALLERS_JSON deletion (anticipating pr-risk "if ci(bump-callers): add the pr-risk caller-bump fleet (BE-6308) #119 has merged" — it has). Since then bump-pr-risk-callers.yml (ci(bump-callers): add the pr-risk caller-bump fleet (BE-6308) #119) and bump-detect-unreviewed-merge-callers.yml have landed. Applying the ticket's list literally would have left those two 403-ing on every run. I followed the ticket's stated intent ("EVERY bump-*-callers.yml present at implementation time") over its enumerated line numbers. All 8 get the VAR_TOKEN step; the 4 that were downscoped stay downscoped.

  2. detect-unreviewed-merge's unseeded roster. That fleet is deliberately unseeded specifically because of this gap — its header and the README say to seed it "as the immediate follow-on to the masking fix". I updated both to say the blocker is now gone and to seed only after a dispatch is observed reading the variable without the 403, rather than seeding it myself (it needs private caller names I don't have, and it is an operator action either way). The run stays red until someone seeds it.

  3. 404 folds into "empty". As specified. Worth knowing the consequence: if GitHub ever answered a permissions failure with 404 instead of 403, a seeded-empty fleet would no-op silently. The must-have-callers fleets (cursor-review, auto-label, detect-unreviewed-merge) would still fail loudly, so the condition cannot go unnoticed fleet-wide.

Verify after merge (needs the App grant first)

  1. workflow_dispatch bump-pr-size-callers (roster seeded empty, ALLOW_EMPTY=true): green, with no CALLERS_JSON in the step env dump. A 403 here means the Variables grant is still missing — grant it, then re-dispatch. A ::warning::No PR_SIZE_CALLERS Actions variable found … instead means the variable is absent rather than seeded empty.
  2. workflow_dispatch bump-cursor-review-callers: env dump carries no roster, repo names appear only as ***, and the caller bump PRs are still opened/updated.

Note for the operator — the already-leaked rosters

This fix stops future leaks; it cannot retract past ones. The rosters already sit in pre-fix public run logs (e.g. run 30604087646, bump-cursor-review, 2026-07-31 — all 19 CURSOR_REVIEW_CALLERS entries; run 30604087518, auto-label, identically). GitHub's 90-day retention ages the July runs out around 2026-10-29; gh run delete on the old bump runs removes them sooner. That is your call — out of scope here.

Out of scope

Converting the roster variables to secrets (rejected in the ticket: the repo deliberately keeps them API-readable for ops/audit; it is the fallback only if the Variables grant is refused). Deleting historical runs.

…e log (BE-6482)

Every bump-*-callers entrypoint passed its caller roster to bump-callers.sh
through the step's `env:` block. GitHub Actions prints a step's env block into
the run log BEFORE the step runs, so in this PUBLIC repo the raw roster landed
in a world-readable log ahead of the script's `::add-mask::` loop — masking
could not win a race it started after.

bump-callers.sh now reads the roster itself, from the Actions variable VAR_NAME
names, keyed on GITHUB_REPOSITORY. The fetch uses `gh api` rather than
`gh variable get` because only the REST path distinguishes 404 (no such
variable → an empty roster, so the existing ALLOW_EMPTY semantics are preserved
exactly) from 403 (the token cannot read Actions variables → an explicit,
operator-actionable error). No error path echoes the fetched value.

CALLERS_JSON survives as an explicit override for the test suite and emergency
manual runs. The test is `+x`, not `-z`: a set-but-empty value still means
"empty roster", so an operator's deliberate `CALLERS_JSON=''` can never fall
through to bumping the live fleet.

Least-privilege regression, deliberate and unavoidable: the four entrypoints
that downscoped their App token (auto-label, groom, pr-risk,
detect-unreviewed-merge) no longer pass any `permission-*` input. A downscoped
installation token cannot read Actions variables, and there is no
`permission-variables` to ask for — GitHub's app-permissions schema has no
`variables` key, verified against the action's own action.yml at the pinned
v1.12.0 (48 permission-* inputs) and at its default branch (54): zero
occurrences of "variab" in either. Minting with no downscoping inputs is the
only spelling that yields the App's "Variables: read" grant, which is what the
other four entrypoints already did.

DEPLOY PRECONDITION: the Cloud Code Bot App needs repository permission
"Variables: read" granted and approved on the Comfy-Org installation. Without
it every fleet fails with the named 403 error.
@mattmillerai mattmillerai added the agent-coded Authored by the agent-work loop label Aug 5, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review August 5, 2026 04:02
@coderabbitai

coderabbitai Bot commented Aug 5, 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: 49 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: a1cbd87d-f332-43a5-a0fb-f8dafb29acc0

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7c9bf and 4a2159f.

📒 Files selected for processing (12)
  • .github/bump-callers/README.md
  • .github/bump-callers/bump-callers.sh
  • .github/bump-callers/tests/test_bump_callers.sh
  • .github/workflows/bump-agents-md-callers.yml
  • .github/workflows/bump-assign-reviewers-callers.yml
  • .github/workflows/bump-auto-label-callers.yml
  • .github/workflows/bump-cursor-review-callers.yml
  • .github/workflows/bump-detect-unreviewed-merge-callers.yml
  • .github/workflows/bump-groom-callers.yml
  • .github/workflows/bump-pr-risk-callers.yml
  • .github/workflows/bump-pr-size-callers.yml
  • AGENTS.md

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

@mattmillerai mattmillerai added the cursor-review Multi-model cursor review label Aug 5, 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 7 finding(s).

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

Panel: 8/8 reviewers contributed findings.

Comment thread .github/workflows/bump-auto-label-callers.yml
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/workflows/bump-detect-unreviewed-merge-callers.yml Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh Outdated
…en, and harden the read's failure modes (BE-6482)

Addresses the cursor-review panel on #138.

- Least-privilege regression (8/8 reviewers): the write token no longer has to
  be un-downscoped to read one variable. Each entrypoint now mints a SECOND app
  token, confined by `repositories: github-workflows` to this one public repo,
  and hands it to the script as VAR_TOKEN for the roster read alone. The four
  entrypoints that had `permission-contents`/`pull-requests`/`issues` get that
  trio back; VAR_TOKEN falls back to GH_TOKEN when unset (manual runs).
- Silent-404 (6/8): an absent variable still means "empty roster", but now
  always logs a ::warning:: naming both scopes tried — a 404 is also how GitHub
  answers "this token cannot see the repository at all", which would otherwise
  stop a whole ALLOW_EMPTY fleet from bumping in total silence.
- Repo-only narrowing: the read falls back to the org-level variable, matching
  what the `${{ vars.* }}` binding it replaced resolved (repo wins on a clash).
  Best-effort — a failed org probe stays an empty roster, never a hard error.
- 403 over-attribution: rate limiting is reported as a throttle, not as a
  missing grant, and the grant error now carries the API's own reason.
- mktemp is checked and the temp file is trapped on EXIT.
- The `::` squash loops to a fixpoint; one `s|::|:|g` pass is not one.

Docs: the bump-callers README and the entrypoint headers now describe the
two-token split instead of the removed downscoping; the detect-unreviewed-merge
ALLOW_EMPTY comment no longer contradicts its own header about seeding.

Tests: 7 new cases (which token performs the read + its fallback, the org-level
fallback and repo-wins precedence, a best-effort org failure, the 404 warning,
a rate-limited 403, and an adversarial error body that must not smuggle a
workflow command into the public log). 238 passed, 0 failed; shellcheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Pushed 4a2159f addressing all seven panel findings — each thread has a specific reply and is resolved.

The headline one: the least-privilege regression is gone, not documented. Each entrypoint now mints a second create-github-app-token confined by repositories: github-workflows, used for the roster read alone (VAR_TOKEN), so the write token that reaches every caller repo keeps its contents/pull-requests/issues downscoping — restored on all four entrypoints that lost it. Also: a 404 is now always warned about (and falls back to the org-level variable first), a rate-limited 403 is no longer reported as a missing permission, mktemp is checked and trapped, and the :: sanitizer loops to a fixpoint.

7 new test cases, 238 passed, 0 failed, shellcheck clean. The deploy precondition is unchanged: the App still needs Variables: read approved on the Comfy-Org installation before any bump-*-callers run can read its roster.

Not merging — this is review-gated.

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