Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/auto-close-missing-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Auto-close PRs without linked issue

on:
schedule:
- cron: '0 0 * * *' # daily at midnight UTC
workflow_dispatch:

jobs:
auto-close:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write

steps:
- name: Close PRs missing linked issue for over 4 days
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const fourDaysAgo = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);

const prs = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: 'missing-issue',
});

for (const pr of prs) {
if (!pr.pull_request) continue;

const isExempt =
/hotfix/i.test(pr.title) ||
pr.user.login === 'dependabot[bot]' ||
pr.labels.some(l => l.name === 'dependencies');

if (isExempt) continue;

// Find when the missing-issue label was applied
const events = await github.paginate(github.rest.issues.listEventsForTimeline, {
owner,
repo,
issue_number: pr.number,
});

const labelEvent = events
.filter(e => e.event === 'labeled' && e.label?.name === 'missing-issue')
.pop(); // most recent application

if (!labelEvent) continue;
if (new Date(labelEvent.created_at) > fourDaysAgo) continue;

await github.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
state: 'closed',
});

await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: `This PR has been automatically closed because no linked issue was provided within 4 days. Feel free to reopen it after linking an issue, just edit the PR description and add a line like \`Closes #123\`.`,
});
}
89 changes: 89 additions & 0 deletions .github/workflows/check-linked-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Check Linked Issue

on:
pull_request:
types: [opened, edited]

jobs:
check-linked-issue:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write

steps:
- name: Check for linked issue
id: check
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const title = pr.title || '';
const body = pr.body || '';
const author = pr.user.login;
const labels = pr.labels.map(l => l.name);

const isExempt =
/hotfix/i.test(title) ||
author === 'dependabot[bot]' ||
labels.includes('dependencies');

if (isExempt) {
core.setOutput('exempt', 'true');
core.setOutput('has_linked_issue', 'true');
return;
}
core.setOutput('exempt', 'false');

const linkedIssuePattern = /\b(closes|fixes|resolves|close|fix|resolve)\s+#\d+/i;
const hasLinkedIssue = linkedIssuePattern.test(body);
core.setOutput('has_linked_issue', hasLinkedIssue);

- name: Apply missing-issue label and comment
if: steps.check.outputs.has_linked_issue == 'false'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;

// Apply label
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: ['missing-issue']
});

// Post comment
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: `Thanks for the PR! To ensure we're solving the right problems, we require all PRs to be linked to an open issue. Please open one describing the problem this PR addresses so we can discuss the implementation there first.\n\nYou have 4 days to link an issue before this PR is automatically closed, it can be reopened at any time after that. Looking forward to the discussion!\n\n> To link an issue, edit the PR description and add a line like \`Closes #123\`.`
});

- name: Remove missing-issue label if issue is now linked
if: steps.check.outputs.has_linked_issue == 'true'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;

const labels = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number
});

const hasMissingLabel = labels.data.some(l => l.name === 'missing-issue');

if (hasMissingLabel) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number,
name: 'missing-issue'
});
}
Loading