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
4 changes: 2 additions & 2 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[build]
# Keep compiler and linker memory from multiplying across CPU-count build jobs.
jobs = 1
# Balance build parallelism against peak compiler and linker memory use.
jobs = 8
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Bug Report
description: Report a bug or unexpected behavior
title: "[Bug]: "
labels: ["type: bug", "status: triage"]
labels: ["type: bug"]
body:
- type: markdown
attributes:
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Feature Request
description: Suggest a new feature or improvement
title: "[Feature]: "
labels: ["type: feature", "status: triage"]
labels: ["type: feature"]
body:
- type: markdown
attributes:
Expand Down
63 changes: 63 additions & 0 deletions .github/scripts/issue-triage.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const TRIAGE_LABEL = "status: triage";

module.exports = async function runIssueTriage({ github, context }) {
const event = context.payload;
if (event.issue.pull_request) return;

let command;
if (context.eventName === "issue_comment") {
if (event.action !== "created" || event.comment.user.type === "Bot") return;
command = event.comment.body.trim().match(/^\/(triaged|duplicate #([1-9]\d*))$/);
if (!command) return;

const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
...context.repo,
username: event.comment.user.login,
});
if (!["write", "admin"].includes(data.permission)) return;
} else if (context.eventName !== "issues" || !["opened", "reopened", "closed"].includes(event.action)) {
return;
}

const source = { ...context.repo, issue_number: event.issue.number };
const { data: issue } = await github.rest.issues.get(source);
const removeTriage = async () => {
if (!issue.labels.some(label => label.name === TRIAGE_LABEL)) return;
try {
await github.rest.issues.removeLabel({ ...source, name: TRIAGE_LABEL });
} catch (error) {
if (error.status !== 404) throw error;
}
};

if (context.eventName === "issues") {
if (event.action === "closed") {
if (issue.state === "closed") await removeTriage();
} else if (issue.state === "open" && !issue.labels.some(label => label.name === TRIAGE_LABEL)) {
await github.rest.issues.addLabels({ ...source, labels: [TRIAGE_LABEL] });
}
return;
}

if (command[1] === "triaged" || issue.state === "closed") {
await removeTriage();
return;
}

const targetNumber = Number(command[2]);
if (!Number.isSafeInteger(targetNumber) || targetNumber === issue.number) {
throw new Error("The duplicate target must be a different issue in this repository.");
}
const { data: target } = await github.rest.issues.get({ ...context.repo, issue_number: targetNumber });
if (target.pull_request) throw new Error("The duplicate target must be an issue, not a pull request.");

await github.rest.issues.update({
...source,
state: "closed",
state_reason: "duplicate",
duplicate_issue_id: target.id,
headers: { "X-GitHub-Api-Version": "2026-03-10" },
});
// GITHUB_TOKEN mutations do not trigger the issue-closed workflow.
await removeTriage();
};
185 changes: 185 additions & 0 deletions .github/scripts/issue-triage.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
const assert = require("node:assert/strict");
const test = require("node:test");
const runIssueTriage = require("./issue-triage.cjs");

function fixture({ permission = "write", state = "open", triage = true, targetState = "open", targetPr = false } = {}) {
const source = {
number: 40, id: 90040, state, assignees: [{ login: "owner" }],
labels: [{ name: "type: bug" }, { name: "area: tui" }, ...(triage ? [{ name: "status: triage" }] : [])],
};
const target = { number: 7, id: 812345, state: targetState, ...(targetPr ? { pull_request: {} } : {}) };
const calls = [];
const github = { rest: { repos: {
async getCollaboratorPermissionLevel(args) {
calls.push(["permission", args]);
return { data: { permission } };
},
}, issues: {
async get(args) {
calls.push(["get", args]);
const issue = args.issue_number === 40 ? source : args.issue_number === 7 ? target : undefined;
if (!issue) throw Object.assign(new Error("not found"), { status: 404 });
return { data: structuredClone(issue) };
},
async addLabels(args) {
calls.push(["add", args]);
for (const name of args.labels) {
if (!source.labels.some(label => label.name === name)) source.labels.push({ name });
}
},
async removeLabel(args) {
calls.push(["remove", args]);
source.labels = source.labels.filter(label => label.name !== args.name);
},
async update(args) {
calls.push(["update", args]);
Object.assign(source, { state: args.state, state_reason: args.state_reason, duplicate_issue_id: args.duplicate_issue_id });
},
} } };
const context = {
repo: { owner: "maintainer", repo: "project" },
eventName: "issues",
payload: { action: "opened", issue: { number: 40 }, comment: { body: "/triaged", user: { login: "reviewer", type: "User" } } },
};
return {
github, context, source, target, calls,
mutations: () => calls.filter(([method]) => ["add", "remove", "update"].includes(method)),
run: () => runIssueTriage({ github, context }),
command(body) { context.eventName = "issue_comment"; context.payload.action = "created"; context.payload.comment.body = body; },
};
}

