ci: added workflow to check if issue is linked with pr #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Check Linked Issue | |
on: | |
pull_request: | |
types: [opened, edited, synchronize, reopened] | |
jobs: | |
check-linked-issue: | |
runs-on: ubuntu-latest | |
name: Ensure PR has linked issue | |
steps: | |
- name: Check for linked issue | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const { data: pullRequest } = await github.rest.pulls.get({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
pull_number: context.issue.number, | |
}); | |
// Check PR body for issue references | |
const prBody = pullRequest.body || ''; | |
const prTitle = pullRequest.title || ''; | |
// Common patterns for linking issues | |
const issuePatterns = [ | |
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi, | |
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:https?:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/)(\d+)/gi, | |
/#(\d+)/g, | |
/(?:issue|issues)\s+#?(\d+)/gi | |
]; | |
let hasLinkedIssue = false; | |
let linkedIssues = new Set(); // Use Set to avoid duplicates | |
// Check PR body and title for issue references | |
const textToCheck = `${prTitle} ${prBody}`; | |
for (const pattern of issuePatterns) { | |
const matches = textToCheck.matchAll(pattern); | |
for (const match of matches) { | |
const issueNumber = match[1]; | |
if (issueNumber) { | |
linkedIssues.add(issueNumber); | |
hasLinkedIssue = true; | |
} | |
} | |
} | |
// Check if PR is currently linked to issues via GitHub's API | |
try { | |
// Get the PR's linked issues using GraphQL API for more accurate results | |
const query = ` | |
query($owner: String!, $repo: String!, $number: Int!) { | |
repository(owner: $owner, name: $repo) { | |
pullRequest(number: $number) { | |
closingIssuesReferences(first: 10) { | |
nodes { | |
number | |
title | |
} | |
} | |
} | |
} | |
} | |
`; | |
const variables = { | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
number: context.issue.number | |
}; | |
const result = await github.graphql(query, variables); | |
const closingIssues = result.repository.pullRequest.closingIssuesReferences.nodes; | |
if (closingIssues && closingIssues.length > 0) { | |
hasLinkedIssue = true; | |
const issueNumbers = closingIssues.map(issue => issue.number); | |
issueNumbers.forEach(num => linkedIssues.add(num.toString())); | |
console.log(`Found ${closingIssues.length} linked issue(s) via GitHub API: ${issueNumbers.join(', ')}`); | |
} | |
} catch (error) { | |
console.log('Could not fetch linked issues via GraphQL:', error.message); | |
// Fallback: Check timeline events but with better logic for current state | |
try { | |
const { data: timelineEvents } = await github.rest.issues.listEventsForTimeline({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: context.issue.number, | |
}); | |
// Track connected and disconnected events to find current state | |
const connectionEvents = timelineEvents.filter(event => | |
event.event === 'connected' || event.event === 'disconnected' | |
).sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); | |
// Check the current connection state by looking at the latest events per issue | |
const currentConnections = new Map(); | |
for (const event of connectionEvents) { | |
if (event.source && event.source.issue) { | |
const issueId = event.source.issue.id; | |
if (event.event === 'connected') { | |
currentConnections.set(issueId, true); | |
} else if (event.event === 'disconnected') { | |
currentConnections.set(issueId, false); | |
} | |
} | |
} | |
// Check if any issues are currently connected | |
const hasCurrentConnections = Array.from(currentConnections.values()).some(connected => connected); | |
if (hasCurrentConnections) { | |
hasLinkedIssue = true; | |
console.log('Found currently linked issues via timeline events fallback'); | |
} | |
} catch (timelineError) { | |
console.log('Could not fetch timeline events:', timelineError.message); | |
} | |
} | |
if (!hasLinkedIssue) { | |
core.setFailed(`❌ This pull request must be linked to an issue. | |
Please link this PR to an issue by: | |
1. Adding "Fixes #<issue-number>" or "Closes #<issue-number>" to the PR description | |
2. Using GitHub's UI to link the PR to an existing issue | |
3. Referencing the issue number with #<issue-number> in the PR title or description | |
Example: "Fixes #123" or "Closes #456"`); | |
} else { | |
const issueList = Array.from(linkedIssues).join(', '); | |
console.log(`✅ PR is linked to issue(s): ${issueList}`); | |
core.notice(`✅ Pull request is properly linked to issue(s): ${issueList}`); | |
} | |
- name: Add comment if no linked issue | |
if: failure() | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
github.rest.issues.createComment({ | |
issue_number: context.issue.number, | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
body: `## ❌ Missing Linked Issue | |
This pull request needs to be linked to at least one issue before it can be merged. | |
### How to link an issue: | |
1. **Add keywords to PR description:** | |
- \`Fixes #123\` | |
- \`Closes #456\` | |
- \`Resolves #789\` | |
- \`Fixes #123, #456, and #789\` (multiple issues) | |
2. **Reference issue in PR title or description:** | |
- \`#123\` | |
- \`Issue #456\` | |
- \`Addresses #123 and #456\` (multiple issues) | |
3. **Use GitHub's UI:** | |
- Go to the "Development" section in the right sidebar | |
- Click "Link an issue" | |
- Select the relevant issue(s) | |
💡 **Tip:** You can link multiple issues using any combination of the above methods! | |
Once you've linked at least one issue, this check will automatically pass! 🚀` | |
}) |