forked from 1jehuang/jcode
-
Notifications
You must be signed in to change notification settings - Fork 3
111 lines (98 loc) · 4.36 KB
/
Copy pathrequire-issue.yml
File metadata and controls
111 lines (98 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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}. ✅`);