feat(github): revival of triage tools (#5007)

* feat(github): revival of triage tools

* fix(github): workflow doc fix

* fix(github): PR detection

* fix(gh): wontfix preserve bug label

Removed state_reason from issue update when adding wontfix label.

* fix(gh): doc cleaning

* one more thing

* fix(gh): prevent Gabi moment

That's the only thing I will agree with this piece of hardware

* Fix syntax error in triage-tools.yaml

* fix(github): restrict triage commands to issues

---------

Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
Ivan Lepekha
2026-08-19 16:03:06 -05:00
committed by GitHub
co-authored by Chris Titus
parent 792122e998
commit 032ce8ead0
4 changed files with 307 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
name: Repo Contributors' Issue Triage Tools
on:
issue_comment:
types: [created, edited]
jobs:
triage-tools:
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: none
contents: none
steps:
- name: Process issue command
uses: actions/github-script@v9
with:
script: |
const trustedUsers = [
7896101, // ChrisTitusTech
57459428, // FluffyPunk
125669256, // FallenGME
90123670, // mewclouds
121827219, // MyDrift-user
17331812, // Callum
101426328, // CodingWonders
70659536 // og-mrk
];
if (context.payload.issue.pull_request) {
console.log("Pull request comments are not supported. Exiting.");
return;
}
const comment = context.payload.comment;
const commentAuthor = comment.user;
if (!trustedUsers.includes(commentAuthor.id)) {
console.log(`Comment author ${commentAuthor.login} is not a trusted user. Exiting.`);
return;
}
const {owner, repo} = context.repo;
const issueNumber = context.issue.number;
const command = comment.body.trim().toLowerCase();
const triageMatch = command.match(/^\/triage$/);
const triageOffMatch = command.match(/^\/triageoff$/);
const notPlannedMatch = command.match(/^\/np(?:\s+(\w+))?$/);
const duplicateMatch = command.match(/^\/duplicate\s+#?(\d+)$/);
if (triageMatch) {
const issue = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});
if (issue.data.labels.some(label => label.name === "needs-triage")) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "This issue already has the 'needs-triage' label. Use /triageoff to remove it."
});
} else {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["needs-triage"]
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} requested maintainer triage for this issue.`
});
}
} else if (triageOffMatch) {
const issue = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});
if (issue.data.labels.some(label => label.name === "needs-triage")) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "needs-triage"
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} removed the triage request from this issue.`
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "This issue is not currently marked for triage."
});
}
} else if (notPlannedMatch) {
const reason = notPlannedMatch[1];
if (reason === "wontfix") {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["wontfix"]
});
} else if (reason === "notrelated") {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["not-related"]
});
}
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: "closed",
state_reason: "not_planned"
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} closed this issue as not planned.`
});
} else if (duplicateMatch) {
const duplicateIssueNumber = Number(duplicateMatch[1]);
if (!Number.isSafeInteger(duplicateIssueNumber) || duplicateIssueNumber < 1) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "The duplicate target must be a positive issue number."
});
} else if (duplicateIssueNumber === issueNumber) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: "An issue cannot be marked as a duplicate of itself."
});
} else {
let duplicateIssue;
try {
const response = await github.rest.issues.get({
owner,
repo,
issue_number: duplicateIssueNumber
});
duplicateIssue = response.data;
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
if (duplicateIssue?.pull_request) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `#${duplicateIssueNumber} is a pull request, not an issue.`
});
} else if (duplicateIssue) {
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: "closed",
state_reason: "duplicate",
duplicate_issue_id: duplicateIssue.id
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `${commentAuthor.login} closed this issue as a duplicate of #${duplicateIssueNumber}.`
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `Issue #${duplicateIssueNumber} does not exist.`
});
}
}
} else {
console.log("Comment does not contain a supported issue command. Exiting.");
return;
}
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id
});
+1
View File
@@ -56,6 +56,7 @@ export default defineConfig({
label: 'Code Reference',
items: [
{ label: 'Architecture & Design', slug: 'code-reference/architecture' },
{ label: 'Issue Triage Commands', slug: 'code-reference/issue-triage' },
{ label: 'Tweaks Reference', items: [{ autogenerate: { directory: 'code-reference/tweaks' } }] },
{ label: 'Features Reference', items: [{ autogenerate: { directory: 'code-reference/features' } }] },
],
@@ -0,0 +1,26 @@
---
title: Issue Triage Commands
description: Commands trusted maintainers can use to label and close GitHub issues.
---
Trusted repository members whose numeric GitHub user IDs are listed in the issue triage workflow can run the commands below. These commands work only on issues. Comments on pull requests are ignored, and the workflow token has no pull-request write permission.
## Commands
- `/triage` adds the `needs-triage` label.
- `/triageoff` removes the `needs-triage` label when it is present.
- `/np` closes the issue as not planned.
- `/np wontfix` adds the `wontfix` label without replacing existing labels, then closes the issue as not planned.
- `/np notrelated` adds the `not-related` label without replacing existing labels, then closes the issue as not planned.
- `/np <reason>` closes the issue as not planned without adding a label for unrecognized reasons.
- `/duplicate <issue number>` closes the issue as a duplicate of another issue. The positive issue number may optionally start with `#`; pull request numbers are rejected.
## Command handling
Put exactly one command in the comment. Leading and trailing whitespace is allowed, but text before or after the command is not. Command matching is case-insensitive, and malformed issue numbers are rejected.
After a command is handled, the workflow deletes the command comment and leaves an audit comment describing the result. A missing duplicate target or a request to duplicate an issue into itself is reported without closing the issue. Unexpected GitHub API failures stop the workflow and leave the command comment available for retry.
## Access
Access is granted by adding the trusted member's immutable numeric GitHub user ID to `.github/workflows/triage-tools.yaml` through a pull request. Abuse can result in removal from the allowlist or other repository moderation action.
+68
View File
@@ -0,0 +1,68 @@
#===========================================================================
# Tests - GitHub Issue Triage Workflow
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$script:workflowPath = Join-Path $script:repoRoot ".github\workflows\triage-tools.yaml"
$script:guidePath = Join-Path $script:repoRoot "docs\src\content\docs\code-reference\issue-triage.mdx"
$script:workflow = Get-Content -Path $script:workflowPath -Raw
$script:guide = Get-Content -Path $script:guidePath -Raw
}
Describe "GitHub issue triage workflow" {
It "cannot run against pull requests" {
$script:workflow | Should -Match '(?m)^\s+if:\s+\$\{\{\s*!github\.event\.issue\.pull_request\s*\}\}\s*$'
$script:workflow | Should -Match 'if \(context\.payload\.issue\.pull_request\)'
$script:workflow | Should -Match '(?m)^\s+issues:\s+write\s*$'
$script:workflow | Should -Match '(?m)^\s+pull-requests:\s+none\s*$'
$script:workflow | Should -Match '(?m)^\s+contents:\s+none\s*$'
$script:workflow | Should -Not -Match '(?m)^\s+pull-requests:\s+write\s*$'
}
It "authorizes trusted users by immutable numeric ID" {
$script:workflow | Should -Match 'trustedUsers\.includes\(commentAuthor\.id\)'
$script:workflow | Should -Not -Match 'trustedUsers\.includes\(commentAuthor\.login\)'
}
It "matches one complete command at a time" {
$script:workflow.Contains('const command = comment.body.trim().toLowerCase();') | Should -BeTrue
$script:workflow.Contains('const triageMatch = command.match(/^\/triage$/);') | Should -BeTrue
$script:workflow.Contains('const triageOffMatch = command.match(/^\/triageoff$/);') | Should -BeTrue
$script:workflow.Contains('const notPlannedMatch = command.match(/^\/np(?:\s+(\w+))?$/);') | Should -BeTrue
$script:workflow.Contains('const duplicateMatch = command.match(/^\/duplicate\s+#?(\d+)$/);') | Should -BeTrue
$script:workflow.Contains('} else if (triageOffMatch) {') | Should -BeTrue
$script:workflow.Contains('} else if (notPlannedMatch) {') | Should -BeTrue
$script:workflow.Contains('} else if (duplicateMatch) {') | Should -BeTrue
}
It "preserves existing labels for not-related closures" {
$notRelatedBlock = [regex]::Match(
$script:workflow,
'(?s)reason === "notrelated".*?github\.rest\.issues\.addLabels\(\{.*?labels: \["not-related"\].*?github\.rest\.issues\.update'
)
$notRelatedBlock.Success | Should -BeTrue
$notRelatedBlock.Value | Should -Not -Match 'issues\.update\(\{.*?labels:'
}
It "validates duplicate targets before closing the issue" {
$script:workflow.Contains('const duplicateIssueNumber = Number(duplicateMatch[1]);') | Should -BeTrue
$script:workflow | Should -Match 'Number\.isSafeInteger\(duplicateIssueNumber\)'
$script:workflow | Should -Match 'duplicateIssueNumber === issueNumber'
$script:workflow | Should -Match 'duplicateIssue = response\.data'
$script:workflow | Should -Match 'duplicateIssue\?\.pull_request'
$script:workflow | Should -Match 'error\.status !== 404'
$script:workflow | Should -Match 'throw error'
$script:workflow | Should -Match 'duplicate_issue_id: duplicateIssue\.id'
}
It "keeps the maintainer guide in the documentation site" {
Test-Path -Path (Join-Path $script:repoRoot ".github\TRIAGE_TOOLS.md") | Should -BeFalse
$script:guide | Should -Match 'These commands work only on issues\.'
$script:guide | Should -Match '`not-related` label without replacing existing labels'
$astroConfig = Get-Content -Path (Join-Path $script:repoRoot "docs\astro.config.mjs") -Raw
$astroConfig | Should -Match "slug: 'code-reference/issue-triage'"
}
}