Skip to content

Page past gh's 100-file cap when matching CODEOWNERS - #26

Merged
bryanbeverly merged 2 commits into
mainfrom
bryanbeverly/cursor/pr-labeler-file-cap
Oct 10, 2026
Merged

bryanbeverly merged 2 commits into
mainfrom
bryanbeverly/cursor/pr-labeler-file-cap

Conversation

@bryanbeverly

@bryanbeverly bryanbeverly commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The labeler silently skipped domain labeling on any PR touching more than 100 files. fetch_pr reads the changed-file list from gh pr view --json files, which gh requests with first: 100 and never pages, so the list comes back truncated with no warning. Still true as of gh 2.101.0.

Truncation isn't a harmless approximation for CODEOWNERS matching. CODEOWNERS is last-match-wins, and the deliberately ownerless go.mod / go.sum / /vendor/ rules that let Renovate automerge sort near the end of the file. A large dependency bump therefore fills the entire 100-file window with unowned vendor churn and buries the handful of first-party paths that carry the ownership signal, so the PR reads as owned by nobody: no domain/* labels and no reason comment.

Found via thog#7041 (since closed), a trufflehog digest bump. At the time it changed 351 files; the 11 owned ones under vendor/github.com/trufflesecurity/trufflehog/ sat at positions 340-350, well past the cutoff. GitHub itself requested Scanning and Integrations as reviewers (it evaluates every file) while the labeler computed zero owners -- that divergence is the bug.

The fix re-fetches the full path list through a paged GraphQL query, gated behind a tripwire so the common case is untouched.

Why GraphQL rather than REST pulls/{n}/files

REST embeds the full patch hunk for every file with no way to project it away. Measured on thog#7041: ~1.8MB transferred to learn 351 strings, versus ~25KB for the paths-only GraphQL query. Same wall time, ~70x the bytes.

Cost

Gated on the returned list being exactly 100 entries. Anything shorter is provably complete and keeps the existing single API call.

Before After
PR under 100 files 1 call 1 call (unchanged)
PR at/over 100 files 1 call, wrong answer 1 call + 1 call per 100 files (max 30)

As of Oct 9, 5 of 243 open thog PRs (about 2%) hit the cap, so ~98% of runs are byte-for-byte identical in cost. This matters most in backfill mode, which fans out across every open PR; an ungated second call would have added one request per PR there.

Testing

  • pytest .github/scripts -- 155 passed (7 new across TestPrChangedFiles and TestFetchPrFilePaths), covering the fast path at 6 and 99 files, the re-fetch at exactly 100, a missing files key, owner/repo splitting into query variables (sent with -f so an all-digit name isn't coerced to an Int), and the $endCursor/hasNextPage pagination contract that --paginate depends on.
  • ruff check and ruff format --check clean at the pinned 0.15.22.
  • Dry runs (DRY_RUN=true) against every open thog PR over the cap, on both main and this branch:
PR Files Paged Difference from main
#4543 19,858 3,000 none
#4928 1,025 1,025 reason comment updated; labels unchanged
#1589 677 677 none
#5986 105 105 none
#6001 101 101 none
  • Original dry run against the regression, before thog#7041 closed:
$ GITHUB_REPOSITORY=trufflesecurity/thog PR_NUMBER=7041 DRY_RUN=true python .github/scripts/pr_labeler.py
Loaded 138 CODEOWNERS rule(s) for domain labeling
PR #7041 (paged 351 changed files past gh's cap)
PR #7041 +domain/scanning +domain/integrations [comment: domain reasons created]

Follow-up after merge

None required. No open thog PR gets different labels; the only change a backfill would make today is refreshing #4928's reason comment, which its next push will do anyway.

Known limit

GitHub caps the files connection at 3000, the same ceiling REST and the "Files changed" tab enforce, so domain labeling stays best-effort beyond that. Noted in a comment on the query rather than worked around. Reaching that ceiling takes 30 page requests: on thog#4543 (a stale draft with 19,858 files) a labeler run goes from under a second to about 56 seconds.


Note

Low Risk
Scoped to PR labeler file discovery for CODEOWNERS; adds one extra GitHub API call only when the file list hits the 100-entry cap, with no change to labeling logic beyond using a complete file list.

Overview
Fixes silent wrong domain labeling on PRs with 100+ changed files. The labeler used gh pr view --json files, which only returns the first 100 paths, so large Renovate/vendor bumps could show no domain/* labels or review-reason comment even when GitHub requested the right teams.

pr_changed_files keeps the existing list when it has fewer than 100 entries; at exactly 100 it re-fetches all paths via a paginated GraphQL query (fetch_pr_file_paths) that returns paths only (not full patches). Domain matching now calls this helper instead of reading pr["files"] directly.

Adds unit tests for the fast path (6/99 files), the refetch at 100 files, GraphQL pagination variables, and query contract ($endCursor / hasNextPage).

Reviewed by Cursor Bugbot for commit 76e4ab4. Bugbot is set up for automated code reviews on this repo. Configure here.

`gh pr view --json files` truncates at 100 files with no warning, so large
dependency bumps were labeled as if they touched nothing anyone owns. The
ownerless go.mod/go.sum//vendor/ rules that enable Renovate automerge sort last
in CODEOWNERS, so vendor churn fills the whole window and buries the first-party
paths that carry the ownership signal -- yielding no domain/* labels and no
reason comment even while GitHub requested the owning teams as reviewers.

Re-fetch the full path list via a paged GraphQL query, but only when the
embedded list is long enough to be suspect, so PRs under the cap keep their
single API call.

Co-authored-by: Cursor <cursoragent@cursor.com>
@bryanbeverly
bryanbeverly requested a review from a team August 6, 2026 16:07
`gh api -F` coerces integer-looking values to JSON numbers, so an all-digit
org or repo name would reach the query as an Int and fail its String!
variable. `-f` sends them verbatim; the PR number stays on -F because the
query types it as Int!.

Co-authored-by: Cursor <cursoragent@cursor.com>
@bryanbeverly
bryanbeverly merged commit 2b24d79 into main Oct 10, 2026
4 checks passed
@bryanbeverly
bryanbeverly deleted the bryanbeverly/cursor/pr-labeler-file-cap branch October 10, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants