Compare commits

...

14 Commits

Author SHA1 Message Date
GitHub Actions
426380f01b chore: bump Claude Code version to 1.0.127 2025-09-26 18:15:45 +00:00
GitHub Actions
77f51d2905 chore: bump Claude Code version to 1.0.126 2025-09-26 01:13:47 +00:00
GitHub Actions
7e5b42b197 chore: bump Claude Code version to 1.0.124 2025-09-25 04:23:38 +00:00
GitHub Actions
1b7c7a77d3 chore: bump Claude Code version to 1.0.123 2025-09-23 23:48:31 +00:00
Vibhor Agrawal
bd70a3ef2b fix: add support for pull_request_target event in GitHub Actions workflows (#579)
Add pull_request_target event support to enable Claude Code usage with forked
repositories while maintaining proper security boundaries. This resolves issues
with dependabot PRs and external contributions that require write permissions.

Changes:
- Add pull_request_target to supported GitHub events in context parsing
- Update type definitions to include PullRequestTargetEvent
- Modify IS_PR calculation to detect pull_request_target as PR context
- Add comprehensive test coverage for pull_request_target workflows
- Update documentation to reflect pull_request_target support

The pull_request_target event provides the same payload structure as
pull_request but runs with write permissions from the base repository,
making it ideal for secure automation of external contributions.

Fixes #347
2025-09-22 09:20:27 -07:00
marcus
f4954b5256 removed mcp_config as input from usage.md and added to deprecated inputs with instructions to migrate to --mcp-config instead (#574) 2025-09-22 09:19:26 -07:00
Leonardo Yvens
93f8ab56c2 Add support for kebab-case --allowed-tools flag (#581)
- Update parseAllowedTools to accept both --allowedTools and --allowed-tools
- Add regex alternation to support both camelCase and kebab-case variants
- Add test cases for unquoted and quoted kebab-case formats
- All existing tests continue to pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-22 09:18:04 -07:00
GitHub Actions
93028b410e chore: bump Claude Code version to 1.0.120 2025-09-19 23:55:52 +00:00
GitHub Actions
838d4d9d25 chore: bump Claude Code version to 1.0.119 2025-09-19 00:53:43 +00:00
GitHub Actions
7ed3b616d5 chore: bump Claude Code version to 1.0.117 2025-09-16 23:49:28 +00:00
kashyap murali
09ea2f00e1 Delete .github/workflows/claude-test.yml (#573) 2025-09-16 13:46:34 -07:00
GitHub Actions
455b943dd7 chore: bump Claude Code version to 1.0.115 2025-09-16 00:52:01 +00:00
GitHub Actions
063d17ebb2 chore: bump Claude Code version to 1.0.113 2025-09-13 02:32:28 +00:00
Kevin Cui
2e92922dd6 fix(tag): no such tool available mcp__github_* (#556)
Signed-off-by: Kevin Cui <bh@bugs.cc>

# Conflicts:
#	src/mcp/install-mcp-server.ts
#	src/modes/tag/index.ts
#	test/modes/agent.test.ts
2025-09-12 12:33:34 -07:00
15 changed files with 579 additions and 78 deletions

View File

@@ -1,38 +0,0 @@
# Test workflow for km-anthropic fork (v1-dev branch)
# This tests the fork implementation, not the main repo
name: Claude Code (Fork Test)
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (
contains(github.event.issue.body, '@claude') ||
contains(github.event.issue.title, '@claude')
))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write # Required for OIDC token exchange
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Claude Code
uses: km-anthropic/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

View File

@@ -177,7 +177,7 @@ runs:
# Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.112
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.127
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
@@ -259,7 +259,7 @@ runs:
GITHUB_EVENT_NAME: ${{ github.event_name }}
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review_comment' }}
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}

View File

@@ -99,7 +99,7 @@ runs:
run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.112
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.127
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH

View File

@@ -15,7 +15,7 @@ The action automatically detects which mode to use based on your configuration:
This action supports the following GitHub events ([learn more GitHub event triggers](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)):
- `pull_request` - When PRs are opened or synchronized
- `pull_request` or `pull_request_target` - When PRs are opened or synchronized
- `issue_comment` - When comments are created on issues or PRs
- `pull_request_comment` - When comments are made on PR diffs
- `issues` - When issues are opened or assigned

View File

@@ -59,7 +59,6 @@ jobs:
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
@@ -90,6 +89,7 @@ These inputs are deprecated and will be removed in a future version:
| `fallback_model` | **DEPRECATED**: Use `claude_args` with fallback configuration | Configure fallback in `claude_args` or `settings` |
| `allowed_tools` | **DEPRECATED**: Use `claude_args` with `--allowedTools` instead | Use `claude_args: "--allowedTools Edit,Read,Write"` |
| `disallowed_tools` | **DEPRECATED**: Use `claude_args` with `--disallowedTools` instead | Use `claude_args: "--disallowedTools WebSearch"` |
| `mcp_config` | **DEPRECATED**: Use `claude_args` with `--mcp-config` instead | Use `claude_args: "--mcp-config '{...}'"` |
| `claude_env` | **DEPRECATED**: Use `settings` with env configuration | Configure environment in `settings` JSON |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)

View File

@@ -384,6 +384,7 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
};
case "pull_request":
case "pull_request_target":
return {
eventType: "PULL_REQUEST",
triggerContext: eventData.eventAction

View File

@@ -78,8 +78,7 @@ type IssueLabeledEvent = {
labelTrigger: string;
};
type PullRequestEvent = {
eventName: "pull_request";
type PullRequestBaseEvent = {
eventAction?: string; // opened, synchronize, etc.
isPR: true;
prNumber: string;
@@ -87,6 +86,14 @@ type PullRequestEvent = {
baseBranch?: string;
};
type PullRequestEvent = PullRequestBaseEvent & {
eventName: "pull_request";
};
type PullRequestTargetEvent = PullRequestBaseEvent & {
eventName: "pull_request_target";
};
// Union type for all possible event types
export type EventData =
| PullRequestReviewCommentEvent
@@ -96,7 +103,8 @@ export type EventData =
| IssueOpenedEvent
| IssueAssignedEvent
| IssueLabeledEvent
| PullRequestEvent;
| PullRequestEvent
| PullRequestTargetEvent;
// Combined type with separate eventData field
export type PreparedContext = CommonFields & {

View File

@@ -174,7 +174,8 @@ export function parseGitHubContext(): GitHubContext {
isPR: Boolean(payload.issue.pull_request),
};
}
case "pull_request": {
case "pull_request":
case "pull_request_target": {
const payload = context.payload as PullRequestEvent;
return {
...commonFields,

View File

@@ -3,6 +3,7 @@ import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config";
import type { GitHubContext } from "../github/context";
import { isEntityContext } from "../github/context";
import { Octokit } from "@octokit/rest";
import type { AutoDetectedMode } from "../modes/detector";
type PrepareConfigParams = {
githubToken: string;
@@ -12,8 +13,8 @@ type PrepareConfigParams = {
baseBranch: string;
claudeCommentId?: string;
allowedTools: string[];
mode: AutoDetectedMode;
context: GitHubContext;
mode: "tag" | "agent";
};
async function checkActionsReadPermission(
@@ -65,8 +66,13 @@ export async function prepareMcpConfig(
try {
const allowedToolsList = allowedTools || [];
// Detect if we're in agent mode (explicit prompt provided)
const isAgentMode = mode === "agent";
const hasGitHubCommentTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github_comment__"),
);
const hasGitHubMcpTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github__"),
);
@@ -86,7 +92,7 @@ export async function prepareMcpConfig(
// Include comment server:
// - Always in tag mode (for updating Claude comments)
// - Only with explicit tools in agent mode
const shouldIncludeCommentServer = !isAgentMode;
const shouldIncludeCommentServer = !isAgentMode || hasGitHubCommentTools;
if (shouldIncludeCommentServer) {
baseMcpConfig.mcpServers.github_comment = {

View File

@@ -135,8 +135,8 @@ export const agentMode: Mode = {
baseBranch: baseBranch,
claudeCommentId: undefined, // No tracking comment in agent mode
allowedTools,
context,
mode: "agent",
context,
});
// Build final claude_args with multiple --mcp-config flags

View File

@@ -1,10 +1,10 @@
export function parseAllowedTools(claudeArgs: string): string[] {
// Match --allowedTools followed by the value
// Match --allowedTools or --allowed-tools followed by the value
// Handle both quoted and unquoted values
const patterns = [
/--allowedTools\s+"([^"]+)"/, // Double quoted
/--allowedTools\s+'([^']+)'/, // Single quoted
/--allowedTools\s+([^\s]+)/, // Unquoted
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/, // Double quoted
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/, // Single quoted
/--(?:allowedTools|allowed-tools)\s+([^\s]+)/, // Unquoted
];
for (const pattern of patterns) {

View File

@@ -14,6 +14,7 @@ import { createPrompt, generateDefaultPrompt } from "../../create-prompt";
import { isEntityContext } from "../../github/context";
import type { PreparedContext } from "../../create-prompt/types";
import type { FetchDataResult } from "../../github/data/fetcher";
import { parseAllowedTools } from "../agent/parse-tools";
/**
* Tag mode implementation.
@@ -112,20 +113,10 @@ export const tagMode: Mode = {
await createPrompt(tagMode, modeContext, githubData, context);
// Get our GitHub MCP servers configuration
const ourMcpConfig = await prepareMcpConfig({
githubToken,
owner: context.repository.owner,
repo: context.repository.repo,
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
baseBranch: branchInfo.baseBranch,
claudeCommentId: commentId.toString(),
allowedTools: [],
context,
mode: "tag",
});
// Don't output mcp_config separately anymore - include in claude_args
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter(
(tool) => tool.startsWith("mcp__github_"),
);
// Build claude_args for tag mode with required tools
// Tag mode REQUIRES these tools to function properly
@@ -141,6 +132,7 @@ export const tagMode: Mode = {
"mcp__github_ci__get_ci_status",
"mcp__github_ci__get_workflow_run_details",
"mcp__github_ci__download_job_log",
...userAllowedMCPTools,
];
// Add git commands when not using commit signing
@@ -162,7 +154,18 @@ export const tagMode: Mode = {
);
}
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
// Get our GitHub MCP servers configuration
const ourMcpConfig = await prepareMcpConfig({
githubToken,
owner: context.repository.owner,
repo: context.repository.repo,
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
baseBranch: branchInfo.baseBranch,
claudeCommentId: commentId.toString(),
allowedTools: Array.from(new Set(tagModeTools)),
mode: "tag",
context,
});
// Build complete claude_args with multiple --mcp-config flags
let claudeArgs = "";

View File

@@ -106,8 +106,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockContextWithSigning,
mode: "tag",
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
@@ -130,8 +130,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: ["mcp__github__create_issue", "mcp__github__create_pr"],
context: mockContext,
mode: "tag",
context: mockContext,
});
const parsed = JSON.parse(result);
@@ -151,8 +151,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: ["mcp__github_inline_comment__create_inline_comment"],
context: mockPRContext,
mode: "tag",
context: mockPRContext,
});
const parsed = JSON.parse(result);
@@ -172,8 +172,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockContext,
mode: "tag",
context: mockContext,
});
const parsed = JSON.parse(result);
@@ -193,8 +193,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockContextWithSigning,
mode: "tag",
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
@@ -213,8 +213,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockContextWithSigning,
mode: "tag",
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
@@ -231,8 +231,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockPRContext,
mode: "tag",
context: mockPRContext,
});
const parsed = JSON.parse(result);
@@ -251,8 +251,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockContext,
mode: "tag",
context: mockContext,
});
const parsed = JSON.parse(result);
@@ -269,8 +269,8 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockPRContext,
mode: "tag",
context: mockPRContext,
});
const parsed = JSON.parse(result);

View File

@@ -68,4 +68,20 @@ describe("parseAllowedTools", () => {
"mcp__github_comment__update",
]);
});
test("parses kebab-case --allowed-tools", () => {
const args = "--allowed-tools mcp__github__*,mcp__github_comment__*";
expect(parseAllowedTools(args)).toEqual([
"mcp__github__*",
"mcp__github_comment__*",
]);
});
test("parses quoted kebab-case --allowed-tools", () => {
const args = '--allowed-tools "mcp__github__*,mcp__github_comment__*"';
expect(parseAllowedTools(args)).toEqual([
"mcp__github__*",
"mcp__github_comment__*",
]);
});
});

View File

@@ -0,0 +1,504 @@
#!/usr/bin/env bun
import { describe, test, expect } from "bun:test";
import {
getEventTypeAndContext,
generatePrompt,
generateDefaultPrompt,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
import type { Mode } from "../src/modes/types";
describe("pull_request_target event support", () => {
// Mock tag mode for testing
const mockTagMode: Mode = {
name: "tag",
description: "Tag mode",
shouldTrigger: () => true,
prepareContext: (context) => ({ mode: "tag", githubContext: context }),
getAllowedTools: () => [],
getDisallowedTools: () => [],
shouldCreateTrackingComment: () => true,
generatePrompt: (context, githubData, useCommitSigning) =>
generateDefaultPrompt(context, githubData, useCommitSigning),
prepare: async () => ({
commentId: 123,
branchInfo: {
baseBranch: "main",
currentBranch: "main",
claudeBranch: undefined,
},
mcpConfig: "{}",
}),
};
const mockGitHubData = {
contextData: {
title: "External PR via pull_request_target",
body: "This PR comes from a forked repository",
author: { login: "external-contributor" },
state: "OPEN",
createdAt: "2023-01-01T00:00:00Z",
additions: 25,
deletions: 3,
baseRefName: "main",
headRefName: "feature-branch",
headRefOid: "abc123",
commits: {
totalCount: 2,
nodes: [
{
commit: {
oid: "commit1",
message: "Initial feature implementation",
author: {
name: "External Dev",
email: "external@example.com",
},
},
},
{
commit: {
oid: "commit2",
message: "Fix typos and formatting",
author: {
name: "External Dev",
email: "external@example.com",
},
},
},
],
},
files: {
nodes: [
{
path: "src/feature.ts",
additions: 20,
deletions: 2,
changeType: "MODIFIED",
},
{
path: "tests/feature.test.ts",
additions: 5,
deletions: 1,
changeType: "ADDED",
},
],
},
comments: { nodes: [] },
reviews: { nodes: [] },
},
comments: [],
changedFiles: [],
changedFilesWithSHA: [
{
path: "src/feature.ts",
additions: 20,
deletions: 2,
changeType: "MODIFIED",
sha: "abc123",
},
{
path: "tests/feature.test.ts",
additions: 5,
deletions: 1,
changeType: "ADDED",
sha: "abc123",
},
],
reviewData: { nodes: [] },
imageUrlMap: new Map<string, string>(),
};
describe("prompt generation for pull_request_target", () => {
test("should generate correct prompt for pull_request_target event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "123",
},
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain pull request event type and metadata
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
expect(prompt).toContain("<pr_number>123</pr_number>");
expect(prompt).toContain(
"<trigger_context>pull request opened</trigger_context>",
);
// Should contain PR-specific information
expect(prompt).toContain(
"- src/feature.ts (MODIFIED) +20/-2 SHA: abc123",
);
expect(prompt).toContain(
"- tests/feature.test.ts (ADDED) +5/-1 SHA: abc123",
);
expect(prompt).toContain("external-contributor");
expect(prompt).toContain("<repository>owner/repo</repository>");
});
test("should handle pull_request_target with commit signing disabled", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "synchronize",
isPR: true,
prNumber: "456",
},
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should include git commands for non-commit-signing mode
expect(prompt).toContain("git push");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
// Should not include commit signing tools
expect(prompt).not.toContain("mcp__github_file_ops__commit_files");
});
test("should handle pull_request_target with commit signing enabled", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "synchronize",
isPR: true,
prNumber: "456",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
// Should include commit signing tools
expect(prompt).toContain("mcp__github_file_ops__commit_files");
expect(prompt).toContain("mcp__github_file_ops__delete_files");
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
// Should not include git command instructions
expect(prompt).not.toContain("Use git commands via the Bash tool");
});
test("should treat pull_request_target same as pull_request in prompt generation", () => {
const baseContext: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "123",
},
};
// Generate prompt for pull_request
const pullRequestContext: PreparedContext = {
...baseContext,
eventData: {
...baseContext.eventData,
eventName: "pull_request",
isPR: true,
prNumber: "123",
},
};
// Generate prompt for pull_request_target
const pullRequestTargetContext: PreparedContext = {
...baseContext,
eventData: {
...baseContext.eventData,
eventName: "pull_request_target",
isPR: true,
prNumber: "123",
},
};
const pullRequestPrompt = generatePrompt(
pullRequestContext,
mockGitHubData,
false,
mockTagMode,
);
const pullRequestTargetPrompt = generatePrompt(
pullRequestTargetContext,
mockGitHubData,
false,
mockTagMode,
);
// Both should have the same event type and structure
expect(pullRequestPrompt).toContain(
"<event_type>PULL_REQUEST</event_type>",
);
expect(pullRequestTargetPrompt).toContain(
"<event_type>PULL_REQUEST</event_type>",
);
expect(pullRequestPrompt).toContain(
"<trigger_context>pull request opened</trigger_context>",
);
expect(pullRequestTargetPrompt).toContain(
"<trigger_context>pull request opened</trigger_context>",
);
// Both should contain PR-specific instructions
expect(pullRequestPrompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
expect(pullRequestTargetPrompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
});
test("should handle pull_request_target in agent mode with custom prompt", () => {
const envVars: PreparedContext = {
repository: "test/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
prompt: "Review this pull_request_target PR for security issues",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "789",
},
};
// Use agent mode which passes through the prompt as-is
const mockAgentMode: Mode = {
name: "agent",
description: "Agent mode",
shouldTrigger: () => true,
prepareContext: (context) => ({
mode: "agent",
githubContext: context,
}),
getAllowedTools: () => [],
getDisallowedTools: () => [],
shouldCreateTrackingComment: () => true,
generatePrompt: (context) => context.prompt || "default prompt",
prepare: async () => ({
commentId: 123,
branchInfo: {
baseBranch: "main",
currentBranch: "main",
claudeBranch: undefined,
},
mcpConfig: "{}",
}),
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockAgentMode,
);
expect(prompt).toBe(
"Review this pull_request_target PR for security issues",
);
});
test("should handle pull_request_target with no custom prompt", () => {
const envVars: PreparedContext = {
repository: "test/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "synchronize",
isPR: true,
prNumber: "456",
},
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should generate default prompt structure
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<pr_number>456</pr_number>");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
});
});
describe("pull_request_target vs pull_request behavior consistency", () => {
test("should produce identical event processing for both event types", () => {
const baseEventData = {
eventAction: "opened",
isPR: true,
prNumber: "100",
};
const pullRequestEvent: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
...baseEventData,
eventName: "pull_request",
isPR: true,
prNumber: "100",
},
};
const pullRequestTargetEvent: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
...baseEventData,
eventName: "pull_request_target",
isPR: true,
prNumber: "100",
},
};
// Both should have identical event type detection
const prResult = getEventTypeAndContext(pullRequestEvent);
const prtResult = getEventTypeAndContext(pullRequestTargetEvent);
expect(prResult.eventType).toBe(prtResult.eventType);
expect(prResult.triggerContext).toBe(prtResult.triggerContext);
});
test("should handle edge cases in pull_request_target events", () => {
// Test with minimal event data
const minimalContext: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
isPR: true,
prNumber: "1",
},
};
const result = getEventTypeAndContext(minimalContext);
expect(result.eventType).toBe("PULL_REQUEST");
expect(result.triggerContext).toBe("pull request event");
// Should not throw when generating prompt
expect(() => {
generatePrompt(minimalContext, mockGitHubData, false, mockTagMode);
}).not.toThrow();
});
test("should handle all valid pull_request_target actions", () => {
const actions = ["opened", "synchronize", "reopened", "closed", "edited"];
actions.forEach((action) => {
const context: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: action,
isPR: true,
prNumber: "1",
},
};
const result = getEventTypeAndContext(context);
expect(result.eventType).toBe("PULL_REQUEST");
expect(result.triggerContext).toBe(`pull request ${action}`);
});
});
});
describe("security considerations for pull_request_target", () => {
test("should maintain same prompt structure regardless of event source", () => {
// Test that external PRs don't get different treatment in prompts
const internalPR: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request",
eventAction: "opened",
isPR: true,
prNumber: "1",
},
};
const externalPR: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "1",
},
};
const internalPrompt = generatePrompt(
internalPR,
mockGitHubData,
false,
mockTagMode,
);
const externalPrompt = generatePrompt(
externalPR,
mockGitHubData,
false,
mockTagMode,
);
// Should have same tool access patterns
expect(
internalPrompt.includes("mcp__github_comment__update_claude_comment"),
).toBe(
externalPrompt.includes("mcp__github_comment__update_claude_comment"),
);
// Should have same branch handling instructions
expect(
internalPrompt.includes(
"Always push to the existing branch when triggered on a PR",
),
).toBe(
externalPrompt.includes(
"Always push to the existing branch when triggered on a PR",
),
);
});
});
});