mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
43 Commits
v1.0.7
...
add-plugin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23e406ca24 | ||
|
|
f5f27d4716 | ||
|
|
fd20c95358 | ||
|
|
d808160c26 | ||
|
|
3eacedbeb7 | ||
|
|
f52f12eba5 | ||
|
|
4a85933f25 | ||
|
|
ba6edd55ef | ||
|
|
06461dddff | ||
|
|
c2a94eead0 | ||
|
|
1c0c3eaced | ||
|
|
23d2d6c6b4 | ||
|
|
e8bad57227 | ||
|
|
0a6d62601b | ||
|
|
777ffcbfc9 | ||
|
|
dc58efed33 | ||
|
|
e5437bfbc5 | ||
|
|
b2dd1006a0 | ||
|
|
ac1a3207f3 | ||
|
|
521d069da7 | ||
|
|
7e4b782d5f | ||
|
|
4fb0ef3be0 | ||
|
|
14ac8aa20e | ||
|
|
90d189f3ab | ||
|
|
9c09b26b2d | ||
|
|
2086c977a5 | ||
|
|
851ef5b84e | ||
|
|
1ce8153c18 | ||
|
|
00391ab25e | ||
|
|
426380f01b | ||
|
|
77f51d2905 | ||
|
|
7e5b42b197 | ||
|
|
1b7c7a77d3 | ||
|
|
bd70a3ef2b | ||
|
|
f4954b5256 | ||
|
|
93f8ab56c2 | ||
|
|
93028b410e | ||
|
|
838d4d9d25 | ||
|
|
7ed3b616d5 | ||
|
|
09ea2f00e1 | ||
|
|
455b943dd7 | ||
|
|
063d17ebb2 | ||
|
|
2e92922dd6 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/claude-review.yml
vendored
2
.github/workflows/claude-review.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
38
.github/workflows/claude-test.yml
vendored
38
.github/workflows/claude-test.yml
vendored
@@ -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 }}
|
||||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
next_version: ${{ steps.next_version.outputs.next_version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout base-action repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: anthropics/claude-code-base-action
|
||||
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
|
||||
4
.github/workflows/test-settings.yml
vendored
4
.github/workflows/test-settings.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "This should not work"
|
||||
Run the command `echo $HOME` to check the home directory path
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
settings: |
|
||||
{
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "This should not work from file"
|
||||
Run the command `echo $HOME` to check the home directory path
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
settings: "test-settings.json"
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ inputs:
|
||||
description: "Claude Code settings as JSON string or path to settings JSON file"
|
||||
required: false
|
||||
default: ""
|
||||
plugins:
|
||||
description: "Comma-separated list of Claude Code plugins to install (e.g., 'plugin-name1,plugin-name2')"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
# Auth configuration
|
||||
anthropic_api_key:
|
||||
@@ -177,7 +181,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 2.0.24
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
@@ -208,6 +212,7 @@ runs:
|
||||
CLAUDE_CODE_ACTION: "1"
|
||||
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
||||
INPUT_SETTINGS: ${{ inputs.settings }}
|
||||
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
|
||||
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
||||
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
||||
@@ -259,7 +264,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 || '' }}
|
||||
|
||||
@@ -336,7 +336,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ inputs:
|
||||
description: "Claude Code settings as JSON string or path to settings JSON file"
|
||||
required: false
|
||||
default: ""
|
||||
plugins:
|
||||
description: "Comma-separated list of Claude Code plugins to install (e.g., 'plugin-name1,plugin-name2')"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
# Action settings
|
||||
claude_args:
|
||||
@@ -99,7 +103,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 2.0.24
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
@@ -123,6 +127,7 @@ runs:
|
||||
INPUT_PROMPT: ${{ inputs.prompt }}
|
||||
INPUT_PROMPT_FILE: ${{ inputs.prompt_file }}
|
||||
INPUT_SETTINGS: ${{ inputs.settings }}
|
||||
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
"ghcr.io/github/github-mcp-server:sha-23fa0dd"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { preparePrompt } from "./prepare-prompt";
|
||||
import { runClaude } from "./run-claude";
|
||||
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
|
||||
import { validateEnvironmentVariables } from "./validate-env";
|
||||
import { installPlugins } from "./install-plugins";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
@@ -15,6 +16,12 @@ async function run() {
|
||||
undefined, // homeDir
|
||||
);
|
||||
|
||||
// Install plugins if specified
|
||||
await installPlugins(
|
||||
process.env.INPUT_PLUGINS,
|
||||
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE || "claude",
|
||||
);
|
||||
|
||||
const promptConfig = await preparePrompt({
|
||||
prompt: process.env.INPUT_PROMPT || "",
|
||||
promptFile: process.env.INPUT_PROMPT_FILE || "",
|
||||
|
||||
80
base-action/src/install-plugins.ts
Normal file
80
base-action/src/install-plugins.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawn } from "child_process";
|
||||
|
||||
// Declare console as global for TypeScript
|
||||
declare const console: {
|
||||
log: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list of plugin names and returns an array of trimmed plugin names
|
||||
*/
|
||||
export function parsePlugins(pluginsInput: string | undefined): string[] {
|
||||
if (!pluginsInput || pluginsInput.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return pluginsInput
|
||||
.split(",")
|
||||
.map((plugin) => plugin.trim())
|
||||
.filter((plugin) => plugin.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a single Claude Code plugin
|
||||
*/
|
||||
export async function installPlugin(
|
||||
pluginName: string,
|
||||
claudeExecutable: string = "claude",
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(claudeExecutable, ["plugin", "install", pluginName], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
process.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to install plugin '${pluginName}' (exit code: ${code})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
process.on("error", (err: Error) => {
|
||||
reject(
|
||||
new Error(`Failed to install plugin '${pluginName}': ${err.message}`),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs Claude Code plugins from a comma-separated list
|
||||
*/
|
||||
export async function installPlugins(
|
||||
pluginsInput: string | undefined,
|
||||
claudeExecutable: string = "claude",
|
||||
): Promise<void> {
|
||||
const plugins = parsePlugins(pluginsInput);
|
||||
|
||||
if (plugins.length === 0) {
|
||||
console.log("No plugins to install");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Installing ${plugins.length} plugin(s)...`);
|
||||
|
||||
for (const plugin of plugins) {
|
||||
console.log(`Installing plugin: ${plugin}`);
|
||||
await installPlugin(plugin, claudeExecutable);
|
||||
console.log(`✓ Successfully installed: ${plugin}`);
|
||||
}
|
||||
|
||||
console.log("All plugins installed successfully");
|
||||
}
|
||||
84
base-action/test/install-plugins.test.ts
Normal file
84
base-action/test/install-plugins.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { parsePlugins } from "../src/install-plugins";
|
||||
|
||||
describe("parsePlugins", () => {
|
||||
test("should return empty array for undefined input", () => {
|
||||
expect(parsePlugins(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should return empty array for empty string", () => {
|
||||
expect(parsePlugins("")).toEqual([]);
|
||||
});
|
||||
|
||||
test("should return empty array for whitespace-only string", () => {
|
||||
expect(parsePlugins(" \n\t ")).toEqual([]);
|
||||
});
|
||||
|
||||
test("should parse single plugin", () => {
|
||||
expect(parsePlugins("feature-dev")).toEqual(["feature-dev"]);
|
||||
});
|
||||
|
||||
test("should parse multiple plugins", () => {
|
||||
expect(parsePlugins("feature-dev,test-coverage-reviewer")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should trim whitespace around plugin names", () => {
|
||||
expect(parsePlugins(" feature-dev , test-coverage-reviewer ")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle spaces between commas", () => {
|
||||
expect(
|
||||
parsePlugins(
|
||||
"feature-dev, test-coverage-reviewer, code-quality-reviewer",
|
||||
),
|
||||
).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
"code-quality-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should filter out empty values from consecutive commas", () => {
|
||||
expect(parsePlugins("feature-dev,,test-coverage-reviewer")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle trailing comma", () => {
|
||||
expect(parsePlugins("feature-dev,test-coverage-reviewer,")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle leading comma", () => {
|
||||
expect(parsePlugins(",feature-dev,test-coverage-reviewer")).toEqual([
|
||||
"feature-dev",
|
||||
"test-coverage-reviewer",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle plugins with special characters", () => {
|
||||
expect(parsePlugins("@scope/plugin-name,plugin-name-2")).toEqual([
|
||||
"@scope/plugin-name",
|
||||
"plugin-name-2",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle complex whitespace patterns", () => {
|
||||
expect(
|
||||
parsePlugins(
|
||||
"\n feature-dev \n,\t test-coverage-reviewer\t, code-quality \n",
|
||||
),
|
||||
).toEqual(["feature-dev", "test-coverage-reviewer", "code-quality"]);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -127,7 +127,7 @@ For performance, Claude uses shallow clones:
|
||||
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
|
||||
|
||||
```
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
depth: 0 # will fetch full repo history
|
||||
```
|
||||
|
||||
|
||||
@@ -13,13 +13,28 @@
|
||||
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
|
||||
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
||||
|
||||
## ⚠️ Prompt Injection Risks
|
||||
|
||||
**Beware of potential hidden markdown when tagging Claude on untrusted content.** External contributors may include hidden instructions through HTML comments, invisible characters, hidden attributes, or other techniques. The action sanitizes content by stripping HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities, but new bypass techniques may emerge. We recommend reviewing the raw content of all input coming from external contributors before allowing Claude to process it.
|
||||
|
||||
## GitHub App Permissions
|
||||
|
||||
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
|
||||
The [Claude Code GitHub app](https://github.com/apps/claude) requests the following permissions:
|
||||
|
||||
- **Pull Requests**: Read and write to create PRs and push changes
|
||||
- **Issues**: Read and write to respond to issues
|
||||
- **Contents**: Read and write to modify repository files
|
||||
### Currently Used Permissions
|
||||
|
||||
- **Contents** (Read & Write): For reading repository files and creating branches
|
||||
- **Pull Requests** (Read & Write): For reading PR data and creating/updating pull requests
|
||||
- **Issues** (Read & Write): For reading issue data and updating issue comments
|
||||
|
||||
### Permissions for Future Features
|
||||
|
||||
The following permissions are requested but not yet actively used. These will enable planned features in future releases:
|
||||
|
||||
- **Discussions** (Read & Write): For interaction with GitHub Discussions
|
||||
- **Actions** (Read): For accessing workflow run data and logs
|
||||
- **Checks** (Read): For reading check run results
|
||||
- **Workflows** (Read & Write): For triggering and managing GitHub Actions workflows
|
||||
|
||||
## Commit Signing
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -344,7 +344,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -456,7 +456,7 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
@@ -513,7 +513,7 @@ jobs:
|
||||
security-events: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -384,6 +384,7 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
||||
};
|
||||
|
||||
case "pull_request":
|
||||
case "pull_request_target":
|
||||
return {
|
||||
eventType: "PULL_REQUEST",
|
||||
triggerContext: eventData.eventAction
|
||||
@@ -708,7 +709,7 @@ What You CANNOT Do:
|
||||
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
|
||||
|
||||
When users ask you to perform actions you cannot do, politely explain the limitation and, when applicable, direct them to the FAQ for more information and workarounds:
|
||||
"I'm unable to [specific action] due to [reason]. You can find more information and potential workarounds in the [FAQ](https://github.com/anthropics/claude-code-action/blob/main/FAQ.md)."
|
||||
"I'm unable to [specific action] due to [reason]. You can find more information and potential workarounds in the [FAQ](https://github.com/anthropics/claude-code-action/blob/main/docs/faq.md)."
|
||||
|
||||
If a user asks for something outside these capabilities (and you have no other tools provided), politely explain that you cannot perform that action and suggest an alternative approach if possible.
|
||||
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
@@ -203,7 +209,7 @@ export async function prepareMcpConfig(
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"-e",
|
||||
"GITHUB_HOST",
|
||||
"ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
|
||||
"ghcr.io/github/github-mcp-server:sha-23fa0dd", // https://github.com/github/github-mcp-server/releases/tag/v0.17.1
|
||||
],
|
||||
env: {
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__*",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
504
test/pull-request-target.test.ts
Normal file
504
test/pull-request-target.test.ts
Normal 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",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user