From 92631f4d12a7f117d0318802d6350f24c7b24c27 Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 17 Oct 2025 09:54:56 +0200 Subject: [PATCH] feat: add support for pull request reviewer triggers (#12) Co-authored-by: Oleg Zaimkin --- README.md | 2 +- src/github/validation/trigger.ts | 14 + test/trigger-validation.test.ts | 451 ++++++++++++++++++++++++++++++- 3 files changed, 465 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ea23ab0..0e7c32f 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ jobs: | `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | | `disallowed_tools` | Tools that Claude should never use | No | "" | | `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" | -| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | +| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue and PR assignment | No | - | | `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | | `claude_git_name` | Git user.name for commits made by Claude | No | `Claude` | | `claude_git_email` | Git user.email for commits made by Claude | No | `claude@anthropic.com` | diff --git a/src/github/validation/trigger.ts b/src/github/validation/trigger.ts index 41eae4e..152095d 100644 --- a/src/github/validation/trigger.ts +++ b/src/github/validation/trigger.ts @@ -112,6 +112,20 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { ); return true; } + + // Check if trigger user is in requested reviewers (treat same as mention in text) + const triggerUser = triggerPhrase.replace(/^@/, ""); + const requestedReviewers = context.payload.pull_request.requested_reviewers || []; + const isReviewerRequested = requestedReviewers.some(reviewer => + 'login' in reviewer && reviewer.login === triggerUser + ); + + if (isReviewerRequested) { + console.log( + `Pull request has '${triggerUser}' as requested reviewer (treating as trigger)`, + ); + return true; + } } // Check for pull request review body trigger diff --git a/test/trigger-validation.test.ts b/test/trigger-validation.test.ts index 6d3ca3c..7b67cef 100644 --- a/test/trigger-validation.test.ts +++ b/test/trigger-validation.test.ts @@ -365,6 +365,343 @@ describe("checkContainsTrigger", () => { }); }); + describe("pull request reviewer trigger", () => { + it("should return true when PR has trigger user as requested reviewer (same as text mention)", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "opened", + isPR: true, + payload: { + action: "opened", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [ + { login: "claude", id: 1, type: "User" }, + { login: "other-reviewer", id: 2, type: "User" }, + ], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(true); + }); + + it("should return true for synchronized PR with trigger user as reviewer", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "synchronized", + isPR: true, + payload: { + action: "synchronized", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [ + { login: "claude", id: 1, type: "User" }, + ], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(true); + }); + + it("should return false when PR has no matching requested reviewers", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "opened", + isPR: true, + payload: { + action: "opened", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [ + { login: "other-reviewer", id: 2, type: "User" }, + ], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(false); + }); + + it("should handle trigger phrase without @ symbol", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "opened", + isPR: true, + payload: { + action: "opened", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [ + { login: "claude", id: 1, type: "User" }, + ], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "claude", // No @ symbol + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(true); + }); + }); + + it("should return true when PR has trigger user as requested reviewer for synchronized event", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "synchronized", + isPR: true, + payload: { + action: "synchronized", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [ + { login: "claude", id: 1, type: "User" }, + ], + requested_teams: [], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(true); + }); + + it("should return false when PR has no matching requested reviewers", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "opened", + isPR: true, + payload: { + action: "opened", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [ + { login: "other-reviewer", id: 2, type: "User" }, + ], + requested_teams: [], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(false); + }); + + it("should handle trigger phrase without @ symbol", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "opened", + isPR: true, + payload: { + action: "opened", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [ + { login: "claude", id: 1, type: "User" }, + ], + requested_teams: [], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "claude", // No @ symbol + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(true); + }); + + it("should handle empty requested_reviewers and requested_teams arrays", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "opened", + isPR: true, + payload: { + action: "opened", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [], + requested_teams: [], + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(false); + }); + + it("should handle missing requested_reviewers and requested_teams fields", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "opened", + isPR: true, + payload: { + action: "opened", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + // requested_reviewers and requested_teams are undefined + }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(false); + }); + }); + describe("comment trigger", () => { it("should return true for issue_comment with trigger phrase", () => { const context = mockIssueCommentContext; @@ -475,6 +812,119 @@ describe("checkContainsTrigger", () => { }); }); + describe("pull request review_requested action", () => { + it("should return true when trigger user is requested as reviewer", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "review_requested", + isPR: true, + payload: { + action: "review_requested", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [{ login: "claude", id: 1, type: "User" }], + requested_teams: [], + }, + requested_reviewer: { login: "claude", id: 1, type: "User" }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(true); + }); + + it("should return false when different user is requested as reviewer", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "review_requested", + isPR: true, + payload: { + action: "review_requested", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [{ login: "john", id: 2, type: "User" }], + requested_teams: [], + }, + requested_reviewer: { login: "john", id: 2, type: "User" }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "@claude", + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(false); + }); + + it("should handle trigger phrase without @ symbol", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "review_requested", + isPR: true, + payload: { + action: "review_requested", + pull_request: { + number: 123, + title: "Test PR", + body: "This PR fixes a bug", + created_at: "2023-01-01T00:00:00Z", + user: { login: "testuser" }, + requested_reviewers: [{ login: "claude", id: 1, type: "User" }], + requested_teams: [], + }, + requested_reviewer: { login: "claude", id: 1, type: "User" }, + } as unknown as PullRequestEvent, + inputs: { + mode: "tag", + triggerPhrase: "claude", // no @ symbol + assigneeTrigger: "", + labelTrigger: "", + directPrompt: "", + overridePrompt: "", + allowedTools: [], + disallowedTools: [], + customInstructions: "", + branchPrefix: "claude/", + useStickyComment: false, + additionalPermissions: new Map(), + useCommitSigning: false, + }, + }); + expect(checkContainsTrigger(context)).toBe(true); + }); + }); + describe("non-matching events", () => { it("should return false for non-matching event type", () => { const context = createMockContext({ @@ -485,7 +935,6 @@ describe("checkContainsTrigger", () => { expect(checkContainsTrigger(context)).toBe(false); }); }); -}); describe("escapeRegExp", () => { it("should escape special regex characters", () => {