for (const action of ["opened", "reopened"]) {
test(`${action} queues assessment without changing existing classification or assignment`, async () => {
const f = fixture({ triage: false });
f.context.payload.action = action;
await f.run();
assert.deepEqual(f.source.labels.map(label => label.name), ["type: bug", "area: tui", "status: triage"]);
assert.deepEqual(f.source.assignees, [{ login: "owner" }]);
await f.run();
assert.equal(f.mutations().length, 1, "replayed event must not write another label");
});
}

test("closure clears only triage and a delayed closure cannot clear a reopened issue", async () => {
for (const state of ["closed", "open"]) {
const f = fixture({ state });
f.context.payload.action = "closed";
await f.run();
assert.deepEqual(f.source.labels.map(label => label.name), state === "closed" ? ["type: bug", "area: tui"] : ["type: bug", "area: tui", "status: triage"]);
}
});

test("a delayed opened event cannot retriage an already closed issue", async () => {
const f = fixture({ state: "closed", triage: false });
await f.run();
assert.deepEqual(f.mutations(), []);
});

test("triaged requires actual write access and never changes ownership or closes the issue", async () => {
for (const permission of ["write", "admin", "read", "none"]) {
const f = fixture({ permission });
f.command("/triaged");
f.context.payload.comment.author_association = "CONTRIBUTOR";
await f.run();
const allowed = ["write", "admin"].includes(permission);
assert.equal(f.source.labels.some(label => label.name === "status: triage"), !allowed);
assert.equal(f.source.state, "open");
assert.deepEqual(f.source.assignees, [{ login: "owner" }]);
assert.equal(f.calls.some(([method]) => method === "update"), false);
}
});

test("untrusted users cannot close an issue with a valid duplicate command", async () => {
const f = fixture({ permission: "read" });
f.command("/duplicate #7");
f.context.payload.comment.author_association = "MEMBER";
await f.run();
assert.deepEqual(f.mutations(), []);
assert.equal(f.source.state, "open");
});

test("permission lookup failure cannot fall back to trusting author association", async () => {
const f = fixture();
f.command("/duplicate #7");
f.context.payload.comment.author_association = "OWNER";
f.github.rest.repos.getCollaboratorPermissionLevel = async () => { throw new Error("permission service unavailable"); };
await assert.rejects(f.run(), /permission service unavailable/);
assert.deepEqual(f.mutations(), []);
});

test("quoted commands, code, prose, malformed targets and edited comments do not run", async () => {
for (const body of ["> /triaged", "`/triaged`", "```\n/duplicate #7\n```", "Example:\n/duplicate #7", "/duplicate #0", "/duplicate owner/other#7", "/duplicate #7; echo unsafe", "/duplicate #7\n/triaged"]) {
const f = fixture();
f.command(body);
await f.run();
assert.deepEqual(f.calls, [], body);
}
const f = fixture();
f.command("/duplicate #7");
f.context.payload.action = "edited";
await f.run();
assert.deepEqual(f.calls, []);
});

test("bot comments and PR threads make no API calls", async () => {
for (const kind of ["bot", "pr"]) {
const f = fixture();
f.command("/duplicate #7");
if (kind === "bot") f.context.payload.comment.user.type = "Bot";
else f.context.payload.issue.pull_request = {};
await f.run();
assert.deepEqual(f.calls, []);
}
});

test("duplicate closure records the canonical issue database ID and preserves other labels", async () => {
for (const targetState of ["open", "closed"]) {
const f = fixture({ targetState });
f.command("/duplicate #7");
await f.run();
assert.equal(f.source.state, "closed");
assert.equal(f.source.state_reason, "duplicate");
assert.equal(f.source.duplicate_issue_id, 812345, "database ID must not be the issue number 7");
assert.deepEqual(f.source.labels.map(label => label.name), ["type: bug", "area: tui"]);
assert.deepEqual(f.calls.find(([method]) => method === "update")[1], {
owner: "maintainer", repo: "project", issue_number: 40,
state: "closed", state_reason: "duplicate", duplicate_issue_id: 812345,
headers: { "X-GitHub-Api-Version": "2026-03-10" },
});
await f.run();
assert.equal(f.calls.filter(([method]) => method === "update").length, 1);
}
});

test("self, nonexistent, unsafe integer and PR targets leave the source untouched", async () => {
for (const [body, targetPr] of [["/duplicate #40", false], ["/duplicate #999", false], ["/duplicate #9007199254740992", false], ["/duplicate #7", true]]) {
const f = fixture({ targetPr });
f.command(body);
await assert.rejects(f.run());
assert.deepEqual(f.mutations(), []);
assert.equal(f.source.state, "open");
}
});

test("a failed duplicate write does not remove triage or change classification", async () => {
const f = fixture();
f.command("/duplicate #7");
f.github.rest.issues.update = async () => { throw new Error("write failed"); };
await assert.rejects(f.run(), /write failed/);
assert.equal(f.source.state, "open");
assert.deepEqual(f.mutations(), []);
});

test("retry after successful closure repairs failed label cleanup without reclosing", async () => {
const f = fixture();
f.command("/duplicate #7");
const remove = f.github.rest.issues.removeLabel;
f.github.rest.issues.removeLabel = async () => { throw new Error("cleanup failed"); };
await assert.rejects(f.run(), /cleanup failed/);
assert.equal(f.source.state, "closed");
f.github.rest.issues.removeLabel = remove;
await f.run();
assert.deepEqual(f.source.labels.map(label => label.name), ["type: bug", "area: tui"]);
assert.equal(f.calls.filter(([method]) => method === "update").length, 1);
});
2 changes: 1 addition & 1 deletion .github/workflows/_agent-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
run: npm run build

- name: Run tests
run: npm run test
run: node --test 'dist/**/*.test.js'

- name: Run lint
run: npm run lint
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/dependency-monitor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:

if [[ "${CLEAN}" == "true" ]]; then
if [[ -n "${existing_issue}" ]]; then
gh issue edit "${existing_issue}" --repo "${REPO}" --remove-label "status: triage"
gh issue close "${existing_issue}" \
--repo "${REPO}" \
--reason completed \
Expand Down Expand Up @@ -123,6 +124,7 @@ jobs:
--repo "${REPO}" \
--title "${issue_title}" \
--label "type: security" \
--label "status: triage" \
--body-file "${issue_file}"
fi

Expand Down Expand Up @@ -238,5 +240,6 @@ jobs:
--title "${issue_title}" \
--label "type: dependencies" \
--label "area: agent-sdk" \
--label "status: triage" \
--body-file "${issue_file}"
fi
37 changes: 37 additions & 0 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Issue Triage

on:
issues:
types: [opened, reopened, closed]
issue_comment:
types: [created]

permissions: {}

concurrency:
group: issue-triage-${{ github.repository }}-${{ github.event.issue.number }}
cancel-in-progress: false
# Keep every pending event; the default drops all but the newest one.
queue: max

jobs:
triage:
name: Manage issue triage
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false

- name: Apply issue action
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const runIssueTriage = require('./.github/scripts/issue-triage.cjs');
await runIssueTriage({ github, context });
19 changes: 19 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,22 @@ jobs:
dist-install/*.zip
if-no-files-found: warn

issue-triage-checks:
name: Issue triage checks
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: "24"

- name: Check triage and duplicate commands
run: node --test .github/scripts/issue-triage.test.cjs

duplicate-code:
name: Duplicate Code
runs-on: ubuntu-latest
Expand Down Expand Up @@ -353,6 +369,7 @@ jobs:
- cargo-deny-advisories
- agent-sdk-check
- npm-package-layout
- issue-triage-checks
- duplicate-code
- codeql
steps:
Expand All @@ -369,6 +386,7 @@ jobs:
CARGO_DENY_ADVISORIES: ${{ needs.cargo-deny-advisories.result }}
AGENT_SDK: ${{ needs.agent-sdk-check.result }}
NPM_PACKAGE_LAYOUT: ${{ needs.npm-package-layout.result }}
ISSUE_TRIAGE_CHECKS: ${{ needs.issue-triage-checks.result }}
DUPLICATE_CODE: ${{ needs.duplicate-code.result }}
CODEQL: ${{ needs.codeql.result }}
run: |
Expand Down Expand Up @@ -399,6 +417,7 @@ jobs:
require_success "terminal-tests" "$TERMINAL_TESTS"
require_success "lockfile" "$LOCKFILE"
require_success "cargo-deny-policy" "$CARGO_DENY_POLICY"
require_success "issue-triage-checks" "$ISSUE_TRIAGE_CHECKS"
require_success "duplicate-code" "$DUPLICATE_CODE"
require_success "codeql" "$CODEQL"

Expand Down
Loading
Loading