a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
112 lines
4.4 KiB
YAML
112 lines
4.4 KiB
YAML
name: Require Linked Issue
|
|
|
|
# Ensure every pull request is linked to a REAL GitHub issue in this repo.
|
|
# A PR passes when it links an issue via any of:
|
|
# * GitHub's "Development" sidebar (closing issue reference), or
|
|
# * a closing keyword in the body/title (e.g. "Closes #123"), or
|
|
# * a plain mention (#123 / owner/repo#123) or full issue URL.
|
|
# Any referenced number is verified against the API: it must resolve to an
|
|
# existing issue (not a pull request) in this repository.
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, edited, reopened, synchronize, ready_for_review]
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
issues: read
|
|
|
|
concurrency:
|
|
group: require-issue-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
require-issue:
|
|
name: Require Linked Issue
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Check for a linked, existing issue
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const pr = context.payload.pull_request;
|
|
if (!pr) {
|
|
core.info('No pull_request payload; skipping.');
|
|
return;
|
|
}
|
|
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const text = `${pr.title || ''}\n\n${pr.body || ''}`;
|
|
|
|
// Collect candidate issue numbers that belong to THIS repo.
|
|
const candidates = new Set();
|
|
|
|
// Full issue URLs: https://github.com/owner/repo/issues/123
|
|
const urlRe = /https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/issues\/(\d+)/gi;
|
|
for (const m of text.matchAll(urlRe)) {
|
|
if (m[1].toLowerCase() === owner.toLowerCase() &&
|
|
m[2].toLowerCase() === repo.toLowerCase()) {
|
|
candidates.add(Number(m[3]));
|
|
}
|
|
}
|
|
|
|
// owner/repo#123 or bare #123 (bare assumed to be this repo).
|
|
const refRe = /(?:([\w.-]+)\/([\w.-]+))?#(\d+)/g;
|
|
for (const m of text.matchAll(refRe)) {
|
|
const refOwner = m[1];
|
|
const refRepo = m[2];
|
|
const num = Number(m[3]);
|
|
if (!refOwner && !refRepo) {
|
|
candidates.add(num); // bare #123 -> this repo
|
|
} else if (refOwner?.toLowerCase() === owner.toLowerCase() &&
|
|
refRepo?.toLowerCase() === repo.toLowerCase()) {
|
|
candidates.add(num);
|
|
}
|
|
}
|
|
|
|
// Issues linked via the Development sidebar (closing references).
|
|
try {
|
|
const query = `query($owner:String!, $repo:String!, $number:Int!) {
|
|
repository(owner:$owner, name:$repo) {
|
|
pullRequest(number:$number) {
|
|
closingIssuesReferences(first: 10) { nodes { number } }
|
|
}
|
|
}
|
|
}`;
|
|
const result = await github.graphql(query, { owner, repo, number: pr.number });
|
|
const nodes = result?.repository?.pullRequest?.closingIssuesReferences?.nodes || [];
|
|
for (const n of nodes) candidates.add(Number(n.number));
|
|
} catch (err) {
|
|
core.warning(`Could not query closing issue references: ${err}`);
|
|
}
|
|
|
|
// Verify at least one candidate is a real issue (not a PR, not this PR).
|
|
let linkedIssue = null;
|
|
for (const num of candidates) {
|
|
if (num === pr.number) continue;
|
|
try {
|
|
const { data } = await github.rest.issues.get({ owner, repo, issue_number: num });
|
|
if (!data.pull_request) { // issues have no pull_request field
|
|
linkedIssue = num;
|
|
break;
|
|
}
|
|
} catch (err) {
|
|
// 404 -> number doesn't exist; ignore and keep checking.
|
|
}
|
|
}
|
|
|
|
if (!linkedIssue) {
|
|
core.setFailed(
|
|
'This PR must be linked to an existing GitHub issue in this repository. ' +
|
|
'Add a closing keyword (e.g. "Closes #123") to the PR description, link an ' +
|
|
'issue via the Development sidebar, or reference an existing issue number ' +
|
|
'(e.g. "#123"). If no issue exists yet, please open one first.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
core.info(`Linked to existing issue #${linkedIssue}. ✅`);
|