mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
2 Commits
v0.0.16
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d100f7832 | ||
|
|
e0dcb85d34 |
2
.github/workflows/claude-review.yml
vendored
2
.github/workflows/claude-review.yml
vendored
@@ -29,4 +29,4 @@ jobs:
|
||||
|
||||
Be constructive and specific in your feedback. Give inline comments where applicable.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
||||
allowed_tools: "mcp__github__add_pull_request_review_comment"
|
||||
|
||||
21
README.md
21
README.md
@@ -70,8 +70,6 @@ jobs:
|
||||
# NODE_ENV: test
|
||||
# DEBUG: true
|
||||
# API_URL: https://api.example.com
|
||||
# Optional: limit the number of conversation turns
|
||||
# max_turns: "5"
|
||||
```
|
||||
|
||||
## Inputs
|
||||
@@ -80,7 +78,6 @@ jobs:
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
||||
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
|
||||
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
|
||||
@@ -314,24 +311,6 @@ You can pass custom environment variables to Claude Code execution using the `cl
|
||||
|
||||
The `claude_env` input accepts YAML format where each line defines a key-value pair. These environment variables will be available to Claude Code during execution, allowing it to run tests, build processes, or other commands that depend on specific environment configurations.
|
||||
|
||||
### Limiting Conversation Turns
|
||||
|
||||
You can use the `max_turns` parameter to limit the number of back-and-forth exchanges Claude can have during task execution. This is useful for:
|
||||
|
||||
- Controlling costs by preventing runaway conversations
|
||||
- Setting time boundaries for automated workflows
|
||||
- Ensuring predictable behavior in CI/CD pipelines
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
max_turns: "5" # Limit to 5 conversation turns
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
When the turn limit is reached, Claude will stop execution gracefully. Choose a value that gives Claude enough turns to complete typical tasks while preventing excessive usage.
|
||||
|
||||
### Custom Tools
|
||||
|
||||
By default, Claude only has access to:
|
||||
|
||||
16
action.yml
16
action.yml
@@ -62,10 +62,6 @@ inputs:
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
max_turns:
|
||||
description: "Maximum number of conversation turns"
|
||||
required: false
|
||||
default: ""
|
||||
timeout_minutes:
|
||||
description: "Timeout in minutes for execution"
|
||||
required: false
|
||||
@@ -87,14 +83,14 @@ runs:
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${GITHUB_ACTION_PATH}
|
||||
cd ${{ github.action_path }}
|
||||
bun install
|
||||
|
||||
- name: Prepare action
|
||||
id: prepare
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
|
||||
bun run ${{ github.action_path }}/src/entrypoints/prepare.ts
|
||||
env:
|
||||
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
||||
@@ -109,13 +105,12 @@ runs:
|
||||
- name: Run Claude Code
|
||||
id: claude-code
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
uses: anthropics/claude-code-base-action@79b8cfc932eb13806c23905842145e6f05c89e2e # v0.0.13
|
||||
uses: anthropics/claude-code-base-action@1370ac97fbba9bddec20ea2924b5726bf10d8b94 # v0.0.9
|
||||
with:
|
||||
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
||||
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||
disallowed_tools: ${{ env.DISALLOWED_TOOLS }}
|
||||
timeout_minutes: ${{ inputs.timeout_minutes }}
|
||||
max_turns: ${{ inputs.max_turns }}
|
||||
model: ${{ inputs.model || inputs.anthropic_model }}
|
||||
mcp_config: ${{ steps.prepare.outputs.mcp_config }}
|
||||
use_bedrock: ${{ inputs.use_bedrock }}
|
||||
@@ -152,7 +147,7 @@ runs:
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
|
||||
bun run ${{ github.action_path }}/src/entrypoints/update-comment-link.ts
|
||||
env:
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
||||
@@ -165,6 +160,7 @@ runs:
|
||||
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
||||
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
||||
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
||||
CLAUDE_CANCELLED: ${{ cancelled() }}
|
||||
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
||||
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
||||
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||
|
||||
2
bun.lock
2
bun.lock
@@ -2,7 +2,7 @@
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@anthropic-ai/claude-code-action",
|
||||
"name": "claude-pr-action",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
|
||||
Provide constructive feedback with specific suggestions for improvement.
|
||||
Use inline comments to highlight specific areas of concern.
|
||||
# allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
||||
# allowed_tools: "mcp__github__add_pull_request_review_comment"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@anthropic-ai/claude-code-action",
|
||||
"name": "claude-pr-action",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -35,35 +35,38 @@ const BASE_ALLOWED_TOOLS = [
|
||||
];
|
||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||
|
||||
export function buildAllowedToolsString(customAllowedTools?: string[]): string {
|
||||
export function buildAllowedToolsString(customAllowedTools?: string): string {
|
||||
let baseTools = [...BASE_ALLOWED_TOOLS];
|
||||
|
||||
let allAllowedTools = baseTools.join(",");
|
||||
if (customAllowedTools && customAllowedTools.length > 0) {
|
||||
allAllowedTools = `${allAllowedTools},${customAllowedTools.join(",")}`;
|
||||
if (customAllowedTools) {
|
||||
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
|
||||
}
|
||||
return allAllowedTools;
|
||||
}
|
||||
|
||||
export function buildDisallowedToolsString(
|
||||
customDisallowedTools?: string[],
|
||||
allowedTools?: string[],
|
||||
customDisallowedTools?: string,
|
||||
allowedTools?: string,
|
||||
): string {
|
||||
let disallowedTools = [...DISALLOWED_TOOLS];
|
||||
|
||||
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
|
||||
if (allowedTools && allowedTools.length > 0) {
|
||||
if (allowedTools) {
|
||||
const allowedToolsArray = allowedTools
|
||||
.split(",")
|
||||
.map((tool) => tool.trim());
|
||||
disallowedTools = disallowedTools.filter(
|
||||
(tool) => !allowedTools.includes(tool),
|
||||
(tool) => !allowedToolsArray.includes(tool),
|
||||
);
|
||||
}
|
||||
|
||||
let allDisallowedTools = disallowedTools.join(",");
|
||||
if (customDisallowedTools && customDisallowedTools.length > 0) {
|
||||
if (customDisallowedTools) {
|
||||
if (allDisallowedTools) {
|
||||
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools.join(",")}`;
|
||||
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
|
||||
} else {
|
||||
allDisallowedTools = customDisallowedTools.join(",");
|
||||
allDisallowedTools = customDisallowedTools;
|
||||
}
|
||||
}
|
||||
return allDisallowedTools;
|
||||
@@ -117,10 +120,8 @@ export function prepareContext(
|
||||
triggerPhrase,
|
||||
...(triggerUsername && { triggerUsername }),
|
||||
...(customInstructions && { customInstructions }),
|
||||
...(allowedTools.length > 0 && { allowedTools: allowedTools.join(",") }),
|
||||
...(disallowedTools.length > 0 && {
|
||||
disallowedTools: disallowedTools.join(","),
|
||||
}),
|
||||
...(allowedTools && { allowedTools }),
|
||||
...(disallowedTools && { disallowedTools }),
|
||||
...(directPrompt && { directPrompt }),
|
||||
...(claudeBranch && { claudeBranch }),
|
||||
};
|
||||
@@ -620,9 +621,7 @@ export async function createPrompt(
|
||||
claudeBranch,
|
||||
);
|
||||
|
||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
||||
recursive: true,
|
||||
});
|
||||
await mkdir("/tmp/claude-prompts", { recursive: true });
|
||||
|
||||
// Generate the prompt
|
||||
const promptContent = generatePrompt(preparedContext, githubData);
|
||||
@@ -633,18 +632,15 @@ export async function createPrompt(
|
||||
console.log("=======================");
|
||||
|
||||
// Write the prompt file
|
||||
await writeFile(
|
||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
||||
promptContent,
|
||||
);
|
||||
await writeFile("/tmp/claude-prompts/claude-prompt.txt", promptContent);
|
||||
|
||||
// Set allowed tools
|
||||
const allAllowedTools = buildAllowedToolsString(
|
||||
context.inputs.allowedTools,
|
||||
preparedContext.allowedTools,
|
||||
);
|
||||
const allDisallowedTools = buildDisallowedToolsString(
|
||||
context.inputs.disallowedTools,
|
||||
context.inputs.allowedTools,
|
||||
preparedContext.disallowedTools,
|
||||
preparedContext.allowedTools,
|
||||
);
|
||||
|
||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
||||
|
||||
@@ -92,7 +92,6 @@ async function run() {
|
||||
branch: branchInfo.currentBranch,
|
||||
additionalMcpConfig,
|
||||
claudeCommentId: commentId.toString(),
|
||||
allowedTools: context.inputs.allowedTools,
|
||||
});
|
||||
core.setOutput("mcp_config", mcpConfig);
|
||||
} catch (error) {
|
||||
|
||||
@@ -146,8 +146,12 @@ async function run() {
|
||||
duration_api_ms?: number;
|
||||
} | null = null;
|
||||
let actionFailed = false;
|
||||
let actionCancelled = false;
|
||||
let errorDetails: string | undefined;
|
||||
|
||||
// Check if the workflow was cancelled
|
||||
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
|
||||
|
||||
// First check if prepare step failed
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
@@ -155,8 +159,18 @@ async function run() {
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
errorDetails = prepareError;
|
||||
} else if (isCancelled) {
|
||||
// If the workflow was cancelled, set the cancelled flag
|
||||
actionCancelled = true;
|
||||
} else {
|
||||
// Check for existence of output file and parse it if available
|
||||
// Check if the Claude action failed
|
||||
// CLAUDE_SUCCESS is set to the result of: steps.claude-code.outputs.conclusion == 'success'
|
||||
// If the step didn't run or didn't set outputs.conclusion, CLAUDE_SUCCESS will be "false"
|
||||
// If the step succeeded, CLAUDE_SUCCESS will be "true"
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
|
||||
// Try to read execution details from output file
|
||||
try {
|
||||
const outputFile = process.env.OUTPUT_FILE;
|
||||
if (outputFile) {
|
||||
@@ -179,14 +193,10 @@ async function run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the Claude action failed
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
||||
actionFailed = !claudeSuccess;
|
||||
} catch (error) {
|
||||
console.error("Error reading output file:", error);
|
||||
// If we can't read the file, check for any failure markers
|
||||
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
||||
// Error reading output file doesn't change the action status
|
||||
// We already determined actionFailed based on CLAUDE_SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +204,7 @@ async function run() {
|
||||
const commentInput: CommentUpdateInput = {
|
||||
currentBody,
|
||||
actionFailed,
|
||||
actionCancelled,
|
||||
executionDetails,
|
||||
jobUrl,
|
||||
branchLink,
|
||||
|
||||
@@ -28,8 +28,8 @@ export type ParsedGitHubContext = {
|
||||
inputs: {
|
||||
triggerPhrase: string;
|
||||
assigneeTrigger: string;
|
||||
allowedTools: string[];
|
||||
disallowedTools: string[];
|
||||
allowedTools: string;
|
||||
disallowedTools: string;
|
||||
customInstructions: string;
|
||||
directPrompt: string;
|
||||
baseBranch?: string;
|
||||
@@ -52,14 +52,8 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
||||
inputs: {
|
||||
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
||||
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
||||
allowedTools: (process.env.ALLOWED_TOOLS ?? "")
|
||||
.split(",")
|
||||
.map((tool) => tool.trim())
|
||||
.filter((tool) => tool.length > 0),
|
||||
disallowedTools: (process.env.DISALLOWED_TOOLS ?? "")
|
||||
.split(",")
|
||||
.map((tool) => tool.trim())
|
||||
.filter((tool) => tool.length > 0),
|
||||
allowedTools: process.env.ALLOWED_TOOLS ?? "",
|
||||
disallowedTools: process.env.DISALLOWED_TOOLS ?? "",
|
||||
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
||||
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
||||
baseBranch: process.env.BASE_BRANCH,
|
||||
|
||||
@@ -9,6 +9,7 @@ export type ExecutionDetails = {
|
||||
export type CommentUpdateInput = {
|
||||
currentBody: string;
|
||||
actionFailed: boolean;
|
||||
actionCancelled?: boolean;
|
||||
executionDetails: ExecutionDetails | null;
|
||||
jobUrl: string;
|
||||
branchLink?: string;
|
||||
@@ -74,6 +75,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
branchLink,
|
||||
prLink,
|
||||
actionFailed,
|
||||
actionCancelled,
|
||||
branchName,
|
||||
triggerUsername,
|
||||
errorDetails,
|
||||
@@ -112,7 +114,13 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
// Build the header
|
||||
let header = "";
|
||||
|
||||
if (actionFailed) {
|
||||
if (actionCancelled) {
|
||||
header = "**Claude's task was cancelled";
|
||||
if (durationStr) {
|
||||
header += ` after ${durationStr}`;
|
||||
}
|
||||
header += "**";
|
||||
} else if (actionFailed) {
|
||||
header = "**Claude encountered an error";
|
||||
if (durationStr) {
|
||||
header += ` after ${durationStr}`;
|
||||
|
||||
@@ -7,7 +7,6 @@ type PrepareConfigParams = {
|
||||
branch: string;
|
||||
additionalMcpConfig?: string;
|
||||
claudeCommentId?: string;
|
||||
allowedTools: string[];
|
||||
};
|
||||
|
||||
export async function prepareMcpConfig(
|
||||
@@ -20,17 +19,24 @@ export async function prepareMcpConfig(
|
||||
branch,
|
||||
additionalMcpConfig,
|
||||
claudeCommentId,
|
||||
allowedTools,
|
||||
} = params;
|
||||
try {
|
||||
const allowedToolsList = allowedTools || [];
|
||||
|
||||
const hasGitHubMcpTools = allowedToolsList.some((tool) =>
|
||||
tool.startsWith("mcp__github__"),
|
||||
);
|
||||
|
||||
const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
|
||||
const baseMcpConfig = {
|
||||
mcpServers: {
|
||||
github: {
|
||||
command: "docker",
|
||||
args: [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/anthropics/github-mcp-server:sha-7382253",
|
||||
],
|
||||
env: {
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||
},
|
||||
},
|
||||
github_file_ops: {
|
||||
command: "bun",
|
||||
args: [
|
||||
@@ -51,23 +57,6 @@ export async function prepareMcpConfig(
|
||||
},
|
||||
};
|
||||
|
||||
if (hasGitHubMcpTools) {
|
||||
baseMcpConfig.mcpServers.github = {
|
||||
command: "docker",
|
||||
args: [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-e9f748f", // https://github.com/github/github-mcp-server/releases/tag/v0.4.0
|
||||
],
|
||||
env: {
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Merge with additional MCP config if provided
|
||||
if (additionalMcpConfig && additionalMcpConfig.trim()) {
|
||||
try {
|
||||
|
||||
@@ -418,4 +418,48 @@ describe("updateCommentBody", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancellation handling", () => {
|
||||
it("shows cancellation message when actionCancelled is true", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
actionCancelled: true,
|
||||
executionDetails: { duration_ms: 30000 }, // 30s
|
||||
triggerUsername: "test-user",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain("**Claude's task was cancelled after 30s**");
|
||||
expect(result).not.toContain("Claude encountered an error");
|
||||
expect(result).not.toContain("Claude finished");
|
||||
});
|
||||
|
||||
it("shows cancellation message without duration when duration is missing", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
actionCancelled: true,
|
||||
triggerUsername: "test-user",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain("**Claude's task was cancelled**");
|
||||
expect(result).not.toContain("after");
|
||||
});
|
||||
|
||||
it("prioritizes cancellation over failure", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
actionCancelled: true,
|
||||
actionFailed: true, // Both are true, cancellation should take precedence
|
||||
executionDetails: { duration_ms: 45000 },
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain("**Claude's task was cancelled after 45s**");
|
||||
expect(result).not.toContain("Claude encountered an error");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -652,7 +652,7 @@ describe("buildAllowedToolsString", () => {
|
||||
});
|
||||
|
||||
test("should append custom tools when provided", () => {
|
||||
const customTools = ["Tool1", "Tool2", "Tool3"];
|
||||
const customTools = "Tool1,Tool2,Tool3";
|
||||
const result = buildAllowedToolsString(customTools);
|
||||
|
||||
// Base tools should be present
|
||||
@@ -683,7 +683,7 @@ describe("buildDisallowedToolsString", () => {
|
||||
});
|
||||
|
||||
test("should append custom disallowed tools when provided", () => {
|
||||
const customDisallowedTools = ["BadTool1", "BadTool2"];
|
||||
const customDisallowedTools = "BadTool1,BadTool2";
|
||||
const result = buildDisallowedToolsString(customDisallowedTools);
|
||||
|
||||
// Base disallowed tools should be present
|
||||
@@ -701,8 +701,8 @@ describe("buildDisallowedToolsString", () => {
|
||||
});
|
||||
|
||||
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
|
||||
const customDisallowedTools = ["BadTool1", "BadTool2"];
|
||||
const allowedTools = ["WebSearch", "SomeOtherTool"];
|
||||
const customDisallowedTools = "BadTool1,BadTool2";
|
||||
const allowedTools = "WebSearch,SomeOtherTool";
|
||||
const result = buildDisallowedToolsString(
|
||||
customDisallowedTools,
|
||||
allowedTools,
|
||||
@@ -720,7 +720,7 @@ describe("buildDisallowedToolsString", () => {
|
||||
});
|
||||
|
||||
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
|
||||
const allowedTools = ["WebSearch", "WebFetch", "SomeOtherTool"];
|
||||
const allowedTools = "WebSearch,WebFetch,SomeOtherTool";
|
||||
const result = buildDisallowedToolsString(undefined, allowedTools);
|
||||
|
||||
// Both hardcoded disallowed tools should be removed
|
||||
@@ -732,8 +732,8 @@ describe("buildDisallowedToolsString", () => {
|
||||
});
|
||||
|
||||
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
|
||||
const customDisallowedTools = ["BadTool1", "BadTool2"];
|
||||
const allowedTools = ["WebSearch", "WebFetch"];
|
||||
const customDisallowedTools = "BadTool1,BadTool2";
|
||||
const allowedTools = "WebSearch,WebFetch";
|
||||
const result = buildDisallowedToolsString(
|
||||
customDisallowedTools,
|
||||
allowedTools,
|
||||
|
||||
@@ -24,19 +24,21 @@ describe("prepareMcpConfig", () => {
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should return base config when no additional config is provided and no allowed_tools", async () => {
|
||||
test("should return base config when no additional config is provided", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
@@ -47,60 +49,6 @@ describe("prepareMcpConfig", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("should include github MCP server when mcp__github__ tools are allowed", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
});
|
||||
|
||||
test("should not include github MCP server when only file_ops tools are allowed", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
allowedTools: [
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__update_claude_comment",
|
||||
],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should include file_ops server even when no GitHub tools are allowed", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
allowedTools: ["Edit", "Read", "Write"],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should return base config when additional config is empty string", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
@@ -108,12 +56,11 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: "",
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -125,12 +72,11 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: " \n\t ",
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -154,10 +100,6 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -191,10 +133,6 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -231,13 +169,12 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.customProperty).toBe("custom-value");
|
||||
expect(parsed.anotherProperty).toEqual({ nested: "value" });
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -250,14 +187,13 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: invalidJson,
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -270,7 +206,6 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: nonObjectJson,
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -280,7 +215,7 @@ describe("prepareMcpConfig", () => {
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -293,7 +228,6 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: nullJson,
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -303,7 +237,7 @@ describe("prepareMcpConfig", () => {
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -316,7 +250,6 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: arrayJson,
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -325,7 +258,7 @@ describe("prepareMcpConfig", () => {
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
|
||||
expect(parsed[0]).toBe(1);
|
||||
@@ -362,13 +295,12 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.server1).toBeDefined();
|
||||
expect(parsed.mcpServers.server2).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
|
||||
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
|
||||
expect(parsed.otherConfig.nested.deeply).toBe("value");
|
||||
@@ -383,7 +315,6 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -403,7 +334,6 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
allowedTools: [],
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
@@ -11,8 +11,8 @@ const defaultInputs = {
|
||||
triggerPhrase: "/claude",
|
||||
assigneeTrigger: "",
|
||||
anthropicModel: "claude-3-7-sonnet-20250219",
|
||||
allowedTools: [] as string[],
|
||||
disallowedTools: [] as string[],
|
||||
allowedTools: "",
|
||||
disallowedTools: "",
|
||||
customInstructions: "",
|
||||
directPrompt: "",
|
||||
useBedrock: false,
|
||||
|
||||
@@ -62,8 +62,8 @@ describe("checkWritePermissions", () => {
|
||||
inputs: {
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
allowedTools: "",
|
||||
disallowedTools: "",
|
||||
customInstructions: "",
|
||||
directPrompt: "",
|
||||
},
|
||||
|
||||
@@ -242,7 +242,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
...mockPullRequestCommentContext,
|
||||
inputs: {
|
||||
...mockPullRequestCommentContext.inputs,
|
||||
allowedTools: ["Tool1", "Tool2"],
|
||||
allowedTools: "Tool1,Tool2",
|
||||
},
|
||||
});
|
||||
const result = prepareContext(contextWithAllowedTools, "12345");
|
||||
|
||||
@@ -30,8 +30,8 @@ describe("checkContainsTrigger", () => {
|
||||
triggerPhrase: "/claude",
|
||||
assigneeTrigger: "",
|
||||
directPrompt: "Fix the bug in the login form",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
allowedTools: "",
|
||||
disallowedTools: "",
|
||||
customInstructions: "",
|
||||
},
|
||||
});
|
||||
@@ -56,8 +56,8 @@ describe("checkContainsTrigger", () => {
|
||||
triggerPhrase: "/claude",
|
||||
assigneeTrigger: "",
|
||||
directPrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
allowedTools: "",
|
||||
disallowedTools: "",
|
||||
customInstructions: "",
|
||||
},
|
||||
});
|
||||
@@ -228,8 +228,8 @@ describe("checkContainsTrigger", () => {
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
directPrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
allowedTools: "",
|
||||
disallowedTools: "",
|
||||
customInstructions: "",
|
||||
},
|
||||
});
|
||||
@@ -255,8 +255,8 @@ describe("checkContainsTrigger", () => {
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
directPrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
allowedTools: "",
|
||||
disallowedTools: "",
|
||||
customInstructions: "",
|
||||
},
|
||||
});
|
||||
@@ -282,8 +282,8 @@ describe("checkContainsTrigger", () => {
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
directPrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
allowedTools: "",
|
||||
disallowedTools: "",
|
||||
customInstructions: "",
|
||||
},
|
||||
});
|
||||
|
||||
160
test/update-comment-link-logic.test.ts
Normal file
160
test/update-comment-link-logic.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||
|
||||
describe("update-comment-link workflow status detection", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
test("should detect prepare step failure", () => {
|
||||
process.env.PREPARE_SUCCESS = "false";
|
||||
process.env.PREPARE_ERROR = "Failed to fetch issue data";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
let errorDetails: string | undefined;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
errorDetails = prepareError;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
expect(errorDetails).toBe("Failed to fetch issue data");
|
||||
});
|
||||
|
||||
test("should detect claude-code step failure when prepare succeeds", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "false";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
});
|
||||
|
||||
test("should detect success when both steps succeed", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "true";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(false);
|
||||
});
|
||||
|
||||
test("should treat missing CLAUDE_SUCCESS env var as failure", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
delete process.env.CLAUDE_SUCCESS;
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
// When CLAUDE_SUCCESS is undefined, it's not === "true", so claudeSuccess = false
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle undefined PREPARE_SUCCESS as success", () => {
|
||||
delete process.env.PREPARE_SUCCESS;
|
||||
delete process.env.PREPARE_ERROR;
|
||||
process.env.CLAUDE_SUCCESS = "true";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
let actionFailed = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(false);
|
||||
});
|
||||
|
||||
test("should detect cancellation when CLAUDE_CANCELLED is true", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "false";
|
||||
process.env.CLAUDE_CANCELLED = "true";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
|
||||
|
||||
let actionFailed = false;
|
||||
let actionCancelled = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else if (isCancelled) {
|
||||
actionCancelled = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(false);
|
||||
expect(actionCancelled).toBe(true);
|
||||
});
|
||||
|
||||
test("should not detect cancellation when CLAUDE_CANCELLED is false", () => {
|
||||
process.env.PREPARE_SUCCESS = "true";
|
||||
process.env.CLAUDE_SUCCESS = "false";
|
||||
process.env.CLAUDE_CANCELLED = "false";
|
||||
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
const isCancelled = process.env.CLAUDE_CANCELLED === "true";
|
||||
|
||||
let actionFailed = false;
|
||||
let actionCancelled = false;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
} else if (isCancelled) {
|
||||
actionCancelled = true;
|
||||
} else {
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS === "true";
|
||||
actionFailed = !claudeSuccess;
|
||||
}
|
||||
|
||||
expect(actionFailed).toBe(true);
|
||||
expect(actionCancelled).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user