Skip to content
101 changes: 101 additions & 0 deletions .github/workflows/close-prs-for-unapproved-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Purpose of this workflow is to automatically close PRs that reference
# issues still carrying the 'Awaiting Approval' label, preventing work
# on issues that have not yet been approved by a maintainer.

name: Close PRs linked to unapproved issues

on:
pull_request_target:
types:
- opened
- edited
Comment thread
Adi-204 marked this conversation as resolved.

jobs:
close-if-unapproved:
runs-on: ubuntu-latest

steps:
- name: Check linked issue and close PR if unapproved
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const prBody = context.payload.pull_request.body || '';
const prNumber = context.payload.pull_request.number;

const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s#(\d+)/gi;
Comment thread
Adi-204 marked this conversation as resolved.
const matches = [...prBody.matchAll(issuePattern)];

if (matches.length === 0) {
console.log(`No linked issues found in PR #${prNumber} body. Closing PR.`);

await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚫 This PR has been automatically closed because it does not reference a linked issue.\n\nPlease update your PR description to include a reference to the issue it addresses (e.g. \`Fixes #123\`, \`Closes #123\`, or \`Resolves #123\`) and reopen this PR.`,
});

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

return;
}

const awaitingLabel = 'Awaiting Approval';

for (const match of matches) {
const issueNumber = parseInt(match[1], 10);
console.log(`Checking issue #${issueNumber} for '${awaitingLabel}' label...`);

try {
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});

if (labels.some(l => l.name === awaitingLabel)) {
console.log(`Issue #${issueNumber} still has '${awaitingLabel}'. Closing PR #${prNumber}.`);

await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚫 This PR has been automatically closed because the linked issue #${issueNumber} has not been approved yet.\n\nPlease wait for a maintainer to approve the issue with the \`/approve\` command before opening a PR.`,
});

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

return;
}
} catch (error) {
if (error.status === 404) {
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚫 This PR references issue #${issueNumber}, which does not exist. Please correct the linked issue reference.`,
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
return;
}
console.log(`Could not fetch labels for issue #${issueNumber}: ${error.message}`);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

console.log('All linked issues are approved or no matching label found. PR stays open.');
142 changes: 142 additions & 0 deletions .github/workflows/issue-approve-command.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Purpose of this workflow is to allow authorized maintainers (derberg, Adi-204)
# to approve issues via the /approve command, swapping the 'Awaiting Approval'
# label for 'Approved Issue' and posting an instruction comment for contributors.

name: Issue /approve command

on:
issue_comment:
types:
- created

jobs:
guard-against-unauthorized-use:
if: >
!github.event.issue.pull_request &&
github.event.issue.state != 'closed' &&
github.actor != 'derberg' &&
github.actor != 'Adi-204' &&
(github.event.comment.body == '/approve' || startsWith(github.event.comment.body, '/approve '))

runs-on: ubuntu-latest

steps:
- name: ❌ @${{ github.actor }} is not authorized to approve issues
uses: actions/github-script@v7
env:
ACTOR: ${{ github.actor }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const commentText = `❌ @${process.env.ACTOR} is not authorized to use the \`/approve\` command.
Only maintainers [@derberg](https://github.com/derberg) and [@Adi-204](https://github.com/Adi-204) can approve issues.`;

console.log(`❌ @${process.env.ACTOR} made an unauthorized attempt to use /approve.`);
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentText,
});

approve-issue:
if: >
!github.event.issue.pull_request &&
github.event.issue.state != 'closed' &&
(github.actor == 'derberg' || github.actor == 'Adi-204') &&
(github.event.comment.body == '/approve' || startsWith(github.event.comment.body, '/approve '))

runs-on: ubuntu-latest

steps:
- name: Remove 'Awaiting Approval' label
id: guard
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
if (currentLabels.some(l => l.name === 'Approved Issue')) {
console.log('Issue already approved, skipping.');
core.setOutput('skip', 'true');
return;
}
const labelName = 'Awaiting Approval';

if (currentLabels.some(l => l.name === labelName)) {
console.log(`Removing label '${labelName}'...`);
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
});
}

- name: Add 'Approved Issue' label
if: steps.guard.outputs.skip != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const labelName = 'Approved Issue';
const labelColor = '0e8a16';
const labelDescription = 'Issue has been approved by a maintainer and is ready for work';

// Ensure the label exists in the repo
const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});

if (!repoLabels.some(l => l.name === labelName)) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
color: labelColor,
description: labelDescription,
});
} catch (e) {
if (e.status !== 422) throw e;
}
}

console.log(`Adding label '${labelName}' to issue #${context.issue.number}...`);
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [labelName],
});

- name: Post approval instruction comment
if: steps.guard.outputs.skip != 'true'
uses: actions/github-script@v7
env:
APPROVER: ${{ github.actor }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const approver = process.env.APPROVER;
const commentBody = `👋 Thanks for your patience. This issue has been reviewed and is ready to be worked on.

**Before you start:**

- Comment below to let others know you're working on it (avoids duplicate work)
- Read our [CONTRIBUTING.md](../blob/master/CONTRIBUTING.md) if you haven't already
- Fork the repo, create a feature branch, and open a draft PR early

_Approved by @${approver}_`;

await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
51 changes: 51 additions & 0 deletions .github/workflows/issue-awaiting-approval.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Purpose of this workflow is to automatically add the 'Awaiting Approval' label
# to every newly opened issue so maintainers can triage before work begins.

name: Add 'Awaiting Approval' label on new issues

on:
issues:
types:
- opened

jobs:
add-awaiting-approval-label:
runs-on: ubuntu-latest

steps:
- name: Add 'Awaiting Approval' label
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const labelName = 'Awaiting Approval';
const labelColor = 'fbca04';
const labelDescription = 'Issue awaiting maintainer approval before work can begin';

// Ensure the label exists in the repo
const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});

if (!repoLabels.some(l => l.name === labelName)) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
color: labelColor,
description: labelDescription,
});
} catch (e) {
if (e.status !== 422) throw e;
}
}

console.log(`Adding label '${labelName}' to issue #${context.issue.number}...`);
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [labelName],
});
Loading