From a34005b699b284c64abb5ea0a7f8105c17cb7726 Mon Sep 17 00:00:00 2001 From: Stephanie Anderson Date: Wed, 1 Apr 2026 13:09:06 +0200 Subject: [PATCH 1/2] feat(validate-pr): Skip all checks when a maintainer reopens a PR When a maintainer reopens a previously closed PR, skip all validation (issue reference, maintainer discussion, assignee checks). This allows maintainers to override the action's decision without the PR being immediately closed again. Co-Authored-By: Claude Opus 4.6 (1M context) --- validate-pr/README.md | 2 +- validate-pr/scripts/validate-pr.js | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/validate-pr/README.md b/validate-pr/README.md index 7549c77..e94a5f3 100644 --- a/validate-pr/README.md +++ b/validate-pr/README.md @@ -6,7 +6,7 @@ Validates non-maintainer pull requests against contribution guidelines. **Validates issue references** — Non-maintainer PRs must reference a GitHub issue where the PR author and a maintainer have discussed the approach. PRs that don't meet this requirement are automatically closed with a descriptive comment. -Maintainers (users with `admin` or `maintain` role) are exempt from validation. +Maintainers (users with `admin` or `maintain` role) are exempt from validation. When a maintainer reopens a previously closed PR, all checks are skipped — this allows maintainers to override the action's decision. ## Usage diff --git a/validate-pr/scripts/validate-pr.js b/validate-pr/scripts/validate-pr.js index f7b1e77..c17528d 100644 --- a/validate-pr/scripts/validate-pr.js +++ b/validate-pr/scripts/validate-pr.js @@ -17,7 +17,18 @@ module.exports = async ({ github, context, core }) => { const prAuthor = pullRequest.user.login; const contributingUrl = `https://github.com/${repo.owner}/${repo.repo}/blob/${context.payload.repository.default_branch}/CONTRIBUTING.md`; - // --- Step 0: Skip allowed bots and service accounts --- + // --- Step 0a: Skip if a maintainer reopened the PR --- + if (context.payload.action === 'reopened') { + const sender = context.payload.sender.login; + const senderIsMaintainer = await isMaintainer(repo.owner, repo.repo, sender); + if (senderIsMaintainer) { + core.info(`PR reopened by maintainer ${sender}. Skipping all checks.`); + core.setOutput('skipped', 'true'); + return; + } + } + + // --- Step 0b: Skip allowed bots and service accounts --- const ALLOWED_BOTS = [ 'codecov-ai[bot]', 'dependabot[bot]', From de842fa5182a6451bc193e7f0f47872522295acc Mon Sep 17 00:00:00 2001 From: Stephanie Anderson Date: Wed, 1 Apr 2026 13:16:05 +0200 Subject: [PATCH 2/2] ref(validate-pr): Move maintainer-reopen check after bot check Avoids an unnecessary GitHub API call when the PR author is a bot. Also renumbers steps for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) --- validate-pr/scripts/validate-pr.js | 34 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/validate-pr/scripts/validate-pr.js b/validate-pr/scripts/validate-pr.js index c17528d..0aba25f 100644 --- a/validate-pr/scripts/validate-pr.js +++ b/validate-pr/scripts/validate-pr.js @@ -17,18 +17,7 @@ module.exports = async ({ github, context, core }) => { const prAuthor = pullRequest.user.login; const contributingUrl = `https://github.com/${repo.owner}/${repo.repo}/blob/${context.payload.repository.default_branch}/CONTRIBUTING.md`; - // --- Step 0a: Skip if a maintainer reopened the PR --- - if (context.payload.action === 'reopened') { - const sender = context.payload.sender.login; - const senderIsMaintainer = await isMaintainer(repo.owner, repo.repo, sender); - if (senderIsMaintainer) { - core.info(`PR reopened by maintainer ${sender}. Skipping all checks.`); - core.setOutput('skipped', 'true'); - return; - } - } - - // --- Step 0b: Skip allowed bots and service accounts --- + // --- Step 0: Skip allowed bots and service accounts --- const ALLOWED_BOTS = [ 'codecov-ai[bot]', 'dependabot[bot]', @@ -67,7 +56,18 @@ module.exports = async ({ github, context, core }) => { return result; } - // --- Step 1: Check if PR author is a maintainer (admin or maintain role) --- + // --- Step 1: Skip if a maintainer reopened the PR --- + if (context.payload.action === 'reopened') { + const sender = context.payload.sender.login; + const senderIsMaintainer = await isMaintainer(repo.owner, repo.repo, sender); + if (senderIsMaintainer) { + core.info(`PR reopened by maintainer ${sender}. Skipping all checks.`); + core.setOutput('skipped', 'true'); + return; + } + } + + // --- Step 2: Check if PR author is a maintainer (admin or maintain role) --- const authorIsMaintainer = await isMaintainer(repo.owner, repo.repo, prAuthor); if (authorIsMaintainer) { core.info(`PR author ${prAuthor} has admin/maintain access. Skipping.`); @@ -76,7 +76,7 @@ module.exports = async ({ github, context, core }) => { } core.info(`PR author ${prAuthor} is not a maintainer.`); - // --- Step 2: Parse issue references from PR body --- + // --- Step 3: Parse issue references from PR body --- const body = pullRequest.body || ''; // Match all issue reference formats: @@ -141,7 +141,7 @@ module.exports = async ({ github, context, core }) => { core.setOutput('was-closed', 'true'); } - // --- Step 3: No issue references --- + // --- Step 4: No issue references --- if (issueRefs.length === 0) { core.info('No issue references found. Closing PR.'); await closePR([ @@ -157,7 +157,7 @@ module.exports = async ({ github, context, core }) => { return; } - // --- Step 4: Validate each referenced issue --- + // --- Step 5: Validate each referenced issue --- // A PR is valid if ANY referenced issue passes all checks. let hasAssigneeConflict = false; let hasNoDiscussion = false; @@ -232,7 +232,7 @@ module.exports = async ({ github, context, core }) => { hasNoDiscussion = true; } - // --- Step 5: No valid issue found — close with the most relevant reason --- + // --- Step 6: No valid issue found — close with the most relevant reason --- if (hasAssigneeConflict) { core.info('Closing PR: referenced issue is assigned to someone else.'); await closePR([