176 lines
6.8 KiB
YAML
176 lines
6.8 KiB
YAML
name: PR Requirements Check
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, reopened, edited, synchronize]
|
|
|
|
jobs:
|
|
check-requirements:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
pull-requests: write
|
|
issues: write
|
|
|
|
steps:
|
|
- name: Check PR has linked issue with assignee
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const pr = context.payload.pull_request;
|
|
const prNumber = pr.number;
|
|
const prBody = pr.body || '';
|
|
const prTitle = pr.title || '';
|
|
const prLabels = (pr.labels || []).map(l => l.name);
|
|
|
|
// Allow micro-fix and documentation PRs without a linked issue
|
|
const isMicroFix = prLabels.includes('micro-fix') || /micro-fix/i.test(prTitle);
|
|
const isDocumentation = prLabels.includes('documentation') || /\bdocs?\b/i.test(prTitle);
|
|
if (isMicroFix || isDocumentation) {
|
|
const reason = isMicroFix ? 'micro-fix' : 'documentation';
|
|
console.log(`PR #${prNumber} is a ${reason}, skipping issue requirement.`);
|
|
return;
|
|
}
|
|
|
|
// Extract issue numbers from body and title
|
|
// Matches: fixes #123, closes #123, resolves #123, or plain #123
|
|
const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)?\s*#(\d+)/gi;
|
|
|
|
const allText = `${prTitle} ${prBody}`;
|
|
const matches = [...allText.matchAll(issuePattern)];
|
|
const issueNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
|
|
|
|
console.log(`PR #${prNumber}:`);
|
|
console.log(` Found issue references: ${issueNumbers.length > 0 ? issueNumbers.join(', ') : 'none'}`);
|
|
|
|
if (issueNumbers.length === 0) {
|
|
const message = `## PR Closed - Requirements Not Met
|
|
|
|
This PR has been automatically closed because it doesn't meet the requirements.
|
|
|
|
**Missing:** No linked issue found.
|
|
|
|
**To fix:**
|
|
1. Create or find an existing issue for this work
|
|
2. Assign yourself to the issue
|
|
3. Re-open this PR and add \`Fixes #123\` in the description
|
|
|
|
**Exception:** To bypass this requirement, you can:
|
|
- Add the \`micro-fix\` label or include \`micro-fix\` in your PR title for trivial fixes
|
|
- Add the \`documentation\` label or include \`doc\`/\`docs\` in your PR title for documentation changes
|
|
|
|
**Why is this required?** See #472 for details.`;
|
|
|
|
const comments = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
});
|
|
|
|
const botComment = comments.data.find(
|
|
(c) => c.user.type === 'Bot' && c.body.includes('PR Closed - Requirements Not Met')
|
|
);
|
|
|
|
if (!botComment) {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
body: message,
|
|
});
|
|
}
|
|
|
|
await github.rest.pulls.update({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
state: 'closed',
|
|
});
|
|
|
|
core.setFailed('PR must reference an issue');
|
|
return;
|
|
}
|
|
|
|
// Check if any linked issue has the PR author as assignee
|
|
const prAuthor = pr.user.login;
|
|
let issueWithAuthorAssigned = null;
|
|
let issuesWithoutAuthor = [];
|
|
|
|
for (const issueNum of issueNumbers) {
|
|
try {
|
|
const { data: issue } = await github.rest.issues.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNum,
|
|
});
|
|
|
|
const assigneeLogins = (issue.assignees || []).map(a => a.login);
|
|
if (assigneeLogins.includes(prAuthor)) {
|
|
issueWithAuthorAssigned = issueNum;
|
|
console.log(` Issue #${issueNum} has PR author ${prAuthor} as assignee`);
|
|
break;
|
|
} else {
|
|
issuesWithoutAuthor.push({
|
|
number: issueNum,
|
|
assignees: assigneeLogins
|
|
});
|
|
console.log(` Issue #${issueNum} assignees: ${assigneeLogins.length > 0 ? assigneeLogins.join(', ') : 'none'} (PR author: ${prAuthor})`);
|
|
}
|
|
} catch (error) {
|
|
console.log(` Issue #${issueNum} not found or inaccessible`);
|
|
}
|
|
}
|
|
|
|
if (!issueWithAuthorAssigned) {
|
|
const issueList = issuesWithoutAuthor.map(i =>
|
|
`#${i.number} (assignees: ${i.assignees.length > 0 ? i.assignees.join(', ') : 'none'})`
|
|
).join(', ');
|
|
|
|
const message = `## PR Closed - Requirements Not Met
|
|
|
|
This PR has been automatically closed because it doesn't meet the requirements.
|
|
|
|
**PR Author:** @${prAuthor}
|
|
**Found issues:** ${issueList}
|
|
**Problem:** The PR author must be assigned to the linked issue.
|
|
|
|
**To fix:**
|
|
1. Assign yourself (@${prAuthor}) to one of the linked issues
|
|
2. Re-open this PR
|
|
|
|
**Exception:** To bypass this requirement, you can:
|
|
- Add the \`micro-fix\` label or include \`micro-fix\` in your PR title for trivial fixes
|
|
- Add the \`documentation\` label or include \`doc\`/\`docs\` in your PR title for documentation changes
|
|
|
|
**Why is this required?** See #472 for details.`;
|
|
|
|
const comments = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
});
|
|
|
|
const botComment = comments.data.find(
|
|
(c) => c.user.type === 'Bot' && c.body.includes('PR Closed - Requirements Not Met')
|
|
);
|
|
|
|
if (!botComment) {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
body: message,
|
|
});
|
|
}
|
|
|
|
await github.rest.pulls.update({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
state: 'closed',
|
|
});
|
|
|
|
core.setFailed('PR author must be assigned to the linked issue');
|
|
} else {
|
|
console.log(`PR requirements met! Issue #${issueWithAuthorAssigned} has ${prAuthor} as assignee.`);
|
|
}
|