-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[TRTLLM-16280][infra] Cap open PRs by contributor merge history #18944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
karljang
wants to merge
5
commits into
NVIDIA:main
Choose a base branch
from
karljang:chore/new-contributor-pr-rate-limit
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+475
−0
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
cd92db8
[None][infra] Rate limit new contributor PR submissions
karljang f956c4c
Enforce open PR cap and harden contributor limit recovery
karljang cf19366
Clarify maintainer-assisted reopening after moderation
karljang f199100
Use an open-PR cap and address moderation review feedback
karljang 0f1fb7b
Cap established contributors at ten open PRs
karljang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| const assert = require('node:assert/strict'); | ||
| const fs = require('node:fs'); | ||
| const { test } = require('node:test'); | ||
| const workflow = fs.readFileSync(`${__dirname}/../workflows/pr-rate-limit.yml`, 'utf8'); | ||
| function extractScript(source) { | ||
| const parts = source.split(' script: |\n'); | ||
| assert.equal(parts.length, 2, 'Expected exactly one inline script block'); | ||
| const lines = parts[1].split('\n'); | ||
| const end = lines.findIndex((line) => line.trim() && !line.startsWith(' ')); | ||
| return lines.slice(0, end < 0 ? lines.length : end) | ||
| .map((line) => line.replace(/^ {12}/, '')).join('\n'); | ||
| } | ||
| const script = extractScript(workflow); | ||
| const execute = new (Object.getPrototypeOf(async function () {}).constructor)( | ||
| 'github', 'context', 'core', 'process', 'Date', script, | ||
| ); | ||
| const now = Date.parse('2026-09-09T12:00:00Z'); | ||
| const hour = 3600000; | ||
| function pr(number, overrides = {}) { | ||
| return { number, created_at: new Date(now - hour + number * 1000).toISOString(), | ||
| state: 'open', labels: [], user: { id: 42, login: 'new-user', type: 'User' }, | ||
| pull_request: {}, ...overrides }; | ||
| } | ||
| async function run(options = {}) { | ||
| const current = options.current || pr(6); | ||
| const writes = options.writes || []; | ||
| const calls = []; | ||
| const warnings = []; | ||
| const github = { rest: { | ||
| pulls: { | ||
| get: async ({ pull_number }) => { | ||
| calls.push(pull_number); | ||
| if (options.readError) throw new Error('API failure'); | ||
| return { data: pull_number === current.number | ||
| ? (calls.filter((n) => n === current.number).length > 1 && options.recheck || current) | ||
| : { ...pr(pull_number), merged_at: options.merged ? '2026-09-08T00:00:00Z' : null } }; | ||
| }, | ||
| update: async (args) => { | ||
| if (options.closeError) throw new Error('close failed'); | ||
| writes.push({ kind: 'close', ...args }); | ||
| }, | ||
| }, | ||
| repos: { getCollaboratorPermissionLevel: async () => { | ||
| if (options.permissionError) throw Object.assign(new Error('permission failed'), { status: options.permissionStatus }); | ||
| return { data: { permission: options.permission || 'read', user: { permissions: { push: options.push } } } }; | ||
| } }, | ||
| issues: { | ||
| listForRepo: async (args) => { | ||
| assert.equal(args.creator, current.user.login); | ||
| assert.ok(['all', 'open'].includes(args.state)); | ||
| if (options.historyError || args.page === options.failedPage) throw new Error('history failed'); | ||
| if (args.state === 'open' && options.openHistoryError) throw new Error('open history failed'); | ||
| if (args.state === 'open') return { data: args.page === 1 | ||
| ? options.openHistory || (options.history || Array.from({ length: 6 }, (_, i) => pr(i + 1))) | ||
| .filter((item) => item.state === 'open') : [] }; | ||
| return { data: options.pages ? (options.pages[args.page - 1] || []) | ||
| : options.history || Array.from({ length: 6 }, (_, i) => pr(i + 1)) }; | ||
| }, | ||
| listComments: () => {}, | ||
| createComment: async (args) => { | ||
| if (options.commentError) throw new Error('comment failed'); | ||
| writes.push({ kind: 'comment', ...args }); | ||
| }, | ||
| }, | ||
| }, paginate: async () => { | ||
| if (options.commentsError) throw new Error('comments failed'); | ||
| return options.comments || []; | ||
| } }; | ||
| const context = { repo: { owner: 'NVIDIA', repo: 'TensorRT-LLM' }, payload: options.payload || { pull_request: current } }; | ||
| await execute(github, context, { info: () => {}, warning: (message) => warnings.push(message) }, { env: options.env || {} }, | ||
| class extends Date { static now() { return now; } }); | ||
| return { writes, calls, warnings }; | ||
| } | ||
| test('first five pass; sixth receives explanation before closure', async () => { | ||
| for (let n = 1; n <= 5; n++) assert.deepEqual((await run({ current: pr(n), history: Array.from({ length: n }, (_, i) => pr(i + 1)) })).writes, []); | ||
| const { writes } = await run(); | ||
| assert.deepEqual(writes.map((w) => w.kind), ['comment', 'close']); | ||
| assert.match(writes[0].body, /currently have 6 open PRs/); | ||
| assert.match(writes[0].body, /no merged PRs/); | ||
| assert.match(writes[0].body, /wait until your existing PRs are reviewed or merged/); | ||
| assert.doesNotMatch(writes[0].body, /24|cooldown|consolidate|rolling/); | ||
| assert.equal(writes[1].state, 'closed'); | ||
| }); | ||
| test('counts drafts but excludes closed PRs, ordinary issues and another author', async () => { | ||
| const history = Array.from({ length: 5 }, (_, i) => pr(i + 1, { draft: true })); | ||
| history.push(pr(-2, { state: 'closed' }), pr(0, { pull_request: undefined }), pr(-1, { user: { id: 99 } })); | ||
| assert.equal((await run({ history })).writes.length, 2); | ||
| }); | ||
|
|
||
|
|
||
| test('history pagination and duplicate entries do not change quota', async () => { | ||
| const first = Array.from({ length: 100 }, () => pr(1)); | ||
| const { writes } = await run({ pages: [first, [pr(2), pr(3), pr(4), pr(5)]] }); | ||
| assert.equal(writes.length, 2); | ||
| assert.match(writes[0].body, /currently have 6 open PRs/); | ||
| }); | ||
| test('actual merged history raises the cap; author association alone does not', async () => { | ||
| const history = Array.from({ length: 6 }, (_, i) => pr(i + 1)); | ||
| history.push(pr(0, { state: 'closed', pull_request: { merged_at: '2026-09-08T00:00:00Z' } })); | ||
| const result = await run({ history }); | ||
| assert.deepEqual(result.writes, []); | ||
| assert.deepEqual(result.calls, [6]); | ||
| assert.equal((await run({ current: pr(6, { author_association: 'CONTRIBUTOR' }) })).writes.length, 2); | ||
| }); | ||
| test('merged contributors may keep ten open PRs but the eleventh closes, including drafts', async () => { | ||
| const merged = pr(0, { state: 'closed', pull_request: { merged_at: '2026-09-08T00:00:00Z' } }); | ||
| for (let n = 6; n <= 10; n++) { | ||
| const history = [merged, ...Array.from({ length: n }, (_, i) => pr(i + 1))]; | ||
| assert.deepEqual((await run({ current: pr(n), history })).writes, []); | ||
| } | ||
| const history = [merged, ...Array.from({ length: 10 }, (_, i) => pr(i + 1))]; | ||
| const current = pr(11, { draft: true }); | ||
| const { writes, calls } = await run({ current, history }); | ||
| assert.deepEqual(writes.map((w) => w.kind), ['comment', 'close']); | ||
| assert.match(writes[0].body, /11 open PRs, including this one \(limit: 10\)/); | ||
| assert.match(writes[0].body, /Contributors with merged PRs/); | ||
| assert.match(writes[0].body, /fewer than 10 of your other PRs/); | ||
| assert.ok(calls.every((number) => number === 11)); | ||
| assert.deepEqual((await run({ current, history, openHistory: history.slice(1, 10) })).writes, []); | ||
| assert.deepEqual((await run({ current, history, env: { DRY_RUN: 'true' } })).writes, []); | ||
| }); | ||
| test('the established-contributor cap is configurable and invalid values skip safely', async () => { | ||
| const history = [pr(0, { state: 'closed', pull_request: { merged_at: '2026-09-08T00:00:00Z' } }), | ||
| ...Array.from({ length: 7 }, (_, i) => pr(i + 1))]; | ||
| const { writes } = await run({ current: pr(7), history, env: { MAX_OPEN_ESTABLISHED: '6' } }); | ||
| assert.match(writes[0].body, /limit: 6/); | ||
| for (const value of ['0', '-1', '1.5', 'bad', 'Infinity', '9007199254740992']) { | ||
| const result = await run({ env: { MAX_OPEN_ESTABLISHED: value } }); | ||
| assert.deepEqual(result.calls, []); | ||
| assert.deepEqual(result.writes, []); | ||
| assert.equal(result.warnings.length, 1); | ||
| } | ||
| }); | ||
| test('maintainers, bots, allowlisted users and labeled PRs are exempt', async () => { | ||
| for (const permission of ['write', 'admin', 'maintain']) | ||
| assert.deepEqual((await run({ permission })).writes, []); | ||
| assert.deepEqual((await run({ env: { EXEMPT_USERS: 'other, NEW-USER ' } })).writes, []); | ||
| assert.deepEqual((await run({ push: true })).writes, []); | ||
| assert.deepEqual((await run({ current: pr(6, { user: { id: 42, login: 'bot', type: 'Bot' } }) })).writes, []); | ||
| assert.deepEqual((await run({ current: pr(6, { labels: [{ name: 'pr-rate-limit-exempt' }] }) })).writes, []); | ||
| }); | ||
| test('dry run, closed PRs, and exemptions added during evaluation make no writes', async () => { | ||
| assert.deepEqual((await run({ env: { DRY_RUN: 'true' } })).writes, []); | ||
| assert.deepEqual((await run({ current: pr(6, { state: 'closed' }) })).writes, []); | ||
| assert.deepEqual((await run({ recheck: pr(6, { labels: [{ name: 'pr-rate-limit-exempt' }] }) })).writes, []); | ||
| assert.deepEqual((await run({ recheck: pr(6, { state: 'closed' }) })).writes, []); | ||
| }); | ||
|
|
||
| test('reruns reuse only bot-authored comments and retry closure', async () => { | ||
| const body = '<!-- new-contributor-pr-rate-limit:v2 -->'; | ||
| const trusted = { user: { login: 'github-actions[bot]' }, body }; | ||
| assert.deepEqual((await run({ comments: [trusted] })).writes.map((w) => w.kind), ['close']); | ||
| assert.equal((await run({ comments: [{ user: { login: 'new-user' }, body }] })).writes.length, 2); | ||
| }); | ||
| test('unexpected API failures abort without moderation', async () => { | ||
| for (const options of [{ readError: true }, { historyError: true }, { commentError: true }, | ||
| { permissionError: true }, { commentsError: true }, { openHistoryError: true }, | ||
| { pages: [Array.from({ length: 100 }, () => pr(1))], failedPage: 2 }]) { | ||
| const writes = []; | ||
| await assert.rejects(run({ ...options, writes })); | ||
| assert.deepEqual(writes, []); | ||
| } | ||
| }); | ||
| test('successful comment survives a close failure and is not duplicated on retry', async () => { | ||
| const writes = []; | ||
| await assert.rejects(run({ closeError: true, writes }), /close failed/); | ||
| assert.deepEqual(writes.map((w) => w.kind), ['comment']); | ||
| const comments = [{ user: { login: 'github-actions[bot]' }, body: writes[0].body }]; | ||
| assert.deepEqual((await run({ comments })).writes.map((w) => w.kind), ['close']); | ||
| }); | ||
| test('workflow uses trusted inline code, minimal permissions and per-PR concurrency', () => { | ||
| assert.match(workflow, /pull_request_target:/); | ||
| assert.match(workflow, /types: \[opened, reopened, ready_for_review\]/); | ||
| assert.match(workflow, /pull-requests: write/); | ||
| assert.match(workflow, /issues: read/); | ||
| assert.match(workflow, /group: pr-rate-limit-\$\{\{ github.event.pull_request.number \|\| inputs.pr_number \}\}/); | ||
| assert.doesNotMatch(workflow, /actions\/checkout|head.sha|secrets\./); | ||
| }); | ||
|
|
||
| test('manual recovery evaluates the requested PR and rejects invalid input before API calls', async () => { | ||
| const { writes, calls } = await run({ payload: { inputs: { pr_number: '6' } } }); | ||
| assert.deepEqual(writes.map((w) => w.kind), ['comment', 'close']); | ||
| assert.ok(calls.every((number) => number === 6)); | ||
| for (const pr_number of ['', '0', '-1', '1.5', '06', ' 6', '6x', '9007199254740992']) { | ||
| await assert.rejects(run({ payload: { inputs: { pr_number } }, readError: true }), /positive integer PR number/); | ||
| } | ||
| await assert.rejects(run({ payload: {}, readError: true }), /positive integer PR number/); | ||
| }); | ||
|
|
||
| test('manual recovery preserves dry run, exemptions, capacity and comment reconciliation', async () => { | ||
| const payload = { inputs: { pr_number: '6' } }; | ||
| assert.deepEqual((await run({ payload, env: { DRY_RUN: 'true' } })).writes, []); | ||
| assert.deepEqual((await run({ payload, permission: 'write' })).writes, []); | ||
| assert.deepEqual((await run({ payload, current: pr(6, { created_at: new Date(now - 24 * hour).toISOString() }), history: [] })).writes, []); | ||
| const comments = [{ user: { login: 'github-actions[bot]' }, body: '<!-- new-contributor-pr-rate-limit:v2 -->' }]; | ||
| assert.deepEqual((await run({ payload, comments })).writes.map((w) => w.kind), ['close']); | ||
| }); | ||
|
|
||
| test('privileged action is pinned and manual runs are restricted to the default branch', () => { | ||
| assert.match(workflow, /uses: actions\/github-script@[0-9a-f]{40} # v8/); | ||
| assert.match(workflow, /workflow_dispatch:/); | ||
| assert.match(workflow, /github.event_name != 'workflow_dispatch' \|\|/); | ||
| assert.ok(workflow.includes("github.ref == format('refs/heads/{0}', github.event.repository.default_branch)")); | ||
| }); | ||
|
|
||
| test('open-PR limit prevents reopening an old backlog', async () => { | ||
| const current = pr(6, { created_at: new Date(now - 48 * hour).toISOString() }); | ||
| const history = Array.from({ length: 5 }, (_, i) => pr(i + 1, { draft: true })); | ||
| const { writes } = await run({ current, history }); | ||
| assert.deepEqual(writes.map((w) => w.kind), ['comment', 'close']); | ||
| assert.match(writes[0].body, /currently have 6 open PRs/); | ||
| assert.match(writes[0].body, /ask a maintainer to reopen this PR when fewer than 5 of your other PRs are open/); | ||
| assert.doesNotMatch(writes[0].body, /cooldown ends/); | ||
| assert.deepEqual((await run({ current, history: history.slice(0, 4) })).writes, []); | ||
| }); | ||
|
|
||
| test('old reopened PR is allowed when capacity becomes available during evaluation', async () => { | ||
| const current = pr(6, { created_at: new Date(now - 48 * hour).toISOString() }); | ||
| assert.deepEqual((await run({ current, openHistory: [pr(1), pr(2)] })).writes, []); | ||
| }); | ||
|
|
||
| test('open-PR limit applies even when recent submissions are below five', async () => { | ||
| const history = Array.from({ length: 5 }, (_, i) => pr(i + 1, { created_at: new Date(now - 48 * hour).toISOString() })); | ||
| const { writes } = await run({ history }); | ||
| assert.match(writes[0].body, /currently have 6 open PRs/); | ||
| assert.doesNotMatch(writes[0].body, /cooldown ends/); | ||
| }); | ||
|
|
||
| test('a reopened backlog of old PRs stays subject to the open cap during manual recovery', async () => { | ||
| const history = Array.from({ length: 100 }, (_, i) => pr(i + 1, { created_at: new Date(now - 48 * hour).toISOString() })); | ||
| const current = history[99]; | ||
| const { writes } = await run({ current, pages: [history, []], openHistory: history, payload: { inputs: { pr_number: '100' } } }); | ||
| assert.match(writes[0].body, /currently have 100 open PRs/); | ||
| assert.equal(writes[1].pull_number, 100); | ||
| }); | ||
|
|
||
| test('closed submissions never consume capacity, regardless of timestamps', async () => { | ||
| const history = Array.from({ length: 30 }, (_, i) => pr(i + 1, { state: 'closed', created_at: 'invalid' })); | ||
| assert.deepEqual((await run({ history })).writes, []); | ||
| }); | ||
| test('permission denials and incomplete history warn and skip', async () => { | ||
| for (const options of [ | ||
| { permissionError: true, permissionStatus: 403 }, | ||
| { permissionError: true, permissionStatus: 404 }, | ||
| { pages: Array.from({ length: 20 }, () => Array.from({ length: 100 }, () => pr(1))) }, | ||
| ]) { | ||
| const result = await run(options); | ||
| assert.deepEqual(result.writes, []); | ||
| assert.equal(result.warnings.length, 1); | ||
| } | ||
| }); | ||
| test('configured open cap is enforced and invalid configuration skips all API calls', async () => { | ||
| assert.deepEqual((await run({ env: { MAX_OPEN: '6' } })).writes, []); | ||
| const { writes } = await run({ env: { MAX_OPEN: '3' } }); | ||
| assert.match(writes[0].body, /limit: 3/); | ||
| for (const value of ['0', '-1', '1.5', 'bad', 'Infinity', '9007199254740992']) { | ||
| const result = await run({ env: { MAX_OPEN: value } }); | ||
| assert.deepEqual(result.calls, []); | ||
| assert.deepEqual(result.writes, []); | ||
| assert.equal(result.warnings.length, 1); | ||
| } | ||
| }); | ||
| test('script extraction stops at a subsequent step and rejects ambiguous blocks', () => { | ||
| assert.equal(extractScript(workflow + '\n - run: echo later\n'), script); | ||
| assert.throws(() => extractScript(''), /exactly one/); | ||
| assert.throws(() => extractScript(workflow + ' script: |\n'), /exactly one/); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.