From a99a039f54f0a3b23318077094552a6da4ad618f Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Mon, 27 Jul 2026 20:59:00 +0530 Subject: [PATCH 1/6] ci: add issue approval workflows for maintainer triage --- .../close-prs-for-unapproved-issues.yml | 86 ++++++++++++ .github/workflows/issue-approve-command.yml | 130 ++++++++++++++++++ .github/workflows/issue-awaiting-approval.yml | 47 +++++++ 3 files changed, 263 insertions(+) create mode 100644 .github/workflows/close-prs-for-unapproved-issues.yml create mode 100644 .github/workflows/issue-approve-command.yml create mode 100644 .github/workflows/issue-awaiting-approval.yml diff --git a/.github/workflows/close-prs-for-unapproved-issues.yml b/.github/workflows/close-prs-for-unapproved-issues.yml new file mode 100644 index 0000000000..edc7961d79 --- /dev/null +++ b/.github/workflows/close-prs-for-unapproved-issues.yml @@ -0,0 +1,86 @@ +# 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 + +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; + 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) { + console.log(`Could not fetch labels for issue #${issueNumber}: ${error.message}`); + } + } + + console.log('All linked issues are approved or no matching label found. PR stays open.'); diff --git a/.github/workflows/issue-approve-command.yml b/.github/workflows/issue-approve-command.yml new file mode 100644 index 0000000000..8cd3467f6d --- /dev/null +++ b/.github/workflows/issue-approve-command.yml @@ -0,0 +1,130 @@ +# 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' && + 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') && + startsWith(github.event.comment.body, '/approve') + + runs-on: ubuntu-latest + + steps: + - name: Remove 'Awaiting Approval' label + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_TOKEN }} + script: | + const labelName = 'Awaiting Approval'; + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + if (labels.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 + 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)) { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: labelName, + color: labelColor, + description: labelDescription, + }); + } + + 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 + 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, + }); diff --git a/.github/workflows/issue-awaiting-approval.yml b/.github/workflows/issue-awaiting-approval.yml new file mode 100644 index 0000000000..78c6bf9c7f --- /dev/null +++ b/.github/workflows/issue-awaiting-approval.yml @@ -0,0 +1,47 @@ +# 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)) { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: labelName, + color: labelColor, + description: labelDescription, + }); + } + + 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], + }); From 5608c3a638632a4397a7f2fb636913a636646e88 Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Tue, 28 Jul 2026 19:46:47 +0530 Subject: [PATCH 2/6] ci: add idempotency guard and race-safe label creation --- .../close-prs-for-unapproved-issues.yml | 17 ++++++++- .github/workflows/issue-approve-command.yml | 38 ++++++++++++------- .github/workflows/issue-awaiting-approval.yml | 18 +++++---- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/.github/workflows/close-prs-for-unapproved-issues.yml b/.github/workflows/close-prs-for-unapproved-issues.yml index edc7961d79..1597471031 100644 --- a/.github/workflows/close-prs-for-unapproved-issues.yml +++ b/.github/workflows/close-prs-for-unapproved-issues.yml @@ -23,7 +23,7 @@ jobs: 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; + const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s#(\d+)/gi; const matches = [...prBody.matchAll(issuePattern)]; if (matches.length === 0) { @@ -79,6 +79,21 @@ jobs: 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}`); } } diff --git a/.github/workflows/issue-approve-command.yml b/.github/workflows/issue-approve-command.yml index 8cd3467f6d..b08f2f8510 100644 --- a/.github/workflows/issue-approve-command.yml +++ b/.github/workflows/issue-approve-command.yml @@ -16,7 +16,7 @@ jobs: github.event.issue.state != 'closed' && github.actor != 'derberg' && github.actor != 'Adi-204' && - startsWith(github.event.comment.body, '/approve') + (github.event.comment.body == '/approve' || startsWith(github.event.comment.body, '/approve ')) runs-on: ubuntu-latest @@ -44,24 +44,30 @@ jobs: !github.event.issue.pull_request && github.event.issue.state != 'closed' && (github.actor == 'derberg' || github.actor == 'Adi-204') && - startsWith(github.event.comment.body, '/approve') + (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 labelName = 'Awaiting Approval'; - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + 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 (labels.some(l => l.name === labelName)) { + if (currentLabels.some(l => l.name === labelName)) { console.log(`Removing label '${labelName}'...`); await github.rest.issues.removeLabel({ issue_number: context.issue.number, @@ -72,6 +78,7 @@ jobs: } - name: Add 'Approved Issue' label + if: steps.guard.outputs.skip != 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.GH_TOKEN }} @@ -87,13 +94,17 @@ jobs: }); if (!repoLabels.some(l => l.name === labelName)) { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: labelName, - color: labelColor, - description: labelDescription, - }); + 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}...`); @@ -105,6 +116,7 @@ jobs: }); - name: Post approval instruction comment + if: steps.guard.outputs.skip != 'true' uses: actions/github-script@v7 env: APPROVER: ${{ github.actor }} @@ -120,7 +132,7 @@ jobs: - 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}`; + _Approved by @${approver}_`; await github.rest.issues.createComment({ issue_number: context.issue.number, diff --git a/.github/workflows/issue-awaiting-approval.yml b/.github/workflows/issue-awaiting-approval.yml index 78c6bf9c7f..e5f00ac7ba 100644 --- a/.github/workflows/issue-awaiting-approval.yml +++ b/.github/workflows/issue-awaiting-approval.yml @@ -29,13 +29,17 @@ jobs: }); if (!repoLabels.some(l => l.name === labelName)) { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: labelName, - color: labelColor, - description: labelDescription, - }); + 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}...`); From e135524c25744d618d5643fe9fc241081a429531 Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Thu, 30 Jul 2026 19:12:17 +0530 Subject: [PATCH 3/6] ci: pin actions/github-script to full SHA in issue approval workflows --- .github/workflows/close-prs-for-unapproved-issues.yml | 2 +- .github/workflows/issue-approve-command.yml | 8 ++++---- .github/workflows/issue-awaiting-approval.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/close-prs-for-unapproved-issues.yml b/.github/workflows/close-prs-for-unapproved-issues.yml index 1597471031..de0c5647e4 100644 --- a/.github/workflows/close-prs-for-unapproved-issues.yml +++ b/.github/workflows/close-prs-for-unapproved-issues.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check linked issue and close PR if unapproved - uses: actions/github-script@v7 + uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | diff --git a/.github/workflows/issue-approve-command.yml b/.github/workflows/issue-approve-command.yml index b08f2f8510..aa1f41c5c5 100644 --- a/.github/workflows/issue-approve-command.yml +++ b/.github/workflows/issue-approve-command.yml @@ -22,7 +22,7 @@ jobs: steps: - name: ❌ @${{ github.actor }} is not authorized to approve issues - uses: actions/github-script@v7 + uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ACTOR: ${{ github.actor }} with: @@ -51,7 +51,7 @@ jobs: steps: - name: Remove 'Awaiting Approval' label id: guard - uses: actions/github-script@v7 + uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | @@ -79,7 +79,7 @@ jobs: - name: Add 'Approved Issue' label if: steps.guard.outputs.skip != 'true' - uses: actions/github-script@v7 + uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | @@ -117,7 +117,7 @@ jobs: - name: Post approval instruction comment if: steps.guard.outputs.skip != 'true' - uses: actions/github-script@v7 + uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: APPROVER: ${{ github.actor }} with: diff --git a/.github/workflows/issue-awaiting-approval.yml b/.github/workflows/issue-awaiting-approval.yml index e5f00ac7ba..f1ecb857fa 100644 --- a/.github/workflows/issue-awaiting-approval.yml +++ b/.github/workflows/issue-awaiting-approval.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Add 'Awaiting Approval' label - uses: actions/github-script@v7 + uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | From 2ffb87f1fadfce24e1c38275f4d554a69cba2c5b Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Sat, 1 Aug 2026 21:35:54 +0530 Subject: [PATCH 4/6] use actions/github-script instead of actions/checkout in workflow files --- .github/workflows/close-prs-for-unapproved-issues.yml | 3 ++- .github/workflows/issue-approve-command.yml | 8 ++++---- .github/workflows/issue-awaiting-approval.yml | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/close-prs-for-unapproved-issues.yml b/.github/workflows/close-prs-for-unapproved-issues.yml index de0c5647e4..c2ef4d927f 100644 --- a/.github/workflows/close-prs-for-unapproved-issues.yml +++ b/.github/workflows/close-prs-for-unapproved-issues.yml @@ -9,6 +9,7 @@ on: types: - opened - edited + - reopened jobs: close-if-unapproved: @@ -16,7 +17,7 @@ jobs: steps: - name: Check linked issue and close PR if unapproved - uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | diff --git a/.github/workflows/issue-approve-command.yml b/.github/workflows/issue-approve-command.yml index aa1f41c5c5..d475172bc9 100644 --- a/.github/workflows/issue-approve-command.yml +++ b/.github/workflows/issue-approve-command.yml @@ -22,7 +22,7 @@ jobs: steps: - name: ❌ @${{ github.actor }} is not authorized to approve issues - uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ACTOR: ${{ github.actor }} with: @@ -51,7 +51,7 @@ jobs: steps: - name: Remove 'Awaiting Approval' label id: guard - uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | @@ -79,7 +79,7 @@ jobs: - name: Add 'Approved Issue' label if: steps.guard.outputs.skip != 'true' - uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | @@ -117,7 +117,7 @@ jobs: - name: Post approval instruction comment if: steps.guard.outputs.skip != 'true' - uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: APPROVER: ${{ github.actor }} with: diff --git a/.github/workflows/issue-awaiting-approval.yml b/.github/workflows/issue-awaiting-approval.yml index f1ecb857fa..9668851a3c 100644 --- a/.github/workflows/issue-awaiting-approval.yml +++ b/.github/workflows/issue-awaiting-approval.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Add 'Awaiting Approval' label - uses: actions/checkout@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_TOKEN }} script: | From 91a837a8123683f6ec6d6d06ab1e54eeb590add6 Mon Sep 17 00:00:00 2001 From: Varshitha Besthavemula Date: Mon, 17 Aug 2026 10:57:08 +0530 Subject: [PATCH 5/6] remove 'see-also' from pull-request-template --- .github/pull-request-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md index 74970b19db..1de22c3551 100644 --- a/.github/pull-request-template.md +++ b/.github/pull-request-template.md @@ -12,7 +12,7 @@ **Related issue(s)** +For example, `Resolves #123` or `Fixes #43` --> **AI assistance**