Skip to content

ci: comment-cop should scan the live base...head compare, not the PR's cached file list - #38127

Open
robobun wants to merge 1 commit into
mainfrom
farm/b57b0ca8/comment-cop-live-compare
Open

robobun wants to merge 1 commit into
mainfrom
farm/b57b0ca8/comment-cop-live-compare

Conversation

@robobun

@robobun robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Problem

Fix

  • Take the diff from repos.compareCommitsWithBasehead with base.ref...head.sha. GitHub computes that from the current merge base at request time, so it is the PR's actual diff no matter what the PR's cached diff says; the file entries have the same shape (filename, status, patch) as pulls.listFiles, so the parser and the dedup keys are unchanged. Requested with per_page: 1: the changed files all come on the first page (at most 300), per_page only pages the commit list.
  • Post at most 25 new comments per run (MAX_COMMENTS_PER_RUN), with a warning annotation when the cap is hit, so a run's write volume is bounded by design instead of by the job timeout. Groups left over are still in the diff on the next push and get posted then (dedup is per group, unchanged). Of about 30 claude PRs I scanned with the new script, all but one have 12 or fewer groups in their real diff; the exception, watcher(windows): watch workspace packages outside the project root #35596, has 40 (33 of them in one file) and would get 25 on the first run and the other 15 on the push after.
  • Since presentKeys now comes from the real diff, the existing auto-resolve step identifies the bogus threads as stale (240 on fetch: reject network errors as TypeError('fetch failed' / 'terminated') with the system error as cause #35988) and resolves them once ci: use a PAT to resolve stale comment-cop review threads #36959 gives it a token that can.
  • Log the file count and merge base of the compare so the next incident of this kind is visible in the run log.

How did you verify your code works?

The workflow runs on pull_request_target from the base branch, so CI on this PR does not exercise it. I dry-ran the embedded script (extracted from the YAML, real GitHub reads, createReviewComment and the resolve mutation recorded instead of sent) against live PRs:

Does not overlap with #37948 (which groups count as a comment) or #36959 (token used to resolve threads); both touch other parts of the same script.

Per-PR audit of existing Comment Cop threads against each PR's live compare
PR #35635: cop_comments=174 on_files_outside_live_diff=172 | pr.changed_files=6    live_compare_files=6
PR #35708: cop_comments=155 on_files_outside_live_diff=154 | pr.changed_files=2    live_compare_files=2
PR #36713: cop_comments=126 on_files_outside_live_diff=123 | pr.changed_files=1946 live_compare_files=11
PR #35596: cop_comments=70  on_files_outside_live_diff=65  | pr.changed_files=8    live_compare_files=8
PR #36956: cop_comments=64  on_files_outside_live_diff=59  | pr.changed_files=21   live_compare_files=21
PR #33632: cop_comments=31  on_files_outside_live_diff=26  | pr.changed_files=10   live_compare_files=10
PR #36534: cop_comments=25  on_files_outside_live_diff=20  | pr.changed_files=9    live_compare_files=9
PR #32435: cop_comments=24  on_files_outside_live_diff=20  | pr.changed_files=13   live_compare_files=13
PR #28531: cop_comments=21  on_files_outside_live_diff=14  | pr.changed_files=11   live_compare_files=11
PR #35988: cop_comments=245 on_files_outside_live_diff=241 | pr.changed_files=3133 live_compare_files=20

For #35635, #35708 and #36713, 43 of 45, 53 of 54 and 37 of 40 of the commented-on paths were never touched by any non-merge commit on the branch (git log --no-merges --name-only origin/main..<head>), and the threads were posted in one burst per push lasting until the 5 minute timeout (for example #35635: 160 threads between 03:08 and 03:13 UTC on Aug 13, right after a rebase). pr.changed_files for most of these has since been recomputed to the right number; the threads were posted while it was not.

comment-cop took its file list from pulls.listFiles. Right after a rebase
or a merge from main, that list can still be relative to the base the PR
was opened against, so it contains every file main has touched since then
and the workflow flags comments the PR never added. It posted one review
comment per group with no bound, so each such run kept posting until the
5 minute job timeout cancelled it (about 120 comments per push).

Take the diff from repos.compareCommitsWithBasehead(base.ref...head.sha),
which GitHub computes from the current merge base at request time, and
post at most 25 new comments per run, logging a warning when capped.
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced and fixed.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 72410478-f489-4d16-a573-cfdeb58d2033

📥 Commits

Reviewing files that changed from the base of the PR and between b7a0431 and 3259e6f.

📒 Files selected for processing (1)
  • .github/workflows/comment-cop.yml

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — targeted CI tooling fix that swaps the stale PR file cache for a live compare and caps per-run posts.

What was reviewed:

  • compareCommitsWithBasehead response shape (files[].filename/status/patch) matches what the existing parser and dedup key consume, so downstream logic is unchanged.
  • Fork-PR head SHAs are reachable in the base repo via refs/pull/N/head, so the compare against oven-sh/bun resolves them; base refs containing / are path-encoded by octokit.
  • per_page: 1 only pages the commit list; the ≤300 files all arrive on the first page, and the 300-file ceiling is acceptable for a best-effort lint on claude-labeled PRs.
  • The .slice(0, MAX_COMMENTS_PER_RUN) cap composes correctly with the existing content-hash dedup, so overflow groups are picked up on the next push rather than lost.
Extended reasoning...

Overview

Single-file change to .github/workflows/comment-cop.yml, the internal bot that flags multi-line code comments on claude-labeled PRs. Two functional edits: (1) source the changed-file list from repos.compareCommitsWithBasehead(${base.ref}...${head.sha}) instead of pulls.listFiles, because the latter can return a stale, massively inflated diff right after a rebase/merge-from-main; (2) cap new review comments to 25 per run with a core.warning when truncating. A diagnostic core.info line logs the compare's file count and merge base.

Security risks

None introduced. The workflow already runs on pull_request_target with pull-requests: write and never checks out PR code. The only new input read from the event payload is base.ref, which is controlled by the base repo, not the PR author. No shell interpolation, no new tokens, no widened permissions.

Level of scrutiny

Low. This is repo-internal CI tooling with a bounded, reversible effect (posting/resolving review comments). The failure mode being fixed is comment spam; the worst case for the new code is under-posting (300-file compare ceiling, 25-comment cap) or a failed API call, both of which degrade to "fewer nag comments" rather than anything user-facing or build-affecting. It cannot be exercised by this PR's own CI (pull_request_target runs base-branch code), and the PR description documents dry-runs against several live PRs — including one with a currently-stale cached diff — showing identical output on accurate PRs and correct output on the pathological ones.

Other factors

The response-shape compatibility (filename, status, patch) means groupsFromPatch, keyFor, and the auto-resolve logic are untouched and continue to work; compare.files ?? [] guards the one field that can be absent. The 25-per-run cap interacts safely with the existing seenKeys dedup: unposted groups remain in the diff and are posted on the next synchronize. No prior reviewer comments to address; the bug-hunting pass found nothing.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:05 AM PT - Aug 13th, 2026

@robobun, your commit 3259e6f is building: #94450

@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:05 AM PT - Aug 13th, 2026

❌ @robobun, your commit 3259e6f has some failures in Build #94450 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38127

That installs a local version of the PR into your bun-38127 executable, so you can run:

bun-38127 --bun

Jarred-Sumner pushed a commit that referenced this pull request Aug 28, 2026
…austed (#39183)

### Problem

- The `Comment Cop` check is red on every claude-labeled PR whenever the
repo's GraphQL quota is used up. The step dies before it scans anything:
  ```
  GraphqlResponseError: Request failed due to following response errors:
   - API rate limit already exceeded for site ID installation.
  ##[error]Unhandled error: GraphqlResponseError: ...
  ```
with `x-ratelimit-resource: graphql`, `x-ratelimit-limit: 10000`,
`x-ratelimit-remaining: 0` (run
[31898901053](https://github.com/oven-sh/bun/actions/runs/31898901053)
on #39139). All 33 comment-cop failures on Aug 15 are this error (197
runs succeeded, 171 were skipped); they hit unrelated PRs at the same
time because the quota is shared by every workflow run in the repo, and
they come back whenever PR volume is high.
- Cause, `.github/workflows/comment-cop.yml:124` on main: the step reads
the PR's existing review threads with `github.graphql()`, outside any
`try`, and dedup and posting both sit behind that call. The REST call
just before it (`pulls.listFiles`) had succeeded in each failing run, so
the step had the diff; it only lacked the dedup data, which it was
reading from the exhausted quota.
- The same thread data also fed an auto-resolve of stale threads. That
part does not work: under the Actions `GITHUB_TOKEN` every
`resolveReviewThread` mutation fails with `Resource not accessible by
integration`, after which the step still logs `Resolved N stale
comment-cop thread(s).` Two successful runs from today:
[31900717034](https://github.com/oven-sh/bun/actions/runs/31900717034)
(20 attempted, 20 failed, "Resolved 20") and
[31900354924](https://github.com/oven-sh/bun/actions/runs/31900354924)
(4 attempted, 4 failed, "Resolved 4"). #36959 documents the same thing
and is the PR that moves resolution to a token that can do it. So the
GraphQL query was paying for one thing REST can provide and one thing
that does not happen.

### Fix

- Dedup reads the PR's review comments with `pulls.listReviewComments`
(REST) and collects the `<!-- comment-cop:KEY -->` markers from them.
Every comment the step posts starts with that marker, so the review
comments carry the same keys the thread roots did; REST is the quota the
step already needs for `listFiles` and `createReviewComment`, and it was
available in every failing run.
- The GraphQL query and the resolve loop are removed, so the step makes
no GraphQL request at all; the failure in the Problem section cannot
happen, rather than being caught. Nothing observable is lost: the
mutations the loop issued all fail today, and the only other thing it
did was log `Resolved N stale comment-cop thread(s).` after they had
failed. Resolving stale threads stays with #36959, which will also need
to move its thread lookup into its token step, since this PR removes the
`GITHUB_TOKEN` lookup it currently reuses (noted there).
- What the step posts is unchanged: the script on main and the REST
dedup were dry-run (real reads, writes recorded) against 9 PRs carrying
existing comment-cop threads (#35988, #36956, #36713, #35635, #33632,
#35596, #39139, #30609, #36959) and chose the same comments to post on
every one of them; the canned scenarios below show the same thing with
GraphQL working and with it exhausted.
- Test: `test/internal/source-lints/comment-cop.test.ts` extracts the
script from the workflow and runs it the way `actions/github-script`
does, against a fake `github` whose GraphQL requests all fail with the
rate limit error above. It checks that groups not yet flagged are posted
with the right line ranges, that a group already flagged (and a stale
marker) are left alone, that a second run recognizes the comments the
first run posted and posts nothing, and that no GraphQL request is made.
Both tests fail against the workflow on main (the script throws the
error above) and pass with this change. `source-lints.yml` now also
triggers on changes to `comment-cop.yml`, so the test runs whenever the
script is edited; it is excluded from the Buildkite shards like the rest
of that directory.
- Also ran: `bun bd test
test/internal/source-lints/comment-cop.test.ts`, `bun test
test/internal/source-lints/` (whole directory green), prettier on the
three files. The comment-cop run on this PR itself still executes the
script from main (`pull_request_target`), so the `Source lints` job is
the one that exercises the change here.
- Does not overlap with #37948 (which groups are flagged) or #38127
(where the file list comes from); both touch other parts of the script.
#36959 rewrites the block this PR deletes and will need a rebase either
way.

### Background

- Comment Cop (`.github/workflows/comment-cop.yml`): on each push to a
claude-labeled PR it reads the PR diff, finds multi-line comments added
under `src/`, and posts one review comment per comment block. Each bot
comment starts with `<!-- comment-cop:KEY -->`, KEY being the file path
plus a hash of the block's text; a block whose KEY is already on the PR
is not posted again. The check is advisory (not required for merge).
- GitHub API quotas: REST and GraphQL requests count against separate
hourly quotas (`x-ratelimit-resource` is `core` for REST and `graphql`
for GraphQL). For the `GITHUB_TOKEN` Actions hands out, each quota is
per repository, so every workflow run in oven-sh/bun draws on the same
two pools, and GraphQL-heavy automation (the `gh pr` / `gh issue` / `gh
search` commands used by other workflows go through GraphQL) empties the
GraphQL pool for everything else when PR volume is high.
- Review threads vs review comments: a review thread is GraphQL's
grouping of a line comment with its replies, and is the only place a
thread's id (what `resolveReviewThread` takes) and its resolved flag
exist. `GET /repos/{owner}/{repo}/pulls/{n}/comments` returns every
review comment on the PR, including each thread's root comment, so the
markers are reachable from REST; only resolving needs GraphQL, and under
`GITHUB_TOKEN` GitHub refuses that mutation regardless of the
`pull-requests: write` permission.

<details>
<summary>Script on main vs this branch against a fake github (the fake's
resolve mutation fails the way GITHUB_TOKEN's does)</summary>

```
main   | quota ok, stale threads present                        | exit 0                                   | posts ["src/foo.ts:5-6"] | resolve attempts ["T_STALE_OPEN"] | graphql ["query","query","mutation"] | warnings 1
main   | graphql quota exhausted, stale threads present         | step fails (unhandled rate limit error)  | posts [] | resolve attempts [] | graphql ["query"] | warnings 0
main   | graphql quota exhausted, nothing stale                 | step fails (unhandled rate limit error)  | posts [] | resolve attempts [] | graphql ["query"] | warnings 0
main   | graphql quota exhausted, PR has no review comments yet | step fails (unhandled rate limit error)  | posts [] | resolve attempts [] | graphql ["query"] | warnings 0
fixed  | quota ok, stale threads present                        | exit 0                                   | posts ["src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0
fixed  | graphql quota exhausted, stale threads present         | exit 0                                   | posts ["src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0
fixed  | graphql quota exhausted, nothing stale                 | exit 0                                   | posts ["src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0
fixed  | graphql quota exhausted, PR has no review comments yet | exit 0                                   | posts ["src/foo.ts:2-3","src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0
```

</details>

<details>
<summary>Dry-run against live PRs: comments the script on main would
post vs the REST dedup (real reads through a user token, writes
recorded)</summary>

```
PR #35988: same posts (0 vs 0)        245 threads on the PR are stale; main attempts to resolve them, this branch does not
PR #36956: same posts (0 vs 0)
PR #36713: same posts (1832 vs 1832)  the stale cached file list that #38127 fixes; identical on both
PR #35635: same posts (0 vs 0)
PR #33632: same posts (0 vs 0)
PR #35596: same posts (0 vs 0)
PR #39139: same posts (1 vs 1)        the comment the failing run above did not get to post
PR #30609: same posts (0 vs 0)
PR #36959: same posts (0 vs 0)
```

</details>

<details>
<summary>Headers from the failing run</summary>

```
errors: [ { type: 'RATE_LIMIT', code: 'graphql_rate_limit',
            message: 'API rate limit already exceeded for site ID installation.' } ]
variables: { owner: 'oven-sh', repo: 'bun', pr: 39139, after: null }
'x-ratelimit-limit': '10000'
'x-ratelimit-remaining': '0'
'x-ratelimit-resource': 'graphql'
'x-ratelimit-used': '10000'
```

</details>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants