From 4166311dafcfd354189be1d62e3a6ef4a07b4b49 Mon Sep 17 00:00:00 2001 From: 2^1 <217781969+2hoch1@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:48:40 +0100 Subject: [PATCH] ci: add rate limits for issues and prs (#66265) --- .github/workflows/issue-rate-limit.yml | 91 ++++++++++++++++++++++++++ .github/workflows/pr-rate-limit.yml | 91 ++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 .github/workflows/issue-rate-limit.yml create mode 100644 .github/workflows/pr-rate-limit.yml diff --git a/.github/workflows/issue-rate-limit.yml b/.github/workflows/issue-rate-limit.yml new file mode 100644 index 0000000..f871fa9 --- /dev/null +++ b/.github/workflows/issue-rate-limit.yml @@ -0,0 +1,91 @@ +name: Issue rate limit + +on: + issues: + types: [opened, reopened] + +permissions: + issues: write + contents: read + +jobs: + limit: + runs-on: ubuntu-latest + env: + BLOCK_USER_ENV: ${{ secrets.BLOCK_USER_ENV }} + steps: + - uses: actions/github-script@v7 + with: + script: | + const timeLimitMinutes = 30; + const blockThreshold = 10; + const skipMaintainers = true; + const skipBots = true; + + const author = context.payload.issue.user.login; + const authorType = context.payload.issue.user.type; + const currentIssue = context.payload.issue.number; + const since = new Date(Date.now() - timeLimitMinutes * 60 * 1000).toISOString(); + const blockToken = process.env.BLOCK_USER_ENV || ""; + + if (skipBots && (authorType === "Bot" || author === "github-bot")) { + core.info(`Skipping rate limit for bot account: ${author}`); + return; + } + + const permission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: author + }); + + if (skipMaintainers && ["admin", "maintain", "write"].includes(permission.data.permission)) { + core.info(`Skipping rate limit for maintainer: ${author}`); + return; + } + + const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue author:${author} created:>=${since}`; + const { search } = await github.graphql( + `query($searchQuery: String!) { + search(type: ISSUE, query: $searchQuery, first: 1) { + issueCount + } + }`, + { searchQuery } + ); + + const count = search.issueCount; + + core.info(`Issues in last ${timeLimitMinutes} min by ${author}: ${count}`); + + if (count > 5) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: currentIssue, + body: `🚫 Rate limit reached: Please wait a while, before creating more issues.` + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: currentIssue, + state: "closed" + }); + } + + if (count > blockThreshold) { + if (!blockToken) { + core.warning(`Missing BLOCK_USER_ENV token. Skipping block for ${author}.`); + return; + } + + const { getOctokit } = require("@actions/github"); + const blockClient = getOctokit(blockToken); + + await blockClient.rest.users.block({ + username: author + }); + + core.info(`Blocked user ${author} after ${count} issues in ${timeLimitMinutes} min.`); + } diff --git a/.github/workflows/pr-rate-limit.yml b/.github/workflows/pr-rate-limit.yml new file mode 100644 index 0000000..eaea63d --- /dev/null +++ b/.github/workflows/pr-rate-limit.yml @@ -0,0 +1,91 @@ +name: PR rate limit + +on: + pull_request: + types: [opened, reopened] + +permissions: + pull-requests: write + contents: read + +jobs: + limit: + runs-on: ubuntu-latest + env: + BLOCK_USER_ENV: ${{ secrets.BLOCK_USER_ENV }} + steps: + - uses: actions/github-script@v7 + with: + script: | + const timeLimitMinutes = 30; + const blockThreshold = 10; + const skipMaintainers = true; + const skipBots = true; + + const author = context.payload.pull_request.user.login; + const authorType = context.payload.pull_request.user.type; + const currentPr = context.payload.pull_request.number; + const since = new Date(Date.now() - timeLimitMinutes * 60 * 1000).toISOString(); + const blockToken = process.env.BLOCK_USER_ENV || ""; + + if (skipBots && (authorType === "Bot" || author === "github-bot")) { + core.info(`Skipping rate limit for bot account: ${author}`); + return; + } + + const permission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: author + }); + + if (skipMaintainers && ["admin", "maintain", "write"].includes(permission.data.permission)) { + core.info(`Skipping rate limit for maintainer: ${author}`); + return; + } + + const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:pr author:${author} created:>=${since}`; + const { search } = await github.graphql( + `query($searchQuery: String!) { + search(type: ISSUE, query: $searchQuery, first: 1) { + issueCount + } + }`, + { searchQuery } + ); + + const count = search.issueCount; + + core.info(`PRs in last ${timeLimitMinutes} min by ${author}: ${count}`); + + if (count > 5) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: currentPr, + body: `🚫 Rate limit reached: Please wait a while, before creating more pull requests.` + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: currentPr, + state: "closed" + }); + } + + if (count > blockThreshold) { + if (!blockToken) { + core.warning(`Missing BLOCK_USER_ENV token. Skipping block for ${author}.`); + return; + } + + const { getOctokit } = require("@actions/github"); + const blockClient = getOctokit(blockToken); + + await blockClient.rest.users.block({ + username: author + }); + + core.info(`Blocked user ${author} after ${count} PRs in ${timeLimitMinutes} min.`); + }