Repository: giselles-ai/giselle
Pull request #926: Add support for more GitHub webhook events
Author: login: toyamarinyon
Labels: name: codex
Created: 2025-05-22T09:18:13Z | Merged: 2025-05-23T02:03:04Z

## Summary
- listen for more GitHub webhook events
- trigger flows for issue closed, pull requests and comments

# Test Code Improvements

This PR significantly improves the test code for GitHub webhook event handlers. While the diff may appear large, the actual implementation changes are relatively minimal, with most of the additions (approximately 695 lines) being new test code.

```
 ✓ src/core/github/event-handlers.test.ts (17 tests) 6ms
   ✓ GitHub Event Handlers > handleIssueOpened > should handle issue opened event and add reaction 2ms
   ✓ GitHub Event Handlers > handleIssueOpened > should not run if event type doesn't match 0ms
   ✓ GitHub Event Handlers > handleIssueOpened > should not run if trigger event ID doesn't match 0ms
   ✓ GitHub Event Handlers > handleIssueCommentCreated > should handle issue comment created event with matching callsign 0ms
   ✓ GitHub Event Handlers > handleIssueCommentCreated > should not run if callsign doesn't match 0ms
   ✓ GitHub Event Handlers > handleIssueClosed > should handle issue closed event and add reaction 0ms
   ✓ GitHub Event Handlers > handleIssueClosed > should not run if trigger event ID doesn't match 0ms
   ✓ GitHub Event Handlers > handlePullRequestOpened > should handle pull request opened event and add reaction 0ms
   ✓ GitHub Event Handlers > handlePullRequestOpened > should not run if trigger event ID doesn't match 0ms
   ✓ GitHub Event Handlers > handlePullRequestClosed > should handle pull request closed event and add reaction 0ms
   ✓ GitHub Event Handlers > handlePullRequestClosed > should not run if trigger event ID doesn't match 0ms
   ✓ GitHub Event Handlers > handlePullRequestCommentCreated > should handle pull request comment created event with matching callsign 0ms
   ✓ GitHub Event Handlers > handlePullRequestCommentCreated > should not run if callsign doesn't match 0ms
   ✓ GitHub Event Handlers > handlePullRequestReadyForReview > should handle pull request ready for review event and add reaction 0ms
   ✓ GitHub Event Handlers > handlePullRequestReadyForReview > should not run if trigger event ID doesn't match 0ms
   ✓ GitHub Event Handlers > processEvent > should process event and run flow when handler returns shouldRun=true 1ms
   ✓ GitHub Event Handlers > processEvent > should return false when trigger is disabled 0ms
```

## Key Architectural Points

1. **Dependency Injection Pattern**: The event handlers are designed to allow explicit injection of dependencies, improving testability. This makes mocking and isolated testing much easier.

2. **Modular Event Handlers**: Each event handler is separated into individual functions with a central event processing mechanism. This improves code maintainability and extensibility.

3. **Enhanced Type Safety**: Ensures type compatibility with complex GitHub webhook events, improving overall type safety.

4. **Simplified Testing Strategy**: 
   - Created clear test cases for individual functions
   - Eliminated complex mocking mechanisms
   - Used `vi.fn()` for flexible testing strategies
   - Removed module-level mocking

5. **Simplified Event Handler Mocking**: Extracted the mock implementation of `ensureWebhookEvent` into a separate reusable function, reducing code duplication.

6. **Improved Test Coverage**: Implemented comprehensive tests for all GitHub event handlers.

## Development Approach

We adopted an approach of "making small, incremental changes" to improve the code gradually without increasing complexity. This approach results in a design that can flexibly accommodate future changes.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Expanded GitHub webhook event support to include additional issue and pull request events.
  - Centralized and modularized event processing for improved maintainability and flexibility.

- **Refactor**
  - Improved dependency injection for event handling, allowing for easier testing and integration.
  - Streamlined flow execution logic by consolidating event-specific handling into a dedicated processing function.

- **Tests**
  - Added comprehensive unit tests for all supported GitHub event handlers and event processing logic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/giselle-engine/src/core/github/event-handlers.test.ts b/packages/giselle-engine/src/core/github/event-handlers.test.ts
