mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 22:14:18 +02:00
ci: also require the linked issue be assigned to the PR author; update CONTRIBUTING
This commit is contained in:
parent
03e062126a
commit
a082415ce7
2 changed files with 88 additions and 27 deletions
111
.github/workflows/require-issue-link.yml
vendored
111
.github/workflows/require-issue-link.yml
vendored
|
|
@ -1,7 +1,8 @@
|
|||
# Require external PRs to reference an issue with an auto-close keyword
|
||||
# (e.g. "Fixes #123"). When missing, the PR is labeled "missing-issue-link",
|
||||
# commented on, and closed. CONTRIBUTING.md requires every PR to address a
|
||||
# tracked issue; this enforces that for outside contributors.
|
||||
# (e.g. "Fixes #123") AND have the PR author assigned to that issue.
|
||||
# Otherwise the PR is labeled "missing-issue-link", commented on, and
|
||||
# closed. CONTRIBUTING.md requires external contributors to be assigned to
|
||||
# an issue before opening a PR; this enforces that.
|
||||
#
|
||||
# Adapted from langchain-ai/langchain's require_issue_link.yml. Differences:
|
||||
# - Self-contained: it does NOT depend on a separate labeler workflow
|
||||
|
|
@ -13,9 +14,6 @@
|
|||
# private appears as CONTRIBUTOR/NONE, so gating on it would wrongly
|
||||
# enforce against private-member maintainers. getCollaboratorPermission
|
||||
# reflects effective write access regardless of membership visibility.
|
||||
# - No assignee requirement. fastmcp's CONTRIBUTING.md states that
|
||||
# referencing an issue "isn't a permission step" — only the link is
|
||||
# enforced, not issue assignment.
|
||||
# - Single github-script step (the upstream version is split across four,
|
||||
# forcing the label/comment/reopen helpers to be duplicated per scope).
|
||||
#
|
||||
|
|
@ -227,12 +225,10 @@ jobs:
|
|||
return;
|
||||
}
|
||||
// Only triage/admin can manage labels, so a non-write actor
|
||||
// reaching here is rare (triage role). Re-add and fall through
|
||||
// to enforcement. addLabels fires a "labeled" event, which the
|
||||
// job gate ignores — no re-trigger loop.
|
||||
console.log(`Non-maintainer ${sender} removed ${LABEL} — re-enforcing`);
|
||||
await closeForMissingLink();
|
||||
return;
|
||||
// reaching here is rare (triage role). Fall through to the
|
||||
// normal check, which recomputes link + assignment and
|
||||
// re-enforces with the correct message if still failing.
|
||||
console.log(`Non-maintainer ${sender} removed ${LABEL} — re-checking`);
|
||||
}
|
||||
|
||||
// ── Maintainer override: reopened a PR we had closed ───────────
|
||||
|
|
@ -261,29 +257,92 @@ jobs:
|
|||
const pattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s*#(\d+)/gi;
|
||||
const matches = [...body.matchAll(pattern)];
|
||||
|
||||
if (matches.length > 0) {
|
||||
const issues = [...new Set(matches.map(m => `#${m[1]}`))].join(', ');
|
||||
console.log(`Found issue link(s): ${issues} — clearing any prior enforcement`);
|
||||
await clearEnforcement();
|
||||
if (matches.length === 0) {
|
||||
console.log('No issue link found in PR body');
|
||||
await enforceFailure('no-link');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('No issue link found in PR body');
|
||||
await closeForMissingLink();
|
||||
// The author must be assigned to at least one linked issue.
|
||||
// CONTRIBUTING.md requires external contributors to be assigned
|
||||
// before opening a PR (so maintainers can deconflict / steer
|
||||
// approach first).
|
||||
const MAX_ISSUES = 5;
|
||||
const allNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
|
||||
const numbers = allNumbers.slice(0, MAX_ISSUES);
|
||||
if (allNumbers.length > MAX_ISSUES) {
|
||||
core.warning(`PR references ${allNumbers.length} issues — checking only the first ${MAX_ISSUES}`);
|
||||
}
|
||||
|
||||
const prAuthor = pr.user.login.toLowerCase();
|
||||
let sawRealIssue = false;
|
||||
let assignedToAny = false;
|
||||
for (const num of numbers) {
|
||||
let issue;
|
||||
try {
|
||||
({ data: issue } = await github.rest.issues.get({
|
||||
owner, repo, issue_number: num,
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log(`#${num} does not exist — ignoring`);
|
||||
continue;
|
||||
}
|
||||
// Same safe-direction rule as hasWriteAccess: a transient
|
||||
// error must not be read as "not assigned" and close the PR.
|
||||
throw new Error(`Cannot fetch issue #${num} (HTTP ${e.status ?? 'unknown'}): ${e.message}`);
|
||||
}
|
||||
sawRealIssue = true;
|
||||
const assignees = (issue.assignees || []).map(a => a.login.toLowerCase());
|
||||
if (assignees.includes(prAuthor)) {
|
||||
console.log(`PR author ${pr.user.login} is assigned to #${num}`);
|
||||
assignedToAny = true;
|
||||
break;
|
||||
}
|
||||
console.log(`PR author ${pr.user.login} is NOT assigned to #${num} (assignees: ${assignees.join(', ') || 'none'})`);
|
||||
}
|
||||
|
||||
if (!sawRealIssue) {
|
||||
console.log('Referenced issue(s) do not exist');
|
||||
await enforceFailure('no-link');
|
||||
return;
|
||||
}
|
||||
if (!assignedToAny) {
|
||||
await enforceFailure('not-assigned');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Linked and assigned — clearing any prior enforcement');
|
||||
await clearEnforcement();
|
||||
|
||||
// ── Label, comment, close, and fail ────────────────────────────
|
||||
async function closeForMissingLink() {
|
||||
// `kind`: 'no-link' (no valid issue reference) or 'not-assigned'
|
||||
// (referenced an issue, but the author isn't assigned to it).
|
||||
async function enforceFailure(kind) {
|
||||
await addLabel();
|
||||
|
||||
const intro = kind === 'no-link'
|
||||
? '**This PR has been automatically closed** because its description does not reference a tracked issue.'
|
||||
: '**This PR has been automatically closed** because you are not assigned to the issue it references.';
|
||||
const steps = kind === 'no-link'
|
||||
? [
|
||||
`1. Find or [open an issue](https://github.com/${owner}/${repo}/issues/new/choose) describing the change.`,
|
||||
'2. Comment on the issue to ask a maintainer to assign it to you.',
|
||||
'3. Add `Fixes #<issue>`, `Closes #<issue>`, or `Resolves #<issue>` to the PR description.',
|
||||
'4. Once you are assigned and the link is present, the PR reopens automatically.',
|
||||
]
|
||||
: [
|
||||
'1. Comment on the linked issue to ask a maintainer to assign it to you.',
|
||||
'2. Once a maintainer assigns you, the PR reopens automatically.',
|
||||
];
|
||||
|
||||
const commentBody = [
|
||||
MARKER,
|
||||
'**This PR has been automatically closed** because its description does not reference a tracked issue.',
|
||||
intro,
|
||||
'',
|
||||
`Per [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md), every PR should address a tracked issue. To proceed:`,
|
||||
`Per [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md), an external PR must reference an issue that is assigned to its author. To proceed:`,
|
||||
'',
|
||||
`1. Find or [open an issue](https://github.com/${owner}/${repo}/issues/new/choose) describing the change.`,
|
||||
'2. Add `Fixes #<issue>`, `Closes #<issue>`, or `Resolves #<issue>` to the PR description.',
|
||||
'3. The PR reopens automatically once the description links an issue.',
|
||||
...steps,
|
||||
'',
|
||||
`*Maintainers: reopen this PR or remove the \`${LABEL}\` label to bypass this check.*`,
|
||||
].join('\n');
|
||||
|
|
@ -313,7 +372,9 @@ jobs:
|
|||
|
||||
if (enforce) {
|
||||
core.setFailed(
|
||||
'PR must reference a tracked issue using an auto-close keyword (e.g. "Fixes #123").',
|
||||
kind === 'no-link'
|
||||
? 'PR must reference a tracked issue using an auto-close keyword (e.g. "Fixes #123").'
|
||||
: 'PR author must be assigned to the referenced issue.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue