Skip to content

Commit d60fe68

Browse files
fix issue webhook trigger gating
1 parent 2fcb731 commit d60fe68

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

src/adapter.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,36 @@ function labelsIncludeTrigger(labels: JsonValue | undefined, policy: JsonObject)
404404
return false;
405405
}
406406

407+
function issueAssignedToBot(issue: JsonObject, policy: JsonObject): boolean {
408+
const wanted = new Set(((policy.bot_usernames as JsonValue[]) || []).map((username) => String(username).toLowerCase()));
409+
if (wanted.size === 0) {
410+
return false;
411+
}
412+
const assignees: JsonValue[] = [];
413+
if (issue.assignee) {
414+
assignees.push(issue.assignee);
415+
}
416+
if (Array.isArray(issue.assignees)) {
417+
assignees.push(...issue.assignees);
418+
}
419+
for (const assignee of assignees) {
420+
const login = typeof assignee === "object" && assignee !== null && !Array.isArray(assignee)
421+
? String((assignee as JsonObject).login || "")
422+
: String(assignee);
423+
if (wanted.has(login.toLowerCase())) {
424+
return true;
425+
}
426+
}
427+
return false;
428+
}
429+
430+
function triggerEnabled(policy: JsonObject, trigger: string): boolean {
431+
if (policy.enabled_triggers === undefined) {
432+
return true;
433+
}
434+
return new Set(((policy.enabled_triggers as JsonValue[]) || []).map((value) => String(value))).has(trigger);
435+
}
436+
407437
export function buildTaskFromEvent(
408438
eventName: string,
409439
deliveryId: string,
@@ -437,6 +467,12 @@ export function buildTaskFromEvent(
437467
if (eventName === "issue_comment") {
438468
const issue = (payload.issue as JsonObject | undefined) || {};
439469
const comment = (payload.comment as JsonObject | undefined) || {};
470+
if (payload.action !== "created") {
471+
return ignored(base, "unsupported_issue_comment_action");
472+
}
473+
if (!triggerEnabled(policy, "issue_comment.created")) {
474+
return ignored(base, "issue_comment_not_enabled");
475+
}
440476
if (!mentioned(comment.body, policy)) {
441477
return ignored(base, "issue_comment_without_mention");
442478
}
@@ -468,6 +504,12 @@ export function buildTaskFromEvent(
468504
if (eventName === "pull_request_review_comment") {
469505
const comment = (payload.comment as JsonObject | undefined) || {};
470506
const pullRequest = (payload.pull_request as JsonObject | undefined) || {};
507+
if (payload.action !== "created") {
508+
return ignored(base, "unsupported_pr_review_comment_action");
509+
}
510+
if (!triggerEnabled(policy, "pull_request_review_comment.created")) {
511+
return ignored(base, "pr_review_comment_not_enabled");
512+
}
471513
if (!mentioned(comment.body, policy)) {
472514
return ignored(base, "pr_review_comment_without_mention");
473515
}
@@ -492,14 +534,23 @@ export function buildTaskFromEvent(
492534
if (eventName === "issues") {
493535
const issue = (payload.issue as JsonObject | undefined) || {};
494536
const action = payload.action;
495-
if (action !== "assigned" && action !== "labeled" && action !== "opened") {
537+
if (action !== "assigned" && action !== "labeled") {
496538
return ignored(base, "unsupported_issue_action");
497539
}
540+
if (action === "assigned" && !triggerEnabled(policy, "issues.assigned")) {
541+
return ignored(base, "issue_assigned_not_enabled");
542+
}
543+
if (action === "assigned" && !issueAssignedToBot(issue, policy)) {
544+
return ignored(base, "issue_assigned_to_unmanaged_user");
545+
}
546+
if (action === "labeled" && !triggerEnabled(policy, "issues.labeled")) {
547+
return ignored(base, "issue_label_not_enabled");
548+
}
498549
if (action === "labeled" && !labelsIncludeTrigger(issue.labels, policy)) {
499550
return ignored(base, "issue_label_not_enabled");
500551
}
501552
Object.assign(base, {
502-
trigger: action === "assigned" ? "issue_assigned" : "issue_mention",
553+
trigger: "issue_assigned",
503554
task: {
504555
kind: "fix_issue",
505556
issue_number: Number(issue.number || 0),

tests/webhook-adapter.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ test("example policy routes a labeled issue to the configured familiar", () => {
335335
);
336336

337337
assert.equal(task.state, "queued");
338-
assert.equal(task.trigger, "issue_mention");
338+
assert.equal(task.trigger, "issue_assigned");
339339
assert.deepEqual(task.task, {
340340
kind: "fix_issue",
341341
issue_number: 42,
@@ -350,6 +350,49 @@ test("example policy routes a labeled issue to the configured familiar", () => {
350350
});
351351
});
352352

353+
test("new issue creation is ignored unless a supported trigger is enabled", () => {
354+
const task = buildTaskFromEvent(
355+
"issues",
356+
"delivery-issue-opened",
357+
{
358+
action: "opened",
359+
installation: {id: 123456},
360+
repository: {
361+
id: 987654321,
362+
full_name: "OpenCoven/example",
363+
clone_url: "https://github.com/OpenCoven/example.git",
364+
default_branch: "main",
365+
},
366+
issue: {
367+
number: 43,
368+
title: "Installer is slow",
369+
body: "A diagnostic issue, not a bot task.",
370+
labels: [],
371+
assignees: [],
372+
},
373+
} as JsonObject,
374+
{
375+
enabled_triggers: [
376+
"issues.labeled",
377+
"issue_comment.created",
378+
"pull_request_review_comment.created",
379+
],
380+
trigger_labels: ["coven:fix"],
381+
bot_usernames: ["coven-cody[bot]"],
382+
familiar: {
383+
id: "cody",
384+
display_name: "Cody",
385+
model: "openai/gpt-5.5",
386+
skills: [],
387+
},
388+
publication: {mode: "record_only"},
389+
} as JsonObject,
390+
);
391+
392+
assert.equal(task.state, "ignored");
393+
assert.equal(task.ignored_reason, "unsupported_issue_action");
394+
});
395+
353396
test("publication body links screenshot-style file mentions to GitHub blobs", () => {
354397
const body = publicationCommentBody(
355398
{

0 commit comments

Comments
 (0)