new file mode 100644
index 0000000000..ad44c97f96
--- /dev/null
+++ b/packages/giselle-engine/src/core/github/event-handlers.test.ts
@@ -0,0 +1,695 @@
+import type { FlowTrigger } from "@giselle-sdk/data-type";
+import { FlowTriggerId } from "@giselle-sdk/data-type";
+import type {
+	GitHubAuthConfig,
+	WebhookEvent,
+	WebhookEventName,
+	ensureWebhookEvent,
+} from "@giselle-sdk/github-tool";
+import { createStorage } from "unstorage";
+import memoryDriver from "unstorage/drivers/memory";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { GiselleEngineContext } from "../types";
+import {
+	type EventHandlerArgs,
+	type EventHandlerDependencies,
+	handleIssueClosed,
+	handleIssueCommentCreated,
+	handleIssueOpened,
+	handlePullRequestClosed,
+	handlePullRequestCommentCreated,
+	handlePullRequestOpened,
+	handlePullRequestReadyForReview,
+	processEvent,
+} from "./event-handlers";
+
+// Helper function to create a mock implementation for ensureWebhookEvent
+function createEnsureWebhookEventMock(shouldMatch = true) {
+	return vi
+		.fn()
+		.mockImplementation(
+			<T extends WebhookEventName>(
+				event: WebhookEvent<WebhookEventName>,
+				expectedName: T,
+			): event is WebhookEvent<T> => shouldMatch,
+		) as unknown as typeof ensureWebhookEvent;
+}
+
+// Define test-specific types
+type TestWebhookEvent = WebhookEvent<WebhookEventName>;
+
+// Generate a valid test trigger ID
+const mockFlowTriggerId = FlowTriggerId.generate();
+
+describe("GitHub Event Handlers", () => {
+	// Common test data
+	let testDeps: EventHandlerDependencies;
+	let baseEventArgs: Omit<EventHandlerArgs<WebhookEventName>, "event">;
+
+	beforeEach(() => {
+		vi.resetAllMocks();
+
+		// Simple dependency mocks with correct type implementations
+		testDeps = {
+			addReaction: vi.fn().mockResolvedValue(undefined),
+			ensureWebhookEvent: createEnsureWebhookEventMock(),
+			runFlow: vi.fn().mockResolvedValue(undefined),
+			parseCommand: vi
+				.fn()
+				.mockReturnValue({ callsign: "giselle", content: "help me" }),
+		};
+
+		// Setup base arguments
+		baseEventArgs = {
+			context: {
+				storage: createStorage({ driver: memoryDriver() }),
+				llmProviders: [],
+				integrationConfigs: {
+					github: {
+						auth: {
+							strategy: "personal-access-token",
+							personalAccessToken: "test-token",
+						},
+						authV2: {
+							appId: "app-id",
+							privateKey: "private-key",
+							clientId: "client-id",
+							clientSecret: "client-secret",
+							webhookSecret: "webhook-secret",
+						},
+					},
+				},
+			} as unknown as GiselleEngineContext,
+			trigger: {
+				id: mockFlowTriggerId,
+				workspaceId: "wrks-test",
+				nodeId: "nd-test",
+				configuration: {
+					provider: "github",
+					event: {
+						id: "github.issue.created",
+						conditions: {
+							callsign: "giselle",
+						},
+					},
+					installationId: 12345,
+					repositoryNodeId: "repo-node-id",
+				},
+				enable: true,
+			} as FlowTrigger,
+			authConfig: {
+				strategy: "app-installation",
+				appId: "app-id",
+				privateKey: "private-key",
+				installationId: 12345,
+			} as GitHubAuthConfig,
+			deps: testDeps,
+		};
+	});
+
+	describe("handleIssueOpened", () => {
+		it("should handle issue opened event and add reaction", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issues.opened",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							issue: { node_id: "issue-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+
+			// Act
+			const result = await handleIssueOpened(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(true);
+			expect(args.deps.addReaction).toHaveBeenCalledWith({
+				id: "issue-node-id",
+				content: "EYES",
+				authConfig: args.authConfig,
+			});
+		});
+
+		it("should not run if event type doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issues.opened",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.deps = {
+				...args.deps,
+				ensureWebhookEvent: createEnsureWebhookEventMock(false),
+			};
+
+			// Act
+			const result = await handleIssueOpened(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+
+		it("should not run if trigger event ID doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issues.opened",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "manual";
+
+			// Act
+			const result = await handleIssueOpened(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+	});
+
+	describe("handleIssueCommentCreated", () => {
+		it("should handle issue comment created event with matching callsign", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issue_comment.created",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							comment: {
+								node_id: "comment-node-id",
+								body: "@giselle help me",
+							},
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.issue_comment.created";
+
+			// Act
+			const result = await handleIssueCommentCreated(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(true);
+			expect(args.deps.parseCommand).toHaveBeenCalledWith("@giselle help me");
+			expect(args.deps.addReaction).toHaveBeenCalledWith({
+				id: "comment-node-id",
+				content: "EYES",
+				authConfig: args.authConfig,
+			});
+		});
+
+		it("should not run if callsign doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issue_comment.created",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							comment: {
+								node_id: "comment-node-id",
+								body: "@someone help me",
+							},
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.issue_comment.created";
+			args.deps = {
+				...args.deps,
+				parseCommand: vi.fn().mockReturnValue({
+					callsign: "someone",
+					content: "help me",
+				}),
+			};
+
+			// Act
+			const result = await handleIssueCommentCreated(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+	});
+
+	describe("handleIssueClosed", () => {
+		it("should handle issue closed event and add reaction", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issues.closed",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							issue: { node_id: "issue-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.issue.closed";
+
+			// Act
+			const result = await handleIssueClosed(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(true);
+			expect(args.deps.addReaction).toHaveBeenCalledWith({
+				id: "issue-node-id",
+				content: "EYES",
+				authConfig: args.authConfig,
+			});
+		});
+
+		it("should not run if trigger event ID doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issues.closed",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							issue: { node_id: "issue-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.issue.created";
+
+			// Act
+			const result = await handleIssueClosed(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+	});
+
+	describe("handlePullRequestOpened", () => {
+		it("should handle pull request opened event and add reaction", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "pull_request.opened",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							pull_request: { node_id: "pr-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.pull_request.opened";
+
+			// Act
+			const result = await handlePullRequestOpened(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(true);
+			expect(args.deps.addReaction).toHaveBeenCalledWith({
+				id: "pr-node-id",
+				content: "EYES",
+				authConfig: args.authConfig,
+			});
+		});
+
+		it("should not run if trigger event ID doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "pull_request.opened",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							pull_request: { node_id: "pr-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.issue.created";
+
+			// Act
+			const result = await handlePullRequestOpened(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+	});
+
+	describe("handlePullRequestClosed", () => {
+		it("should handle pull request closed event and add reaction", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "pull_request.closed",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							pull_request: { node_id: "pr-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.pull_request.closed";
+
+			// Act
+			const result = await handlePullRequestClosed(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(true);
+			expect(args.deps.addReaction).toHaveBeenCalledWith({
+				id: "pr-node-id",
+				content: "EYES",
+				authConfig: args.authConfig,
+			});
+		});
+
+		it("should not run if trigger event ID doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "pull_request.closed",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							pull_request: { node_id: "pr-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.issue.created";
+
+			// Act
+			const result = await handlePullRequestClosed(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+	});
+
+	describe("handlePullRequestCommentCreated", () => {
+		it("should handle pull request comment created event with matching callsign", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issue_comment.created",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							issue: {
+								pull_request: {},
+							},
+							comment: {
+								node_id: "comment-node-id",
+								body: "@giselle help me",
+							},
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id =
+				"github.pull_request_comment.created";
+
+			// Act
+			const result = await handlePullRequestCommentCreated(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(true);
+			expect(args.deps.parseCommand).toHaveBeenCalledWith("@giselle help me");
+			expect(args.deps.addReaction).toHaveBeenCalledWith({
+				id: "comment-node-id",
+				content: "EYES",
+				authConfig: args.authConfig,
+			});
+		});
+
+		it("should not run if callsign doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "issue_comment.created",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							issue: {
+								pull_request: {},
+							},
+							comment: {
+								node_id: "comment-node-id",
+								body: "@someone help me",
+							},
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id =
+				"github.pull_request_comment.created";
+			args.deps = {
+				...args.deps,
+				parseCommand: vi.fn().mockReturnValue({
+					callsign: "someone",
+					content: "help me",
+				}),
+			};
+
+			// Act
+			const result = await handlePullRequestCommentCreated(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+	});
+
+	describe("handlePullRequestReadyForReview", () => {
+		it("should handle pull request ready for review event and add reaction", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "pull_request.ready_for_review",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							pull_request: { node_id: "pr-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id =
+				"github.pull_request.ready_for_review";
+
+			// Act
+			const result = await handlePullRequestReadyForReview(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(true);
+			expect(args.deps.addReaction).toHaveBeenCalledWith({
+				id: "pr-node-id",
+				content: "EYES",
+				authConfig: args.authConfig,
+			});
+		});
+
+		it("should not run if trigger event ID doesn't match", async () => {
+			// Arrange
+			const args = {
+				...baseEventArgs,
+				event: {
+					name: "pull_request.ready_for_review",
+					data: {
+						payload: {
+							installation: { id: 12345 },
+							repository: { node_id: "repo-node-id" },
+							pull_request: { node_id: "pr-node-id" },
+						},
+					},
+				} as TestWebhookEvent,
+			};
+			args.trigger.configuration.event.id = "github.issue.created";
+
+			// Act
+			const result = await handlePullRequestReadyForReview(args);
+
+			// Assert
+			expect(result.shouldRun).toBe(false);
+			expect(args.deps.addReaction).not.toHaveBeenCalled();
+		});
+	});
+
+	describe("processEvent", () => {
+		it("should process event and run flow when handler returns shouldRun=true", async () => {
+			// Arrange
+			const event = {
+				name: "issues.opened",
+				data: {
+					payload: {
+						installation: { id: 12345 },
+						repository: { node_id: "repo-node-id" },
+						issue: { node_id: "issue-node-id" },
+					},
+				},
+			} as TestWebhookEvent;
+
+			const trigger = {
+				id: mockFlowTriggerId,
+				workspaceId: "wrks-test",
+				nodeId: "nd-test",
+				enable: true,
+				configuration: {
+					provider: "github",
+					event: {
+						id: "github.issue.created",
+					},
+					installationId: 12345,
+					repositoryNodeId: "repo-node-id",
+				},
+			} as FlowTrigger;
+
+			const createAuthConfig = vi.fn().mockReturnValue({
+				strategy: "app-installation",
+				appId: "app-id",
+				privateKey: "private-key",
+				installationId: 12345,
+			});
+
+			// Act
+			const result = await processEvent({
+				event,
+				context: {
+					llmProviders: [],
+					storage: createStorage({ driver: memoryDriver() }),
+					integrationConfigs: {
+						github: {
+							auth: {
+								strategy: "personal-access-token",
+								personalAccessToken: "test-token",
+							},
+							authV2: {
+								appId: "app-id",
+								privateKey: "private-key",
+								clientId: "client-id",
+								clientSecret: "client-secret",
+								webhookSecret: "webhook-secret",
+							},
+						},
+					},
+				} as unknown as GiselleEngineContext,
+				trigger,
+				createAuthConfig,
+				deps: testDeps,
+			});
+
+			// Assert
+			expect(result).toBe(true);
+			expect(testDeps.runFlow).toHaveBeenCalledWith({
+				context: expect.anything(),
+				triggerId: mockFlowTriggerId,
+				payload: event,
+			});
+		});
+
+		it("should return false when trigger is disabled", async () => {
+			// Arrange
+			const event = {
+				name: "issues.opened",
+				data: {
+					payload: {
+						installation: { id: 12345 },
+						repository: { node_id: "repo-node-id" },
+					},
+				},
+			} as TestWebhookEvent;
+
+			const trigger = {
+				id: mockFlowTriggerId,
+				workspaceId: "wrks-test",
+				nodeId: "nd-test",
+				enable: false,
+				configuration: {
+					provider: "github",
+					event: {
+						id: "github.issue.created",
+					},
+					installationId: 12345,
+					repositoryNodeId: "repo-node-id",
+				},
+			} as FlowTrigger;
+
+			// Act
+			const result = await processEvent({
+				event,
+				context: {
+					llmProviders: [],
+					storage: createStorage({ driver: memoryDriver() }),
+					integrationConfigs: {
+						github: {
+							auth: {
+								strategy: "personal-access-token",
+								personalAccessToken: "test-token",
+							},
+							authV2: {
+								appId: "app-id",
+								privateKey: "private-key",
+								clientId: "client-id",
+								clientSecret: "client-secret",
+								webhookSecret: "webhook-secret",
+							},
+						},
+					},
+				} as unknown as GiselleEngineContext,
+				trigger,
+				createAuthConfig: vi.fn(),
+				deps: testDeps,
+			});
+
+			// Assert
+			expect(result).toBe(false);
+			expect(testDeps.runFlow).not.toHaveBeenCalled();
+		});
+	});
+});
diff --git a/packages/giselle-engine/src/core/github/event-handlers.ts b/packages/giselle-engine/src/core/github/event-handlers.ts
new file mode 100644
index 0000000000..069ff5edb2
--- /dev/null
+++ b/packages/giselle-engine/src/core/github/event-handlers.ts
@@ -0,0 +1,275 @@
+import type { FlowTrigger } from "@giselle-sdk/data-type";
+import type {
+	GitHubAuthConfig,
+	WebhookEvent,
+	WebhookEventName,
+	addReaction,
+	ensureWebhookEvent,
+} from "@giselle-sdk/github-tool";
+import type { runFlow } from "../flows";
+import type { GiselleEngineContext } from "../types";
+import type { parseCommand } from "./utils";
+
+export interface EventHandlerDependencies {
+	addReaction: typeof addReaction;
+	ensureWebhookEvent: typeof ensureWebhookEvent;
+	runFlow: typeof runFlow;
+	parseCommand: typeof parseCommand;
+}
+
+export type EventHandlerArgs<TEventName extends WebhookEventName> = {
+	event: WebhookEvent<TEventName>;
+	context: GiselleEngineContext;
+	trigger: FlowTrigger;
+	authConfig: GitHubAuthConfig;
+	deps: EventHandlerDependencies;
+};
+
+export type EventHandlerResult = {
+	shouldRun: boolean;
+};
+
+export async function handleIssueOpened<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.opened") ||
+		args.trigger.configuration.event.id !== "github.issue.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueClosed<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.closed") ||
+		args.trigger.configuration.event.id !== "github.issue.closed"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
+		args.trigger.configuration.event.id !== "github.issue_comment.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const comment = args.event.data.payload.comment;
+	if (!comment) {
+		return { shouldRun: false };
+	}
+
+	const command = args.deps.parseCommand(comment.body);
+	const conditions =
+		args.trigger.configuration.event.id === "github.issue_comment.created"
+			? args.trigger.configuration.event.conditions
+			: undefined;
+
+	if (command?.callsign !== conditions?.callsign) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: comment.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
+		args.trigger.configuration.event.id !==
+			"github.pull_request_comment.created"
+	) {
+		return { shouldRun: false };
+	}
+	if (args.event.data.payload.issue?.pull_request === null) {
+		return { shouldRun: false };
+	}
+
+	const command = args.deps.parseCommand(args.event.data.payload.comment.body);
+	const conditions =
+		args.trigger.configuration.event.id ===
+		"github.pull_request_comment.created"
+			? args.trigger.configuration.event.conditions
+			: undefined;
+
+	if (command?.callsign !== conditions?.callsign) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: args.event.data.payload.comment.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestOpened<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "pull_request.opened") ||
+		args.trigger.configuration.event.id !== "github.pull_request.opened"
+	) {
+		return { shouldRun: false };
+	}
+
+	const pullRequest = args.event.data.payload.pull_request;
+	if (!pullRequest) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: pullRequest.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestReadyForReview<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(
+			args.event,
+			"pull_request.ready_for_review",
+		) ||
+		args.trigger.configuration.event.id !==
+			"github.pull_request.ready_for_review"
+	) {
+		return { shouldRun: false };
+	}
+
+	const pullRequest = args.event.data.payload.pull_request;
+	if (!pullRequest) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: pullRequest.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestClosed<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "pull_request.closed") ||
+		args.trigger.configuration.event.id !== "github.pull_request.closed"
+	) {
+		return { shouldRun: false };
+	}
+
+	const pullRequest = args.event.data.payload.pull_request;
+	if (!pullRequest) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: pullRequest.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export const eventHandlers = [
+	handleIssueOpened,
+	handleIssueClosed,
+	handleIssueCommentCreated,
+	handlePullRequestCommentCreated,
+	handlePullRequestOpened,
+	handlePullRequestReadyForReview,
+	handlePullRequestClosed,
+];
+
+export async function processEvent<TEventName extends WebhookEventName>(
+	args: Omit<
+		EventHandlerArgs<TEventName>,
+		"trigger" | "authConfig" | "deps"
+	> & {
+		trigger: EventHandlerArgs<TEventName>["trigger"];
+		createAuthConfig: (installationId: number) => GitHubAuthConfig;
+		deps: EventHandlerDependencies;
+	},
+): Promise<boolean> {
+	if (
+		!args.trigger.enable ||
+		args.trigger.configuration.provider !== "github"
+	) {
+		return false;
+	}
+
+	const installationId = args.trigger.configuration.installationId;
+	const authConfig = args.createAuthConfig(installationId);
+
+	// Merge provided dependencies with defaults
+	const deps = { ...args.deps };
+
+	for (const handler of eventHandlers) {
+		const result = await handler({
+			...args,
+			authConfig,
+			deps,
+		});
+
+		if (result.shouldRun) {
+			await deps.runFlow({
+				context: args.context,
+				triggerId: args.trigger.id,
+				payload: args.event,
+			});
+			return true;
+		}
+	}
+
+	return false;
+}
diff --git a/packages/giselle-engine/src/core/github/handle-webhook-v2.ts b/packages/giselle-engine/src/core/github/handle-webhook-v2.ts
index 1a2e090470..3e4cab5ea5 100644
--- a/packages/giselle-engine/src/core/github/handle-webhook-v2.ts
+++ b/packages/giselle-engine/src/core/github/handle-webhook-v2.ts
@@ -6,14 +6,22 @@ import {
 	ensureWebhookEvent,
 	handleWebhook,
 } from "@giselle-sdk/github-tool";
-import type { Storage } from "unstorage";
 import { runFlow } from "../flows";
 import { getFlowTrigger } from "../flows/utils";
 import { getGitHubRepositoryIntegrationIndex } from "../integrations/utils";
 import type { GiselleEngineContext } from "../types";
+import { type EventHandlerDependencies, processEvent } from "./event-handlers";
 import { parseCommand } from "./utils";
 
-const events: WebhookEventName[] = ["issues.opened", "issue_comment.created"];
+const events: WebhookEventName[] = [
+	"issues.opened",
+	"issues.closed",
+	"issue_comment.created",
+	"pull_request_review_comment.created",
+	"pull_request.opened",
+	"pull_request.ready_for_review",
+	"pull_request.closed",
+];
 
 export async function handleGitHubWebhookV2(args: {
 	context: GiselleEngineContext;
@@ -29,6 +37,14 @@ export async function handleGitHubWebhookV2(args: {
 		process({
 			event,
 			context: args.context,
+			deps: {
+				getFlowTrigger,
+				getGitHubRepositoryIntegrationIndex,
+				addReaction,
+				ensureWebhookEvent,
+				runFlow,
+				parseCommand,
+			},
 		});
 
 	const handlers: Partial<
@@ -74,83 +90,61 @@ function hasRequiredPayloadProps(event: unknown): event is {
 		"id" in event.data.payload.installation
 	);
 }
+export interface ProcessDeps {
+	getFlowTrigger: typeof getFlowTrigger;
+	getGitHubRepositoryIntegrationIndex: typeof getGitHubRepositoryIntegrationIndex;
+}
+
 async function process<TEventName extends WebhookEventName>(args: {
 	event: WebhookEvent<TEventName>;
 	context: GiselleEngineContext;
+	deps: ProcessDeps & EventHandlerDependencies;
 }) {
 	if (!hasRequiredPayloadProps(args.event)) {
 		return;
 	}
-	const installationId = args.event.data.payload.installation.id;
-	const githubRepositoryIntegration = await getGitHubRepositoryIntegrationIndex(
-		{
+
+	const githubRepositoryIntegration =
+		await args.deps.getGitHubRepositoryIntegrationIndex({
 			storage: args.context.storage,
 			repositoryNodeId: args.event.data.payload.repository.node_id,
-		},
-	);
+		});
 
 	if (githubRepositoryIntegration === undefined) {
 		return;
 	}
+
 	await Promise.all(
 		githubRepositoryIntegration.flowTriggerIds.map(async (flowTriggerId) => {
-			const trigger = await getFlowTrigger({
+			const trigger = await args.deps.getFlowTrigger({
 				storage: args.context.storage,
 				flowTriggerId,
 			});
 
-			if (!trigger.enable || trigger.configuration.provider !== "github") {
-				return;
-			}
-
 			const githubAuthV2 = args.context.integrationConfigs?.github?.authV2;
 			if (githubAuthV2 === undefined) {
 				throw new Error("GitHub authV2 configuration is missing");
 			}
-			const authConfig = {
+
+			const createAuthConfig = (installationId: number): GitHubAuthConfig => ({
 				strategy: "app-installation",
 				appId: githubAuthV2.appId,
 				privateKey: githubAuthV2.privateKey,
 				installationId,
-			} satisfies GitHubAuthConfig;
-
-			let run = false;
-			if (
-				ensureWebhookEvent(args.event, "issues.opened") &&
-				trigger.configuration.event.id === "github.issue.created"
-			) {
-				run = true;
-				await addReaction({
-					id: args.event.data.payload.issue.node_id,
-					content: "EYES",
-					authConfig,
-				});
-			}
-			if (
-				ensureWebhookEvent(args.event, "issue_comment.created") &&
-				trigger.configuration.event.id === "github.issue_comment.created"
-			) {
-				const command = parseCommand(args.event.data.payload.comment.body);
-				if (
-					command?.callsign !== trigger.configuration.event.conditions.callsign
-				) {
-					return;
-				}
+			});
 
-				run = true;
-				await addReaction({
-					id: args.event.data.payload.comment.node_id,
-					content: "EYES",
-					authConfig,
-				});
-			}
-			if (run) {
-				runFlow({
-					context: args.context,
-					triggerId: trigger.id,
-					payload: args.event,
-				});
-			}
+			await processEvent({
+				event: args.event,
+				context: args.context,
+				trigger,
+				createAuthConfig,
+				deps: {
+					addReaction: args.deps.addReaction,
+					ensureWebhookEvent: args.deps.ensureWebhookEvent,
+					runFlow: args.deps.runFlow,
+					parseCommand: args.deps.parseCommand,
+				},
+			});
 		}),
 	);
 }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds support for additional GitHub webhook events by extracting per-event logic into a new `event-handlers.ts` module and delegating all processing through a shared `processEvent` function.

- Expanded the set of events handled in `handle-webhook-v2.ts` (now including issue closed, PR opened/closed, comments, ready for review).
- Introduced `event-handlers.ts` with individual handlers for each event plus a unified `processEvent` orchestration.
- Updated `handleGitHubWebhookV2` to inject dependencies and delegate to `processEvent`.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File                                                        | Description                                                                                               |
|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| packages/giselle-engine/src/core/github/handle-webhook-v2.ts | Broadened the events list and refactored inline logic to call `processEvent` with injected dependencies. |
| packages/giselle-engine/src/core/github/event-handlers.ts   | Added modular handler functions for each GitHub event and a central `processEvent` driver.               |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**packages/giselle-engine/src/core/github/event-handlers.ts:119**
* The trigger configuration ID for pull request comments is inconsistent with the event key. Update it to "github.pull_request_review_comment.created" to align with the supported event.
```
args.trigger.configuration.event.id !== "github.pull_request_comment.created"
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T01:40:03Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/giselle-engine/src/core/github/event-handlers.ts (1)</summary><blockquote>

`152-170`: **Repetitive reaction-handling blocks could be DRYed up**  

All handlers repeat identical logic:

1. Gate-check event name & trigger id  
2. Extract a node_id  
3. Add the 👀 reaction  
4. `return { shouldRun: true }`

Consider extracting a small helper, e.g. `reactAndRun`, or building a mapping `{eventName, triggerId, nodeSelector}`, which would cut >50 LoC and make future event additions safer.  
This is an optional refactor, but it will pay off as the list of supported events grows.

</blockquote></details>
<details>
<summary>packages/giselle-engine/src/core/github/event-handlers.test.ts (2)</summary><blockquote>

`195-208`: **Mocked `parseCommand` diverges from real parser semantics**

The production `parseCommand` expects slash-prefixed commands (e.g. `/giselle help`), yet the test stubs it to parse **`@giselle` mentions**.  
This hides potential regressions in command parsing and could produce false confidence.

Consider:

```ts
// In tests, mimic the real contract
parseCommand: vi.fn().mockReturnValue(null) // default
```

and build test inputs that match the real syntax, or keep the stub but add a **note** explaining the intentional deviation.

---

`110-137`: **Shallow-copy of `baseEventArgs` shares nested objects**

`{ ...baseEventArgs, event: {...} }` only clones the *first level*; nested objects (`trigger`, `deps`) are still shared.  
Mutations like `args.trigger.configuration.event.id = "manual"` alter the original and might leak between tests if `beforeEach` ever stops deep-recreating those objects.

Recommend using a deep clone (`structuredClone`, lodash `cloneDeep`, or explicit reconstruction) to future-proof the suite.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7c739f0ee28eeaa4957258ff40bcbfcf6537cfd1 and 12decfc98d428d086e4250ba95a0d4268450cd30.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `packages/giselle-engine/src/core/github/event-handlers.test.ts` (1 hunks)
* `packages/giselle-engine/src/core/github/event-handlers.ts` (1 hunks)
* `packages/giselle-engine/src/core/github/handle-webhook-v2.ts` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>packages/giselle-engine/src/core/github/handle-webhook-v2.ts (6)</summary><blockquote>

<details>
<summary>packages/giselle-engine/src/core/flows/utils.ts (1)</summary>

* `getFlowTrigger` (21-38)

</details>
<details>
<summary>packages/giselle-engine/src/core/integrations/utils.ts (1)</summary>

* `getGitHubRepositoryIntegrationIndex` (13-24)

</details>
<details>
<summary>packages/github-tool/src/webhooks.ts (1)</summary>

* `WebhookEvent` (14-17)

</details>
<details>
<summary>packages/giselle-engine/src/core/types.ts (1)</summary>

* `GiselleEngineContext` (11-25)

</details>
<details>
<summary>packages/giselle-engine/src/core/github/event-handlers.ts (2)</summary>

* `EventHandlerDependencies` (13-18)
* `processEvent` (234-275)

</details>
<details>
<summary>packages/github-tool/src/types.ts (1)</summary>

* `GitHubAuthConfig` (19-22)

</details>

</blockquote></details>
<details>
<summary>packages/giselle-engine/src/core/github/event-handlers.ts (6)</summary><blockquote>

<details>
<summary>packages/github-tool/src/reactions.ts (1)</summary>

* `addReaction` (19-26)

</details>
<details>
<summary>packages/github-tool/src/webhooks.ts (2)</summary>

* `ensureWebhookEvent` (19-24)
* `WebhookEvent` (14-17)

</details>
<details>
<summary>packages/giselle-engine/src/core/flows/run-flow.ts (1)</summary>

* `runFlow` (17-104)

</details>
<details>
<summary>packages/giselle-engine/src/core/github/utils.ts (1)</summary>

* `parseCommand` (269-292)

</details>
<details>
<summary>packages/giselle-engine/src/core/types.ts (1)</summary>

* `GiselleEngineContext` (11-25)

</details>
<details>
<summary>packages/github-tool/src/types.ts (1)</summary>

* `GitHubAuthConfig` (19-22)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>packages/giselle-engine/src/core/github/handle-webhook-v2.ts (1)</summary>

`16-24`: **Event list contains an unhandled type**

`"pull_request_review_comment.created"` is declared in `events`, yet there is **no corresponding handler** in `event-handlers.ts`.  
Dispatching it will always yield `false`, wasting a round-trip and possibly confusing maintainers later.

Either  
1. add a proper `handlePullRequestReviewCommentCreated` handler, or  
2. drop the event from this array until support is implemented.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T01:41:16Z
user: login: shige
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T01:44:07Z
user: login: shige
body: Thank you! 👍 

@toyamarinyon Please check https://github.com/giselles-ai/giselle/pull/926#discussion_r2103647629 before merge this pr!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T01:50:27Z
user: login: toyamarinyon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:02:54Z

Review comments:
diff_hunk: @@ -0,0 +1,275 @@
+import type { FlowTrigger } from "@giselle-sdk/data-type";
+import type {
+	GitHubAuthConfig,
+	WebhookEvent,
+	WebhookEventName,
+	addReaction,
+	ensureWebhookEvent,
+} from "@giselle-sdk/github-tool";
+import type { runFlow } from "../flows";
+import type { GiselleEngineContext } from "../types";
+import type { parseCommand } from "./utils";
+
+export interface EventHandlerDependencies {
+	addReaction: typeof addReaction;
+	ensureWebhookEvent: typeof ensureWebhookEvent;
+	runFlow: typeof runFlow;
+	parseCommand: typeof parseCommand;
+}
+
+export type EventHandlerArgs<TEventName extends WebhookEventName> = {
+	event: WebhookEvent<TEventName>;
+	context: GiselleEngineContext;
+	trigger: FlowTrigger;
+	authConfig: GitHubAuthConfig;
+	deps: EventHandlerDependencies;
+};
+
+export type EventHandlerResult = {
+	shouldRun: boolean;
+};
+
+export async function handleIssueOpened<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.opened") ||
+		args.trigger.configuration.event.id !== "github.issue.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueClosed<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.closed") ||
+		args.trigger.configuration.event.id !== "github.issue.closed"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
+		args.trigger.configuration.event.id !== "github.issue_comment.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const comment = args.event.data.payload.comment;
+	if (!comment) {
+		return { shouldRun: false };
+	}
+
+	const command = args.deps.parseCommand(comment.body);
+	const conditions =
+		args.trigger.configuration.event.id === "github.issue_comment.created"
+			? args.trigger.configuration.event.conditions
+			: undefined;
+
+	if (command?.callsign !== conditions?.callsign) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: comment.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
path: packages/giselle-engine/src/core/github/event-handlers.ts
user: login: Copilot
body: The pull request comment handler is filtering using the issue comment event name. It should use "pull_request_review_comment.created" (to match the expanded events list).
```suggestion
		!args.deps.ensureWebhookEvent(args.event, "pull_request_review_comment.created") ||
```
created_at: 2025-05-23T01:40:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,275 @@
+import type { FlowTrigger } from "@giselle-sdk/data-type";
+import type {
+	GitHubAuthConfig,
+	WebhookEvent,
+	WebhookEventName,
+	addReaction,
+	ensureWebhookEvent,
+} from "@giselle-sdk/github-tool";
+import type { runFlow } from "../flows";
+import type { GiselleEngineContext } from "../types";
+import type { parseCommand } from "./utils";
+
+export interface EventHandlerDependencies {
+	addReaction: typeof addReaction;
+	ensureWebhookEvent: typeof ensureWebhookEvent;
+	runFlow: typeof runFlow;
+	parseCommand: typeof parseCommand;
+}
+
+export type EventHandlerArgs<TEventName extends WebhookEventName> = {
+	event: WebhookEvent<TEventName>;
+	context: GiselleEngineContext;
+	trigger: FlowTrigger;
+	authConfig: GitHubAuthConfig;
+	deps: EventHandlerDependencies;
+};
+
+export type EventHandlerResult = {
+	shouldRun: boolean;
+};
+
+export async function handleIssueOpened<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.opened") ||
+		args.trigger.configuration.event.id !== "github.issue.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueClosed<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.closed") ||
+		args.trigger.configuration.event.id !== "github.issue.closed"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
+		args.trigger.configuration.event.id !== "github.issue_comment.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const comment = args.event.data.payload.comment;
+	if (!comment) {
+		return { shouldRun: false };
+	}
+
+	const command = args.deps.parseCommand(comment.body);
+	const conditions =
+		args.trigger.configuration.event.id === "github.issue_comment.created"
+			? args.trigger.configuration.event.conditions
+			: undefined;
+
+	if (command?.callsign !== conditions?.callsign) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: comment.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
+		args.trigger.configuration.event.id !==
+			"github.pull_request_comment.created"
+	) {
+		return { shouldRun: false };
+	}
+	if (args.event.data.payload.issue?.pull_request === null) {
+		return { shouldRun: false };
+	}
+
+	const command = args.deps.parseCommand(args.event.data.payload.comment.body);
path: packages/giselle-engine/src/core/github/event-handlers.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`pull_request_comment` guard misses the `undefined` case**  

`issue?.pull_request` is `undefined` for issue comments and **only present (object) for PR comments**.  
The current strict-equality check (`=== null`) lets `undefined` slip through, so an *issue* comment without the `pull_request` key would be processed as a PR comment.

```diff
- if (args.event.data.payload.issue?.pull_request === null) {
+ // Treat as PR-comment only when the key exists (truthy).
+ if (!args.event.data.payload.issue?.pull_request) {
     return { shouldRun: false };
 }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    // Treat as PR-comment only when the key exists (truthy).
    if (!args.event.data.payload.issue?.pull_request) {
        return { shouldRun: false };
    }

    const command = args.deps.parseCommand(args.event.data.payload.comment.body);
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/giselle-engine/src/core/github/event-handlers.ts around lines 124 to
128, the check for pull_request uses strict equality against null, which misses
the case when pull_request is undefined. Update the condition to check if
pull_request is either null or undefined (e.g., using == null or explicitly
checking both) to correctly exclude issue comments and only process pull request
comments.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T01:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,83 +90,61 @@ function hasRequiredPayloadProps(event: unknown): event is {
 		"id" in event.data.payload.installation
 	);
 }
+export interface ProcessDeps {
+	getFlowTrigger: typeof getFlowTrigger;
+	getGitHubRepositoryIntegrationIndex: typeof getGitHubRepositoryIntegrationIndex;
+}
+
path: packages/giselle-engine/src/core/github/handle-webhook-v2.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`ProcessDeps` no longer represents the full dependency surface**

`process` now requires the union of `ProcessDeps` **and** `EventHandlerDependencies`, but `ProcessDeps` itself only lists two functions.  
If new functions are added to `EventHandlerDependencies`, forgetting to pipe them through here will yield a silent runtime `undefined` rather than a compile-time error.

A safer pattern is:

```ts
type ProcessDeps = EventHandlerDependencies & {
    getFlowTrigger: typeof getFlowTrigger;
    getGitHubRepositoryIntegrationIndex: typeof getGitHubRepositoryIntegrationIndex;
};
```

so the compiler enforces completeness.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/giselle-engine/src/core/github/handle-webhook-v2.ts around lines 93
to 97, the ProcessDeps interface currently only includes two functions and does
not extend EventHandlerDependencies, which can cause missing dependencies to go
unnoticed at compile time. Update ProcessDeps to be a type that intersects
EventHandlerDependencies with the two specific functions, ensuring the compiler
enforces that all dependencies from EventHandlerDependencies are included along
with getFlowTrigger and getGitHubRepositoryIntegrationIndex.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T01:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,275 @@
+import type { FlowTrigger } from "@giselle-sdk/data-type";
+import type {
+	GitHubAuthConfig,
+	WebhookEvent,
+	WebhookEventName,
+	addReaction,
+	ensureWebhookEvent,
+} from "@giselle-sdk/github-tool";
+import type { runFlow } from "../flows";
+import type { GiselleEngineContext } from "../types";
+import type { parseCommand } from "./utils";
+
+export interface EventHandlerDependencies {
+	addReaction: typeof addReaction;
+	ensureWebhookEvent: typeof ensureWebhookEvent;
+	runFlow: typeof runFlow;
+	parseCommand: typeof parseCommand;
+}
+
+export type EventHandlerArgs<TEventName extends WebhookEventName> = {
+	event: WebhookEvent<TEventName>;
+	context: GiselleEngineContext;
+	trigger: FlowTrigger;
+	authConfig: GitHubAuthConfig;
+	deps: EventHandlerDependencies;
+};
+
+export type EventHandlerResult = {
+	shouldRun: boolean;
+};
+
+export async function handleIssueOpened<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.opened") ||
+		args.trigger.configuration.event.id !== "github.issue.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueClosed<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.closed") ||
+		args.trigger.configuration.event.id !== "github.issue.closed"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
+		args.trigger.configuration.event.id !== "github.issue_comment.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const comment = args.event.data.payload.comment;
+	if (!comment) {
+		return { shouldRun: false };
+	}
+
+	const command = args.deps.parseCommand(comment.body);
+	const conditions =
+		args.trigger.configuration.event.id === "github.issue_comment.created"
+			? args.trigger.configuration.event.conditions
+			: undefined;
+
+	if (command?.callsign !== conditions?.callsign) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: comment.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
path: packages/giselle-engine/src/core/github/event-handlers.ts
user: login: shige
body: @toyamarinyon I'd like to see this comment!
created_at: 2025-05-23T01:44:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,275 @@
+import type { FlowTrigger } from "@giselle-sdk/data-type";
+import type {
+	GitHubAuthConfig,
+	WebhookEvent,
+	WebhookEventName,
+	addReaction,
+	ensureWebhookEvent,
+} from "@giselle-sdk/github-tool";
+import type { runFlow } from "../flows";
+import type { GiselleEngineContext } from "../types";
+import type { parseCommand } from "./utils";
+
+export interface EventHandlerDependencies {
+	addReaction: typeof addReaction;
+	ensureWebhookEvent: typeof ensureWebhookEvent;
+	runFlow: typeof runFlow;
+	parseCommand: typeof parseCommand;
+}
+
+export type EventHandlerArgs<TEventName extends WebhookEventName> = {
+	event: WebhookEvent<TEventName>;
+	context: GiselleEngineContext;
+	trigger: FlowTrigger;
+	authConfig: GitHubAuthConfig;
+	deps: EventHandlerDependencies;
+};
+
+export type EventHandlerResult = {
+	shouldRun: boolean;
+};
+
+export async function handleIssueOpened<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.opened") ||
+		args.trigger.configuration.event.id !== "github.issue.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueClosed<TEventName extends WebhookEventName>(
+	args: EventHandlerArgs<TEventName>,
+): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issues.closed") ||
+		args.trigger.configuration.event.id !== "github.issue.closed"
+	) {
+		return { shouldRun: false };
+	}
+
+	const issue = args.event.data.payload.issue;
+	if (!issue) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: issue.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handleIssueCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
+		args.trigger.configuration.event.id !== "github.issue_comment.created"
+	) {
+		return { shouldRun: false };
+	}
+
+	const comment = args.event.data.payload.comment;
+	if (!comment) {
+		return { shouldRun: false };
+	}
+
+	const command = args.deps.parseCommand(comment.body);
+	const conditions =
+		args.trigger.configuration.event.id === "github.issue_comment.created"
+			? args.trigger.configuration.event.conditions
+			: undefined;
+
+	if (command?.callsign !== conditions?.callsign) {
+		return { shouldRun: false };
+	}
+
+	await args.deps.addReaction({
+		id: comment.node_id,
+		content: "EYES",
+		authConfig: args.authConfig,
+	});
+
+	return { shouldRun: true };
+}
+
+export async function handlePullRequestCommentCreated<
+	TEventName extends WebhookEventName,
+>(args: EventHandlerArgs<TEventName>): Promise<EventHandlerResult> {
+	if (
+		!args.deps.ensureWebhookEvent(args.event, "issue_comment.created") ||
path: packages/giselle-engine/src/core/github/event-handlers.ts
user: login: toyamarinyon
body: In this case, 'issue_comment' is the correct choice because we want to handle Pull request comments. 'review_comment' is created when someone comments on a selected file. Here, we want to capture simple comments made directly on the Pull request.
created_at: 2025-05-23T02:02:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-22T09:18:16Z
body: [vc]: #ZU06wgCPTVg08Pi9M5ATP+ShEvABcU6px0KltV2MSBg=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJnaXNlbGxlIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvc3R1ZGlvLmdpc2VsbGVzLmFpIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3IwNi1lZGdlL2dpc2VsbGUvSGVhYk5Dd2ZIN2hiNzRrc0JiYmdpb3luOUNQMiIsInByZXZpZXdVcmwiOiJnaXNlbGxlLWdpdC1jb2RleC1hZGQtc3VwcG9ydC1mb3ItYWRkaXRpb25hbC1naS0xNTQzZGMtcjA2LWVkZ2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJnaXNlbGxlLWdpdC1jb2RleC1hZGQtc3VwcG9ydC1mb3ItYWRkaXRpb25hbC1naS0xNTQzZGMtcjA2LWVkZ2UudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJnaXNlbGxlLXBsYXlncm91bmQiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9wbGF5Z3JvdW5kIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3IwNi1lZGdlL2dpc2VsbGUtcGxheWdyb3VuZC83Z1BacGtxTjFQdEZrYWVkOXhZRzJwaktZQjlSIiwicHJldmlld1VybCI6Imdpc2VsbGUtcGxheWdyb3VuZC1naXQtY29kZXgtYWRkLXN1cHBvcnQtZm9yLWFkLWY5MDMyYi1yMDYtZWRnZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Imdpc2VsbGUtcGxheWdyb3VuZC1naXQtY29kZXgtYWRkLXN1cHBvcnQtZm9yLWFkLWY5MDMyYi1yMDYtZWRnZS52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **giselle** | ✅ Ready ([Inspect](https://vercel.com/r06-edge/giselle/HeabNCwfH7hb74ksBbbgioyn9CP2)) | [Visit Preview](https://giselle-git-codex-add-support-for-additional-gi-1543dc-r06-edge.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/giselle-git-codex-add-support-for-additional-gi-1543dc-r06-edge.vercel.app?via=pr-comment-feedback-link) | May 23, 2025 1:35am |
| **giselle-playground** | ✅ Ready ([Inspect](https://vercel.com/r06-edge/giselle-playground/7gPZpkqN1PtFkaed9xYG2pjKYB9R)) | [Visit Preview](https://giselle-playground-git-codex-add-support-for-ad-f9032b-r06-edge.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/giselle-playground-git-codex-add-support-for-ad-f9032b-r06-edge.vercel.app?via=pr-comment-feedback-link) | May 23, 2025 1:35am |



author_association: NONE
user: login: changeset-bot[bot]
created_at: 2025-05-22T09:18:17Z
body: ###  ⚠️  No Changeset found

Latest commit: 12decfc98d428d086e4250ba95a0d4268450cd30

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**



[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/giselles-ai/giselle/new/codex/add-support-for-additional-github-events?filename=.changeset/friendly-dancers-remember.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0AAdd%20support%20for%20more%20GitHub%20webhook%20events%0A)

<details><summary>💥 An error occurred when fetching the changed packages and changesets in this PR</summary>

```
Some errors occurred when validating the changesets config:
The package "playground" depends on the ignored package "giselle-sdk", but "playground" is not being ignored. Please add "playground" to the `ignore` option.
The package "studio.giselles.ai" depends on the ignored package "giselle-sdk", but "studio.giselles.ai" is not being ignored. Please add "studio.giselles.ai" to the `ignore` option.
The package "@giselle-internal/workflow-designer-ui" depends on the ignored package "giselle-sdk", but "@giselle-internal/workflow-designer-ui" is not being ignored. Please add "@giselle-internal/workflow-designer-ui" to the `ignore` option.
```

</details>

author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-22T09:18:19Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes modularize and extend GitHub webhook event handling in the Giselle engine by introducing dedicated event handler functions for various GitHub events, centralizing event processing logic, and improving dependency injection. Comprehensive unit tests are added for all handlers and the new event processing flow.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| packages/giselle-engine/src/core/github/event-handlers.ts                                    | New module defining event handler interfaces, multiple async handler functions for specific GitHub events (issues, pull requests, comments), a centralized `processEvent` function, and dependency injection for all external interactions.                                                                                                                                                                                                                                                                                                                                                                                         |
| packages/giselle-engine/src/core/github/event-handlers.test.ts                               | New test suite providing comprehensive unit tests for all event handler functions and the `processEvent` logic using mocks for dependencies and Vitest for assertions.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| packages/giselle-engine/src/core/github/handle-webhook-v2.ts                                 | Refactored to delegate event-specific logic to the new `processEvent` function, expanded supported event types, and restructured dependency management using injected utility functions and integrations.                                                                                                                                                                                                                                                                                                                                                                                      |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant GitHub as GitHub Webhook
    participant Engine as Giselle Engine
    participant Handlers as Event Handlers
    participant Flow as Flow Runner

    GitHub->>Engine: Sends webhook event
    Engine->>Handlers: Delegates event to processEvent
    Handlers->>Handlers: Iterate event handlers
    Handlers->>Handlers: Validate and handle event
    alt Handler matches and trigger enabled
        Handlers->>Flow: Run flow with event context
        Flow-->>Handlers: Flow execution result
    else No handler matches or trigger disabled
        Handlers-->>Engine: Return false
    end
    Handlers-->>Engine: Return result
```

## Possibly related PRs

- giselles-ai/giselle#914: Refactors and extends earlier webhook processing logic, making it directly related as a predecessor to this modular approach.
- giselles-ai/giselle#921: Introduces initial support for v2 GitHub webhook events and type safety, which this PR expands and modularizes further.

## Suggested reviewers

- shige

## Poem

> In the warren, code hops anew,  
> Handlers for issues, PRs too!  
> With modular flair and tests in tow,  
> Each event finds where it should go.  
> 🐇✨ Flows run swift, reactions gleam—  
> The engine hums, a rabbit’s dream!

<!-- walkthrough_end -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=giselles-ai/giselle&utm_content=926):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR