This is an automated email from the ASF dual-hosted git repository.

thisisnic pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 20373450d9 GH-51199: [CI] Limit concurrent PRs from new contributors 
(#51200)
20373450d9 is described below

commit 20373450d9abf067b8ef3e840a7ee21ac32f8c4b
Author: Nic Crane <[email protected]>
AuthorDate: Mon Sep 14 14:09:31 2026 -0400

    GH-51199: [CI] Limit concurrent PRs from new contributors (#51200)
    
    ### Rationale for this change
    
    Too many low-quality or drive-by AI-generated PRs from non-committers 
blocking up review capacity
    
    ### What changes are included in this PR?
    
    Prevent non-committers opening more than 3 concurrent PRs.  This may be a 
little annoying for those who are engaging with effort, but PRs can be reopened 
later and so is an overall net positive.
    
    Committers can reopen closed PRs on behalf of non-committers without this 
triggering.
    
    ### Are these changes tested?
    
    I'll test on my fork
    
    ### Are there any user-facing changes?
    
    No
    
    ### AI Usage
    
    Generated this, but asked questions about security etc
    
    * GitHub Issue: #51199
    
    Lead-authored-by: Nic Crane <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    Signed-off-by: Nic Crane <[email protected]>
---
 .github/workflows/pr_limit.yml         |  66 +++++++++++++++++
 .github/workflows/pr_limit/check.js    | 130 +++++++++++++++++++++++++++++++++
 .github/workflows/pr_limit/comment.md  |  31 ++++++++
 docs/source/developers/bug_reports.rst |  12 +++
 4 files changed, 239 insertions(+)

diff --git a/.github/workflows/pr_limit.yml b/.github/workflows/pr_limit.yml
new file mode 100644
index 0000000000..2174a61ed3
--- /dev/null
+++ b/.github/workflows/pr_limit.yml
@@ -0,0 +1,66 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# Limits the number of concurrently open pull requests a contributor
+# without write access can have. This mirrors GitHub's native
+# "pull request creation cap" setting, which requires admin rights and
+# so can only be configured for ASF repositories via .asf.yaml. Once
+# https://github.com/apache/infrastructure-asfyaml/pull/111 is merged,
+# this workflow can be replaced by the `github.pull_requests.creation_cap`
+# directive in .asf.yaml.
+
+name: PR Limit
+
+on:
+  pull_request_target:
+    types:
+      - opened
+      - reopened
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.repository }}-${{ 
github.event.number }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+  pull-requests: write
+  issues: write
+env:
+  # Maximum number of open pull requests per contributor without write
+  # access. A pull request that takes the contributor over this limit is
+  # closed with an explanatory comment.
+  PR_LIMIT: 3
+
+jobs:
+  check:
+    name: Check
+    runs-on: ubuntu-latest
+    steps:
+      # Always run the script from main, never from the PR head.
+      - uses: actions/checkout@v7
+        with:
+          repository: apache/arrow
+          ref: main
+          persist-credentials: false
+
+      - name: Check open pull request count
+        uses: actions/github-script@v9
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const script = 
require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/pr_limit/check.js`);
+            await script({github, context, core});
diff --git a/.github/workflows/pr_limit/check.js 
b/.github/workflows/pr_limit/check.js
new file mode 100644
index 0000000000..496895ddc0
--- /dev/null
+++ b/.github/workflows/pr_limit/check.js
@@ -0,0 +1,130 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 fs = require("fs");
+
+const WRITE_PERMISSIONS = new Set(["write", "maintain", "admin"]);
+
+/**
+ * Returns whether the user has write access to the repository.
+ *
+ * Note that `author_association` is not a reliable signal for this:
+ * ASF members show up as MEMBER regardless of their permission on this
+ * repository, and triage collaborators show up as COLLABORATOR.
+ *
+ * @param {Object} github
+ * @param {Object} context
+ * @param {String} username
+ */
+async function hasWriteAccess(github, context, username) {
+  try {
+    const {data} = await github.rest.repos.getCollaboratorPermissionLevel({
+      owner: context.repo.owner,
+      repo: context.repo.repo,
+      username: username
+    });
+    return WRITE_PERMISSIONS.has(data.permission);
+  } catch (error) {
+    if (error.status === 404) {
+      return false;
+    }
+    throw error;
+  }
+}
+
+/**
+ * Returns the number of open pull requests authored by the user in this
+ * repository, including the one that triggered the workflow.
+ *
+ * @param {Object} github
+ * @param {Object} context
+ * @param {String} username
+ */
+async function countOpenPullRequests(github, context, username) {
+  const {data} = await github.rest.search.issuesAndPullRequests({
+    q: `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open 
author:${username}`,
+    per_page: 1
+  });
+  return data.total_count;
+}
+
+/**
+ * Comments on the pull request explaining the limit, then closes it.
+ *
+ * @param {Object} github
+ * @param {Object} context
+ * @param {Number} pullRequestNumber
+ * @param {String} username
+ * @param {Number} limit
+ * @param {Number} count
+ */
+async function commentAndClose(github, context, pullRequestNumber, username, 
limit, count) {
+  const commentPath = ".github/workflows/pr_limit/comment.md";
+  const comment = fs.readFileSync(commentPath).toString()
+    .replaceAll("${PR_LIMIT}", limit)
+    .replaceAll("${OPEN_COUNT}", count)
+    .replaceAll("${USERNAME}", username);
+  await github.rest.issues.createComment({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    issue_number: pullRequestNumber,
+    body: comment
+  });
+  await github.rest.pulls.update({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    pull_number: pullRequestNumber,
+    state: "closed"
+  });
+}
+
+module.exports = async ({github, context, core}) => {
+  const limit = parseInt(process.env.PR_LIMIT, 10);
+  if (!Number.isInteger(limit) || limit < 1) {
+    throw new Error(`PR_LIMIT must be a positive integer, got: 
${process.env.PR_LIMIT}`);
+  }
+
+  const pullRequestNumber = context.payload.number;
+  const user = context.payload.pull_request.user;
+
+  if (user.type === "Bot") {
+    core.info(`Skipping: ${user.login} is a bot.`);
+    return;
+  }
+
+  if (await hasWriteAccess(github, context, user.login)) {
+    core.info(`Skipping: ${user.login} has write access.`);
+    return;
+  }
+
+  // A committer reopening a previously closed pull request is a deliberate
+  // decision to accept it, so don't close it again.
+  const sender = context.payload.sender;
+  if (sender.login !== user.login && await hasWriteAccess(github, context, 
sender.login)) {
+    core.info(`Skipping: ${context.payload.action} by ${sender.login}, who has 
write access.`);
+    return;
+  }
+
+  const count = await countOpenPullRequests(github, context, user.login);
+  core.info(`${user.login} has ${count} open pull request(s); limit is 
${limit}.`);
+  if (count <= limit) {
+    return;
+  }
+
+  core.info(`Closing #${pullRequestNumber}: over the limit.`);
+  await commentAndClose(github, context, pullRequestNumber, user.login, limit, 
count);
+};
diff --git a/.github/workflows/pr_limit/comment.md 
b/.github/workflows/pr_limit/comment.md
new file mode 100644
index 0000000000..cd6f76d76c
--- /dev/null
+++ b/.github/workflows/pr_limit/comment.md
@@ -0,0 +1,31 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+
+Thanks for opening a pull request!
+
+**This pull request has been automatically closed because you currently have 
${OPEN_COUNT} open pull requests, which is more than the limit of ${PR_LIMIT}.**
+
+Due to the increase in pull requests opened by AI bots, and in order to keep 
the review queue manageable, Apache Arrow limits contributors without write 
access to at most ${PR_LIMIT} concurrently open pull requests. This helps make 
sure each pull request gets the attention it needs and that work in progress 
does not go stale.
+
+Once one of [your other open pull 
requests](https://github.com/apache/arrow/pulls/${USERNAME}) has been merged or 
closed, you are welcome to reopen this one.
+
+See also:
+
+  * [Contribution Guidelines - Limit on concurrent pull 
requests](https://arrow.apache.org/docs/dev/developers/bug_reports.html#pr-limit)
+  * [Contribution Guidelines - Contributing 
Overview](https://arrow.apache.org/docs/developers/overview.html)
diff --git a/docs/source/developers/bug_reports.rst 
b/docs/source/developers/bug_reports.rst
index 307261617a..a715211fde 100644
--- a/docs/source/developers/bug_reports.rst
+++ b/docs/source/developers/bug_reports.rst
@@ -237,6 +237,18 @@ still active.
    ``Status: needs champion`` are confirmed-wanted enhancements that need
    a contributor. See :ref:`finding-issues` for more.
 
+.. _pr-limit:
+
+Limit on concurrent pull requests
++++++++++++++++++++++++++++++++++
+
+Due to the increase in pull requests opened by AI bots, and in order to keep
+the review queue manageable, contributors without write access to the
+repository may have at most **3 pull requests open at the same time**.
+A pull request opened beyond that limit is automatically closed by a GitHub
+Actions workflow, with a comment explaining why. Once one of your other pull
+requests has been merged or closed, you can reopen it.
+
 .. _issue-assignment:
 
 Issue assignment

Reply via email to