// Copyright (c) Microsoft. All rights reserved. function getPullRequest(context) { const pullRequest = context.payload.pull_request; if (!pullRequest?.number || !pullRequest.user?.login) { throw new Error('This script must be run from a pull_request_target event.'); } return { author: pullRequest.user.login, authorType: pullRequest.user.type, labels: pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? [], number: pullRequest.number, }; } async function ensureLabel({ github, owner, repo, labelName }) { try { await github.rest.issues.getLabel({ owner, repo, name: labelName, }); } catch (error) { if (error.status !== 404) { throw error; } try { await github.rest.issues.createLabel({ owner, repo, name: labelName, color: 'd93f0b', description: 'Community author has exceeded the open pull request limit.', }); } catch (createError) { if (createError.status !== 422) { throw createError; } } } } function hasLabel(labels, labelName) { if (!labelName) { return false; } return labels.some((label) => label.toLowerCase() === labelName.toLowerCase()); } function isDependabotAuthor({ author, authorType }) { return authorType === 'Bot' && author.toLowerCase() === 'dependabot[bot]'; } function buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount }) { return [ `Thank you for your contribution, @${author}.`, '', `To keep the review queue manageable, we currently limit community contributors to ${maxOpenPrs} ` + `open pull requests at a time. This PR would put you at ${openPrCount} open pull requests, ` + 'so we are closing it automatically.', '', 'Please focus on getting your existing PRs reviewed, merged, or closed before opening another one. ' + `If a maintainer asked you to open this PR, they can apply the \`${exemptLabelName}\` label and reopen it.`, ].join('\n'); } async function getOpenPrCount({ github, owner, repo, author, pullRequestNumber }) { const openPullRequests = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100, }); const authorOpenPullRequestNumbers = openPullRequests .filter((pullRequest) => pullRequest.user?.login === author) .map((pullRequest) => pullRequest.number); const currentPrIsOpen = authorOpenPullRequestNumbers.includes(pullRequestNumber); const existingOpenPrCount = currentPrIsOpen ? authorOpenPullRequestNumbers.length - 1 : authorOpenPullRequestNumbers.length; return existingOpenPrCount + 1; } async function enforcePrLimit({ github, context, core, exemptLabelName, maxOpenPrs, labelName }) { const { owner, repo } = context.repo; const { author, authorType, labels, number } = getPullRequest(context); if (isDependabotAuthor({ author, authorType })) { core.info(`Author ${author} is Dependabot; skipping open PR limit enforcement.`); return { author, closed: false, dependabotExempt: true, openPrCount: null, }; } if (hasLabel(labels, exemptLabelName)) { core.info(`PR #${number} has the ${exemptLabelName} label; skipping open PR limit enforcement.`); return { author, closed: false, exempt: true, openPrCount: null, }; } const openPrCount = await getOpenPrCount({ github, owner, repo, author, pullRequestNumber: number, }); if (openPrCount <= maxOpenPrs) { core.info( `${author} has ${openPrCount} open pull request(s), which is within the limit of ${maxOpenPrs}.`, ); return { author, closed: false, openPrCount, }; } await ensureLabel({ github, owner, repo, labelName, }); await github.rest.issues.addLabels({ owner, repo, issue_number: number, labels: [labelName], }); await github.rest.issues.createComment({ owner, repo, issue_number: number, body: buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount, }), }); await github.rest.pulls.update({ owner, repo, pull_number: number, state: 'closed', }); core.info( `${author} has ${openPrCount} open pull request(s), which exceeds the limit of ${maxOpenPrs}. ` + `Closed PR #${number}.`, ); return { author, closed: true, openPrCount, }; } module.exports = { buildLimitMessage, enforcePrLimit, getOpenPrCount, };