Skip to content

Commit cc5165a

Browse files
committed
ci: added workflow to check if issue is linked with pr
1 parent a3bd9a2 commit cc5165a

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
name: Check Linked Issue
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited, synchronize, reopened]
6+
7+
jobs:
8+
check-linked-issue:
9+
runs-on: ubuntu-latest
10+
name: Ensure PR has linked issue
11+
12+
steps:
13+
- name: Check for linked issue
14+
uses: actions/github-script@v7
15+
with:
16+
script: |
17+
const { data: pullRequest } = await github.rest.pulls.get({
18+
owner: context.repo.owner,
19+
repo: context.repo.repo,
20+
pull_number: context.issue.number,
21+
});
22+
23+
// Check PR body for issue references
24+
const prBody = pullRequest.body || '';
25+
const prTitle = pullRequest.title || '';
26+
27+
// Common patterns for linking issues
28+
const issuePatterns = [
29+
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi,
30+
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:https?:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/)(\d+)/gi,
31+
/#(\d+)/g,
32+
/(?:issue|issues)\s+#?(\d+)/gi
33+
];
34+
35+
let hasLinkedIssue = false;
36+
let linkedIssues = new Set(); // Use Set to avoid duplicates
37+
38+
// Check PR body and title for issue references
39+
const textToCheck = `${prTitle} ${prBody}`;
40+
41+
for (const pattern of issuePatterns) {
42+
const matches = textToCheck.matchAll(pattern);
43+
for (const match of matches) {
44+
const issueNumber = match[1];
45+
if (issueNumber) {
46+
linkedIssues.add(issueNumber);
47+
hasLinkedIssue = true;
48+
}
49+
}
50+
}
51+
52+
// Check if PR is currently linked to issues via GitHub's API
53+
try {
54+
// Get the PR's linked issues using GraphQL API for more accurate results
55+
const query = `
56+
query($owner: String!, $repo: String!, $number: Int!) {
57+
repository(owner: $owner, name: $repo) {
58+
pullRequest(number: $number) {
59+
closingIssuesReferences(first: 10) {
60+
nodes {
61+
number
62+
title
63+
}
64+
}
65+
}
66+
}
67+
}
68+
`;
69+
70+
const variables = {
71+
owner: context.repo.owner,
72+
repo: context.repo.repo,
73+
number: context.issue.number
74+
};
75+
76+
const result = await github.graphql(query, variables);
77+
const closingIssues = result.repository.pullRequest.closingIssuesReferences.nodes;
78+
79+
if (closingIssues && closingIssues.length > 0) {
80+
hasLinkedIssue = true;
81+
const issueNumbers = closingIssues.map(issue => issue.number);
82+
issueNumbers.forEach(num => linkedIssues.add(num.toString()));
83+
console.log(`Found ${closingIssues.length} linked issue(s) via GitHub API: ${issueNumbers.join(', ')}`);
84+
}
85+
} catch (error) {
86+
console.log('Could not fetch linked issues via GraphQL:', error.message);
87+
88+
// Fallback: Check timeline events but with better logic for current state
89+
try {
90+
const { data: timelineEvents } = await github.rest.issues.listEventsForTimeline({
91+
owner: context.repo.owner,
92+
repo: context.repo.repo,
93+
issue_number: context.issue.number,
94+
});
95+
96+
// Track connected and disconnected events to find current state
97+
const connectionEvents = timelineEvents.filter(event =>
98+
event.event === 'connected' || event.event === 'disconnected'
99+
).sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
100+
101+
// Check the current connection state by looking at the latest events per issue
102+
const currentConnections = new Map();
103+
104+
for (const event of connectionEvents) {
105+
if (event.source && event.source.issue) {
106+
const issueId = event.source.issue.id;
107+
if (event.event === 'connected') {
108+
currentConnections.set(issueId, true);
109+
} else if (event.event === 'disconnected') {
110+
currentConnections.set(issueId, false);
111+
}
112+
}
113+
}
114+
115+
// Check if any issues are currently connected
116+
const hasCurrentConnections = Array.from(currentConnections.values()).some(connected => connected);
117+
118+
if (hasCurrentConnections) {
119+
hasLinkedIssue = true;
120+
console.log('Found currently linked issues via timeline events fallback');
121+
}
122+
} catch (timelineError) {
123+
console.log('Could not fetch timeline events:', timelineError.message);
124+
}
125+
}
126+
127+
if (!hasLinkedIssue) {
128+
core.setFailed(`❌ This pull request must be linked to an issue.
129+
130+
Please link this PR to an issue by:
131+
1. Adding "Fixes #<issue-number>" or "Closes #<issue-number>" to the PR description
132+
2. Using GitHub's UI to link the PR to an existing issue
133+
3. Referencing the issue number with #<issue-number> in the PR title or description
134+
135+
Example: "Fixes #123" or "Closes #456"`);
136+
} else {
137+
const issueList = Array.from(linkedIssues).join(', ');
138+
console.log(`✅ PR is linked to issue(s): ${issueList}`);
139+
core.notice(`✅ Pull request is properly linked to issue(s): ${issueList}`);
140+
}
141+
142+
- name: Add comment if no linked issue
143+
if: failure()
144+
uses: actions/github-script@v7
145+
with:
146+
script: |
147+
github.rest.issues.createComment({
148+
issue_number: context.issue.number,
149+
owner: context.repo.owner,
150+
repo: context.repo.repo,
151+
body: `## ❌ Missing Linked Issue
152+
153+
This pull request needs to be linked to at least one issue before it can be merged.
154+
155+
### How to link an issue:
156+
157+
1. **Add keywords to PR description:**
158+
- \`Fixes #123\`
159+
- \`Closes #456\`
160+
- \`Resolves #789\`
161+
- \`Fixes #123, #456, and #789\` (multiple issues)
162+
163+
2. **Reference issue in PR title or description:**
164+
- \`#123\`
165+
- \`Issue #456\`
166+
- \`Addresses #123 and #456\` (multiple issues)
167+
168+
3. **Use GitHub's UI:**
169+
- Go to the "Development" section in the right sidebar
170+
- Click "Link an issue"
171+
- Select the relevant issue(s)
172+
173+
💡 **Tip:** You can link multiple issues using any combination of the above methods!
174+
175+
Once you've linked at least one issue, this check will automatically pass! 🚀`
176+
})

0 commit comments

Comments
 (0)