feat: add support for pull request reviewer triggers (#12)

Co-authored-by: Oleg Zaimkin <oleg.zaimkin@developertools.com>
This commit is contained in:
Oleg
2025-10-17 09:54:56 +02:00
committed by GitHub
parent 225a4e6f3a
commit 92631f4d12
3 changed files with 465 additions and 2 deletions

View File

@@ -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` |

View File

@@ -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

View File

@@ -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", () => {