diff --git a/.github/workflows/auto-close-missing-issue.yml b/.github/workflows/auto-close-missing-issue.yml new file mode 100644 index 0000000000..1b45290797 --- /dev/null +++ b/.github/workflows/auto-close-missing-issue.yml @@ -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\`.`, + }); + } \ No newline at end of file diff --git a/.github/workflows/check-linked-issue.yml b/.github/workflows/check-linked-issue.yml new file mode 100644 index 0000000000..8678c97eff --- /dev/null +++ b/.github/workflows/check-linked-issue.yml @@ -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' + }); + }