ci: auto-reopen external PRs after issue link requirement is satisfied (#35699)

Auto-reopen external PRs that were closed by the `require_issue_link`
workflow once the author fixes their PR description. Previously, the
workflow closed non-compliant PRs but required a maintainer to manually
reopen them — creating unnecessary back-and-forth when the contributor
just needed to add an issue link or get assigned.

## Changes
- Add reopen logic to the success path in `require_issue_link.yml`:
after removing the `missing-issue-link` label, call `pulls.update({
state: 'open' })` if the PR is closed *and* still carries the
`missing-issue-link` label — gating on the label ensures only
workflow-closed PRs are reopened, not PRs closed manually by maintainers
- Update the bot's auto-close comments to tell contributors the PR will
reopen automatically once they fix the issue, instead of directing them
to ask a maintainer
This commit is contained in:
Mason Daugherty
2026-03-09 15:28:50 -04:00
committed by GitHub
parent 1a39508469
commit ee64597c1b
2 changed files with 23 additions and 20 deletions

View File

@@ -7,6 +7,8 @@
# - Also re-checks on PR edits/reopens for PRs that already have the label.
# - Validates the PR author is an assignee on at least one linked issue.
# - Adds a "missing-issue-link" label on failure; removes it on pass.
# - Automatically reopens PRs that were closed by this workflow once the
# check passes (e.g. author edits the body to add a valid issue link).
# - Posts a comment explaining the requirement on failure.
# - Deduplicates comments via an HTML marker so re-runs don't spam.
#
@@ -92,7 +94,7 @@ jobs:
owner, repo, issue_number: prNumber, labels: ['missing-issue-link'],
});
- name: Remove missing-issue-link label
- name: Remove missing-issue-link label and reopen PR
if: steps.check-link.outputs.has-link == 'true' && steps.check-link.outputs.is-assigned == 'true'
uses: actions/github-script@v8
with:
@@ -107,6 +109,18 @@ jobs:
if (error.status !== 404) throw error;
}
// Reopen PR only if it was previously closed by this workflow
const labels = context.payload.pull_request.labels.map(l => l.name);
if (context.payload.pull_request.state === 'closed' && labels.includes('missing-issue-link')) {
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
state: 'open',
});
console.log(`Reopened PR #${prNumber}`);
}
- name: Post comment, close PR, and fail
if: steps.check-link.outputs.has-link != 'true' || steps.check-link.outputs.is-assigned != 'true'
uses: actions/github-script@v8
@@ -127,8 +141,7 @@ jobs:
'All external contributions must reference an approved issue or discussion. Please:',
'1. Find or [open an issue](https://github.com/' + owner + '/' + repo + '/issues/new/choose) describing the change',
'2. Wait for a maintainer to approve and assign you',
'3. Add `Fixes #<issue_number>`, `Closes #<issue_number>`, or `Resolves #<issue_number>` to your PR description',
'4. Ask a maintainer to reopen this PR',
'3. Add `Fixes #<issue_number>`, `Closes #<issue_number>`, or `Resolves #<issue_number>` to your PR description and the PR will be reopened automatically',
];
} else {
lines = [
@@ -137,7 +150,7 @@ jobs:
'',
'External contributors must be assigned to an issue before opening a PR for it. Please:',
'1. Comment on the linked issue to request assignment from a maintainer',
'2. Once assigned, ask a maintainer to reopen this PR',
'2. Once assigned, edit your PR description and the PR will be reopened automatically',
];
}

View File

@@ -16,8 +16,7 @@
# Without it, the workflow will fail.
#
# Contributor tier thresholds:
# - trusted-contributor: >= 4 merged PRs
# - experienced-contributor: >= 10 merged PRs
# - trusted-contributor: >= 5 merged PRs
name: Tag External Contributions
@@ -184,8 +183,7 @@ jobs:
const author = item.user.login;
const issueNumber = item.number;
const TRUSTED_THRESHOLD = 4;
const EXPERIENCED_THRESHOLD = 10;
const TRUSTED_THRESHOLD = 5;
const mergedQuery = `repo:${owner}/${repo} is:pr is:merged author:"${author}"`;
let mergedCount = 0;
@@ -201,12 +199,7 @@ jobs:
return;
}
let label = null;
if (mergedCount >= EXPERIENCED_THRESHOLD) {
label = 'experienced-contributor';
} else if (mergedCount >= TRUSTED_THRESHOLD) {
label = 'trusted-contributor';
}
const label = mergedCount >= TRUSTED_THRESHOLD ? 'trusted-contributor' : null;
if (label) {
await github.rest.issues.addLabels({
@@ -244,12 +237,11 @@ jobs:
const maxItems = parseInt('${{ inputs.max_items }}') || 100;
const backfillType = '${{ inputs.backfill_type }}';
const TRUSTED_THRESHOLD = 4;
const EXPERIENCED_THRESHOLD = 10;
const TRUSTED_THRESHOLD = 5;
const LABEL_COLOR = 'b76e79';
const sizeLabels = ['size: XS', 'size: S', 'size: M', 'size: L', 'size: XL'];
const tierLabels = ['trusted-contributor', 'experienced-contributor'];
const tierLabels = ['trusted-contributor'];
// Ensure tier and size labels exist
for (const name of [...tierLabels, ...sizeLabels]) {
@@ -303,9 +295,7 @@ jobs:
}
function getTierLabel(mergedCount) {
if (mergedCount >= EXPERIENCED_THRESHOLD) return 'experienced-contributor';
if (mergedCount >= TRUSTED_THRESHOLD) return 'trusted-contributor';
return null;
return mergedCount >= TRUSTED_THRESHOLD ? 'trusted-contributor' : null;
}
function getSizeLabel(totalChangedLines) {