Compare commits

..

6 Commits

Author SHA1 Message Date
Ashwin Bhat
82596f1b28 readme 2025-06-02 08:31:59 -07:00
claude[bot]
8eb3f514bd docs: add mcp_config example with sequential-thinking server
- Add mcp_config to inputs table
- Add example section showing how to use mcp_config with sequential-thinking MCP server
- Include clear explanation that custom servers override built-in servers

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-06-02 15:16:33 +00:00
claude[bot]
7149391b4b test: add comprehensive unit tests for prepareMcpConfig
Add tests covering:
- Basic functionality with no additional config
- Valid JSON merging scenarios
- Invalid JSON handling
- Empty/null config handling
- Server name collision scenarios
- Complex nested configurations
- Environment variable handling

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-05-30 15:28:40 +00:00
claude[bot]
feca21446a refactor: improve MCP config logging per review feedback
- Remove configPreview from error logging to avoid cluttering output
- Add informational log when merging MCP server configurations
- Simplify error message for failed config parsing

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-05-30 14:50:13 +00:00
claude[bot]
67d2d8682a refactor: improve MCP config validation and merging logic
- Add JSON validation to ensure parsed config is an object
- Simplify merge logic with explicit mcpServers merging
- Enhance error logging with config preview for debugging

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-05-30 14:39:54 +00:00
claude[bot]
850023f24a feat: add mcp_config input that merges with existing mcp server
- Add mcp_config input parameter to action.yml 
- Modify prepareMcpConfig() to accept and merge additional config
- Provided config overrides built-in servers in case of naming collisions
- Pass MCP_CONFIG environment variable from action to prepare step

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-05-30 14:30:04 +00:00
15 changed files with 209 additions and 737 deletions

View File

@@ -36,4 +36,3 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."
model: "claude-opus-4-20250514"

2
FAQ.md
View File

@@ -6,7 +6,7 @@ This FAQ addresses common questions and gotchas when using the Claude Code GitHu
### Why doesn't tagging @claude from my automated workflow work?
The `github-actions` user cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user, or use a separate app token of your own. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow.
The `github-actions` user (and other GitHub Apps/bots) cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow.
### Why does Claude say I don't have permission to trigger it?

View File

@@ -65,11 +65,6 @@ jobs:
# trigger_phrase: "/claude"
# Optional: add assignee trigger for issues
# assignee_trigger: "claude"
# Optional: add custom environment variables (YAML format)
# claude_env: |
# NODE_ENV: test
# DEBUG: true
# API_URL: https://api.example.com
```
## Inputs
@@ -90,7 +85,6 @@ jobs:
| `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 | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
@@ -295,22 +289,6 @@ This action is built on top of [`anthropics/claude-code-base-action`](https://gi
## Advanced Configuration
### Custom Environment Variables
You can pass custom environment variables to Claude Code execution using the `claude_env` input. This is useful for CI/test setups that require specific environment variables:
```yaml
- uses: anthropics/claude-code-action@beta
with:
claude_env: |
NODE_ENV: test
CI: true
DATABASE_URL: postgres://test:test@localhost:5432/test_db
# ... other inputs
```
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.
### Custom Tools
By default, Claude only has access to:

View File

@@ -41,8 +41,6 @@ inputs:
default: ""
mcp_config:
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
claude_env:
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
required: false
default: ""
@@ -105,7 +103,7 @@ runs:
- name: Run Claude Code
id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true'
uses: anthropics/claude-code-base-action@1370ac97fbba9bddec20ea2924b5726bf10d8b94 # v0.0.9
uses: anthropics/claude-code-base-action@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8
with:
prompt_file: /tmp/claude-prompts/claude-prompt.txt
allowed_tools: ${{ env.ALLOWED_TOOLS }}
@@ -116,7 +114,6 @@ runs:
use_bedrock: ${{ inputs.use_bedrock }}
use_vertex: ${{ inputs.use_vertex }}
anthropic_api_key: ${{ inputs.anthropic_api_key }}
claude_env: ${{ inputs.claude_env }}
env:
# Model configuration
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}

View File

@@ -31,13 +31,24 @@ const BASE_ALLOWED_TOOLS = [
"Write",
"mcp__github_file_ops__commit_files",
"mcp__github_file_ops__delete_files",
"mcp__github_file_ops__update_claude_comment",
];
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
export function buildAllowedToolsString(customAllowedTools?: string): string {
export function buildAllowedToolsString(
eventData: EventData,
customAllowedTools?: string,
): string {
let baseTools = [...BASE_ALLOWED_TOOLS];
// Add the appropriate comment tool based on event type
if (eventData.eventName === "pull_request_review_comment") {
// For inline PR review comments, only use PR comment tool
baseTools.push("mcp__github__update_pull_request_comment");
} else {
// For all other events (issue comments, PR reviews, issues), use issue comment tool
baseTools.push("mcp__github__update_issue_comment");
}
let allAllowedTools = baseTools.join(",");
if (customAllowedTools) {
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
@@ -436,15 +447,33 @@ ${sanitizeContent(context.directPrompt)}
</direct_prompt>`
: ""
}
${`<comment_tool_info>
IMPORTANT: You have been provided with the mcp__github_file_ops__update_claude_comment tool to update your comment. This tool automatically handles both issue and PR comments.
${
eventData.eventName === "pull_request_review_comment"
? `<comment_tool_info>
IMPORTANT: For this inline PR review comment, you have been provided with ONLY the mcp__github__update_pull_request_comment tool to update this specific review comment.
Tool usage example for mcp__github_file_ops__update_claude_comment:
Tool usage example for mcp__github__update_pull_request_comment:
{
"owner": "${context.repository.split("/")[0]}",
"repo": "${context.repository.split("/")[1]}",
"commentId": ${eventData.commentId || context.claudeCommentId},
"body": "Your comment text here"
}
Only the body parameter is required - the tool automatically knows which comment to update.
</comment_tool_info>`}
All four parameters (owner, repo, commentId, body) are required.
</comment_tool_info>`
: `<comment_tool_info>
IMPORTANT: For this event type, you have been provided with ONLY the mcp__github__update_issue_comment tool to update comments.
Tool usage example for mcp__github__update_issue_comment:
{
"owner": "${context.repository.split("/")[0]}",
"repo": "${context.repository.split("/")[1]}",
"commentId": ${context.claudeCommentId},
"body": "Your comment text here"
}
All four parameters (owner, repo, commentId, body) are required.
</comment_tool_info>`
}
Your task is to analyze the context, understand the request, and provide helpful responses and/or implement code changes as needed.
@@ -458,7 +487,7 @@ Follow these steps:
1. Create a Todo List:
- Use your GitHub comment to maintain a detailed task list based on the request.
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
- Update the comment using mcp__github_file_ops__update_claude_comment with each task completion.
- Update the comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__github__update_pull_request_comment" : "mcp__github__update_issue_comment"} with each task completion.
2. Gather Context:
- Analyze the pre-fetched data provided above.
@@ -488,11 +517,11 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
- Look for bugs, security issues, performance problems, and other issues
- Suggest improvements for readability and maintainability
- Check for best practices and coding standards
- Reference specific code sections with file paths and line numbers${eventData.isPR ? "\n - AFTER reading files and analyzing code, you MUST call mcp__github_file_ops__update_claude_comment to post your review" : ""}
- Reference specific code sections with file paths and line numbers${eventData.isPR ? "\n - AFTER reading files and analyzing code, you MUST call mcp__github__update_issue_comment to post your review" : ""}
- Formulate a concise, technical, and helpful response based on the context.
- Reference specific code with inline formatting or code blocks.
- Include relevant file paths and line numbers when applicable.
- ${eventData.isPR ? "IMPORTANT: Submit your review feedback by updating the Claude comment using mcp__github_file_ops__update_claude_comment. This will be displayed as your PR review." : "Remember that this feedback must be posted to the GitHub comment using mcp__github_file_ops__update_claude_comment."}
- ${eventData.isPR ? "IMPORTANT: Submit your review feedback by updating the Claude comment. This will be displayed as your PR review." : "Remember that this feedback must be posted to the GitHub comment."}
B. For Straightforward Changes:
- Use file system tools to make the change locally.
@@ -547,8 +576,8 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
Important Notes:
- All communication must happen through GitHub PR comments.
- Never create new comments. Only update the existing comment using mcp__github_file_ops__update_claude_comment.
- This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__github_file_ops__update_claude_comment. Do NOT just respond with a normal response, the user will not see it." : ""}
- Never create new comments. Only update the existing comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__github__update_pull_request_comment" : "mcp__github__update_issue_comment"} with comment_id: ${context.claudeCommentId}.
- This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__github__update_issue_comment. Do NOT just respond with a normal response, the user will not see it." : ""}
- You communicate exclusively by editing your single comment - not through any other means.
- Use this spinner HTML when work is in progress: <img src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch || "the created branch"}). Never create new branches when triggered on issues or closed/merged PRs.`}
@@ -636,6 +665,7 @@ export async function createPrompt(
// Set allowed tools
const allAllowedTools = buildAllowedToolsString(
preparedContext.eventData,
preparedContext.allowedTools,
);
const allDisallowedTools = buildDisallowedToolsString(

View File

@@ -85,14 +85,13 @@ async function run() {
// Step 11: Get MCP configuration
const additionalMcpConfig = process.env.MCP_CONFIG || "";
const mcpConfig = await prepareMcpConfig({
const mcpConfig = await prepareMcpConfig(
githubToken,
owner: context.repository.owner,
repo: context.repository.repo,
branch: branchInfo.currentBranch,
context.repository.owner,
context.repository.repo,
branchInfo.currentBranch,
additionalMcpConfig,
claudeCommentId: commentId.toString(),
});
);
core.setOutput("mcp_config", mcpConfig);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);

View File

@@ -12,7 +12,6 @@ import {
} from "../github/context";
import { GITHUB_SERVER_URL } from "../github/api/config";
import { checkAndDeleteEmptyBranch } from "../github/operations/branch-cleanup";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
async function run() {
try {
@@ -167,7 +166,7 @@ async function run() {
if (Array.isArray(outputData) && outputData.length > 0) {
const lastElement = outputData[outputData.length - 1];
if (
lastElement.type === "result" &&
lastElement.role === "system" &&
"cost_usd" in lastElement &&
"duration_ms" in lastElement
) {
@@ -205,14 +204,23 @@ async function run() {
const updatedBody = updateCommentBody(commentInput);
// Update the comment using the appropriate API
try {
await updateClaudeComment(octokit.rest, {
owner,
repo,
commentId,
body: updatedBody,
isPullRequestReviewComment: isPRReviewComment,
});
if (isPRReviewComment) {
await octokit.rest.pulls.updateReviewComment({
owner,
repo,
comment_id: commentId,
body: updatedBody,
});
} else {
await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body: updatedBody,
});
}
console.log(
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
);

View File

@@ -9,10 +9,7 @@ export type Octokits = {
export function createOctokit(token: string): Octokits {
return {
rest: new Octokit({
auth: token,
baseUrl: GITHUB_API_URL,
}),
rest: new Octokit({ auth: token }),
graphql: graphql.defaults({
baseUrl: GITHUB_API_URL,
headers: {

View File

@@ -1,70 +0,0 @@
import { Octokit } from "@octokit/rest";
export type UpdateClaudeCommentParams = {
owner: string;
repo: string;
commentId: number;
body: string;
isPullRequestReviewComment: boolean;
};
export type UpdateClaudeCommentResult = {
id: number;
html_url: string;
updated_at: string;
};
/**
* Updates a Claude comment on GitHub (either an issue/PR comment or a PR review comment)
*
* @param octokit - Authenticated Octokit instance
* @param params - Parameters for updating the comment
* @returns The updated comment details
* @throws Error if the update fails
*/
export async function updateClaudeComment(
octokit: Octokit,
params: UpdateClaudeCommentParams,
): Promise<UpdateClaudeCommentResult> {
const { owner, repo, commentId, body, isPullRequestReviewComment } = params;
let response;
try {
if (isPullRequestReviewComment) {
// Try PR review comment API first
response = await octokit.rest.pulls.updateReviewComment({
owner,
repo,
comment_id: commentId,
body,
});
} else {
// Use issue comment API (works for both issues and PR general comments)
response = await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body,
});
}
} catch (error: any) {
// If PR review comment update fails with 404, fall back to issue comment API
if (isPullRequestReviewComment && error.status === 404) {
response = await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body,
});
} else {
throw error;
}
}
return {
id: response.data.id,
html_url: response.data.html_url,
updated_at: response.data.updated_at,
};
}

View File

@@ -15,7 +15,6 @@ import {
isPullRequestReviewCommentEvent,
type ParsedGitHubContext,
} from "../../context";
import { updateClaudeComment } from "./update-claude-comment";
export async function updateTrackingComment(
octokit: Octokits,
@@ -37,19 +36,25 @@ export async function updateTrackingComment(
// Update the existing comment with the branch link
try {
const isPRReviewComment = isPullRequestReviewCommentEvent(context);
await updateClaudeComment(octokit.rest, {
owner,
repo,
commentId,
body: updatedBody,
isPullRequestReviewComment: isPRReviewComment,
});
console.log(
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with branch link`,
);
if (isPullRequestReviewCommentEvent(context)) {
// For PR review comments (inline comments), use the pulls API
await octokit.rest.pulls.updateReviewComment({
owner,
repo,
comment_id: commentId,
body: updatedBody,
});
console.log(`✅ Updated PR review comment ${commentId} with branch link`);
} else {
// For all other comments, use the issues API
await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: commentId,
body: updatedBody,
});
console.log(`✅ Updated issue comment ${commentId} with branch link`);
}
} catch (error) {
console.error("Error updating comment with branch link:", error);
throw error;

View File

@@ -7,8 +7,6 @@ import { readFile } from "fs/promises";
import { join } from "path";
import fetch from "node-fetch";
import { GITHUB_API_URL } from "../github/api/config";
import { Octokit } from "@octokit/rest";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
type GitHubRef = {
object: {
@@ -441,69 +439,6 @@ server.tool(
},
);
server.tool(
"update_claude_comment",
"Update the Claude comment with progress and results (automatically handles both issue and PR comments)",
{
body: z.string().describe("The updated comment content"),
},
async ({ body }) => {
try {
const githubToken = process.env.GITHUB_TOKEN;
const claudeCommentId = process.env.CLAUDE_COMMENT_ID;
const eventName = process.env.GITHUB_EVENT_NAME;
if (!githubToken) {
throw new Error("GITHUB_TOKEN environment variable is required");
}
if (!claudeCommentId) {
throw new Error("CLAUDE_COMMENT_ID environment variable is required");
}
const owner = REPO_OWNER;
const repo = REPO_NAME;
const commentId = parseInt(claudeCommentId, 10);
const octokit = new Octokit({
auth: githubToken,
});
const isPullRequestReviewComment =
eventName === "pull_request_review_comment";
const result = await updateClaudeComment(octokit, {
owner,
repo,
commentId,
body,
isPullRequestReviewComment,
});
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);

View File

@@ -1,25 +1,12 @@
import * as core from "@actions/core";
type PrepareConfigParams = {
githubToken: string;
owner: string;
repo: string;
branch: string;
additionalMcpConfig?: string;
claudeCommentId?: string;
};
export async function prepareMcpConfig(
params: PrepareConfigParams,
githubToken: string,
owner: string,
repo: string,
branch: string,
additionalMcpConfig?: string,
): Promise<string> {
const {
githubToken,
owner,
repo,
branch,
additionalMcpConfig,
claudeCommentId,
} = params;
try {
const baseMcpConfig = {
mcpServers: {
@@ -49,9 +36,6 @@ export async function prepareMcpConfig(
REPO_NAME: repo,
BRANCH_NAME: branch,
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
...(claudeCommentId && { CLAUDE_COMMENT_ID: claudeCommentId }),
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
IS_PR: process.env.IS_PR || "false",
},
},
},

View File

@@ -8,6 +8,7 @@ import {
buildDisallowedToolsString,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
import type { EventData } from "../src/create-prompt/types";
describe("generatePrompt", () => {
const mockGitHubData = {
@@ -618,7 +619,15 @@ describe("getEventTypeAndContext", () => {
describe("buildAllowedToolsString", () => {
test("should return issue comment tool for regular events", () => {
const result = buildAllowedToolsString();
const mockEventData: EventData = {
eventName: "issue_comment",
commentId: "123",
isPR: true,
prNumber: "456",
commentBody: "Test comment",
};
const result = buildAllowedToolsString(mockEventData);
// The base tools should be in the result
expect(result).toContain("Edit");
@@ -627,15 +636,22 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
expect(result).toContain("mcp__github_file_ops__update_claude_comment");
expect(result).not.toContain("mcp__github__update_issue_comment");
expect(result).toContain("mcp__github__update_issue_comment");
expect(result).not.toContain("mcp__github__update_pull_request_comment");
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
});
test("should return PR comment tool for inline review comments", () => {
const result = buildAllowedToolsString();
const mockEventData: EventData = {
eventName: "pull_request_review_comment",
isPR: true,
prNumber: "456",
commentBody: "Test review comment",
commentId: "789",
};
const result = buildAllowedToolsString(mockEventData);
// The base tools should be in the result
expect(result).toContain("Edit");
@@ -644,16 +660,23 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
expect(result).toContain("mcp__github_file_ops__update_claude_comment");
expect(result).not.toContain("mcp__github__update_issue_comment");
expect(result).not.toContain("mcp__github__update_pull_request_comment");
expect(result).toContain("mcp__github__update_pull_request_comment");
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
});
test("should append custom tools when provided", () => {
const mockEventData: EventData = {
eventName: "issue_comment",
commentId: "123",
isPR: true,
prNumber: "456",
commentBody: "Test comment",
};
const customTools = "Tool1,Tool2,Tool3";
const result = buildAllowedToolsString(customTools);
const result = buildAllowedToolsString(mockEventData, customTools);
// Base tools should be present
expect(result).toContain("Edit");

View File

@@ -25,12 +25,12 @@ describe("prepareMcpConfig", () => {
});
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",
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
);
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
@@ -50,13 +50,13 @@ describe("prepareMcpConfig", () => {
});
test("should return base config when additional config is empty string", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: "",
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
"",
);
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
@@ -66,13 +66,13 @@ describe("prepareMcpConfig", () => {
});
test("should return base config when additional config is whitespace only", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: " \n\t ",
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
" \n\t ",
);
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
@@ -94,13 +94,13 @@ describe("prepareMcpConfig", () => {
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
additionalConfig,
);
const parsed = JSON.parse(result);
expect(consoleInfoSpy).toHaveBeenCalledWith(
@@ -127,13 +127,13 @@ describe("prepareMcpConfig", () => {
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
additionalConfig,
);
const parsed = JSON.parse(result);
expect(consoleInfoSpy).toHaveBeenCalledWith(
@@ -163,13 +163,13 @@ describe("prepareMcpConfig", () => {
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
additionalConfig,
);
const parsed = JSON.parse(result);
expect(parsed.customProperty).toBe("custom-value");
@@ -181,13 +181,13 @@ describe("prepareMcpConfig", () => {
test("should handle invalid JSON gracefully", async () => {
const invalidJson = "{ invalid json }";
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: invalidJson,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
invalidJson,
);
const parsed = JSON.parse(result);
expect(consoleWarningSpy).toHaveBeenCalledWith(
@@ -200,13 +200,13 @@ describe("prepareMcpConfig", () => {
test("should handle non-object JSON values", async () => {
const nonObjectJson = JSON.stringify("string value");
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: nonObjectJson,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
nonObjectJson,
);
const parsed = JSON.parse(result);
expect(consoleWarningSpy).toHaveBeenCalledWith(
@@ -222,13 +222,13 @@ describe("prepareMcpConfig", () => {
test("should handle null JSON value", async () => {
const nullJson = JSON.stringify(null);
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: nullJson,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
nullJson,
);
const parsed = JSON.parse(result);
expect(consoleWarningSpy).toHaveBeenCalledWith(
@@ -244,13 +244,13 @@ describe("prepareMcpConfig", () => {
test("should handle array JSON value", async () => {
const arrayJson = JSON.stringify([1, 2, 3]);
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: arrayJson,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
arrayJson,
);
const parsed = JSON.parse(result);
// Arrays are objects in JavaScript, so they pass the object check
@@ -289,13 +289,13 @@ describe("prepareMcpConfig", () => {
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
additionalMcpConfig: additionalConfig,
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
additionalConfig,
);
const parsed = JSON.parse(result);
expect(parsed.mcpServers.server1).toBeDefined();
@@ -310,12 +310,12 @@ describe("prepareMcpConfig", () => {
const oldEnv = process.env.GITHUB_ACTION_PATH;
process.env.GITHUB_ACTION_PATH = "/test/action/path";
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
);
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_file_ops.args[1]).toBe(
@@ -329,12 +329,12 @@ describe("prepareMcpConfig", () => {
const oldEnv = process.env.GITHUB_WORKSPACE;
delete process.env.GITHUB_WORKSPACE;
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
});
const result = await prepareMcpConfig(
"test-token",
"test-owner",
"test-repo",
"test-branch",
);
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_file_ops.env.REPO_DIR).toBe(process.cwd());

View File

@@ -1,413 +0,0 @@
import { describe, test, expect, jest, beforeEach } from "bun:test";
import { Octokit } from "@octokit/rest";
import {
updateClaudeComment,
type UpdateClaudeCommentParams,
} from "../src/github/operations/comments/update-claude-comment";
describe("updateClaudeComment", () => {
let mockOctokit: Octokit;
beforeEach(() => {
mockOctokit = {
rest: {
issues: {
updateComment: jest.fn(),
},
pulls: {
updateReviewComment: jest.fn(),
},
},
} as any as Octokit;
});
test("should update issue comment successfully", async () => {
const mockResponse = {
data: {
id: 123456,
html_url: "https://github.com/owner/repo/issues/1#issuecomment-123456",
updated_at: "2024-01-01T00:00:00Z",
body: "Updated comment",
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 123456,
body: "Updated comment",
isPullRequestReviewComment: false,
};
const result = await updateClaudeComment(mockOctokit, params);
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 123456,
body: "Updated comment",
});
expect(result).toEqual({
id: 123456,
html_url: "https://github.com/owner/repo/issues/1#issuecomment-123456",
updated_at: "2024-01-01T00:00:00Z",
});
});
test("should update PR comment successfully", async () => {
const mockResponse = {
data: {
id: 789012,
html_url: "https://github.com/owner/repo/pull/2#issuecomment-789012",
updated_at: "2024-01-02T00:00:00Z",
body: "Updated PR comment",
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 789012,
body: "Updated PR comment",
isPullRequestReviewComment: false,
};
const result = await updateClaudeComment(mockOctokit, params);
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 789012,
body: "Updated PR comment",
});
expect(result).toEqual({
id: 789012,
html_url: "https://github.com/owner/repo/pull/2#issuecomment-789012",
updated_at: "2024-01-02T00:00:00Z",
});
});
test("should update PR review comment successfully", async () => {
const mockResponse = {
data: {
id: 345678,
html_url: "https://github.com/owner/repo/pull/3#discussion_r345678",
updated_at: "2024-01-03T00:00:00Z",
body: "Updated review comment",
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 345678,
body: "Updated review comment",
isPullRequestReviewComment: true,
};
const result = await updateClaudeComment(mockOctokit, params);
expect(mockOctokit.rest.pulls.updateReviewComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 345678,
body: "Updated review comment",
});
expect(result).toEqual({
id: 345678,
html_url: "https://github.com/owner/repo/pull/3#discussion_r345678",
updated_at: "2024-01-03T00:00:00Z",
});
});
test("should fallback to issue comment API when PR review comment update fails with 404", async () => {
const mockError = new Error("Not Found") as any;
mockError.status = 404;
const mockResponse = {
data: {
id: 456789,
html_url: "https://github.com/owner/repo/pull/4#issuecomment-456789",
updated_at: "2024-01-04T00:00:00Z",
body: "Updated via fallback",
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockRejectedValue(mockError);
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 456789,
body: "Updated via fallback",
isPullRequestReviewComment: true,
};
const result = await updateClaudeComment(mockOctokit, params);
expect(mockOctokit.rest.pulls.updateReviewComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 456789,
body: "Updated via fallback",
});
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 456789,
body: "Updated via fallback",
});
expect(result).toEqual({
id: 456789,
html_url: "https://github.com/owner/repo/pull/4#issuecomment-456789",
updated_at: "2024-01-04T00:00:00Z",
});
});
test("should propagate error when PR review comment update fails with non-404 error", async () => {
const mockError = new Error("Internal Server Error") as any;
mockError.status = 500;
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockRejectedValue(mockError);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 567890,
body: "This will fail",
isPullRequestReviewComment: true,
};
await expect(updateClaudeComment(mockOctokit, params)).rejects.toEqual(
mockError,
);
expect(mockOctokit.rest.pulls.updateReviewComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 567890,
body: "This will fail",
});
// Ensure fallback wasn't attempted
expect(mockOctokit.rest.issues.updateComment).not.toHaveBeenCalled();
});
test("should propagate error when issue comment update fails", async () => {
const mockError = new Error("Forbidden");
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockRejectedValue(mockError);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 678901,
body: "This will also fail",
isPullRequestReviewComment: false,
};
await expect(updateClaudeComment(mockOctokit, params)).rejects.toEqual(
mockError,
);
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 678901,
body: "This will also fail",
});
});
test("should handle empty body", async () => {
const mockResponse = {
data: {
id: 111222,
html_url: "https://github.com/owner/repo/issues/5#issuecomment-111222",
updated_at: "2024-01-05T00:00:00Z",
body: "",
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 111222,
body: "",
isPullRequestReviewComment: false,
};
const result = await updateClaudeComment(mockOctokit, params);
expect(result).toEqual({
id: 111222,
html_url: "https://github.com/owner/repo/issues/5#issuecomment-111222",
updated_at: "2024-01-05T00:00:00Z",
});
});
test("should handle very long body", async () => {
const longBody = "x".repeat(10000);
const mockResponse = {
data: {
id: 333444,
html_url: "https://github.com/owner/repo/issues/6#issuecomment-333444",
updated_at: "2024-01-06T00:00:00Z",
body: longBody,
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 333444,
body: longBody,
isPullRequestReviewComment: false,
};
const result = await updateClaudeComment(mockOctokit, params);
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 333444,
body: longBody,
});
expect(result).toEqual({
id: 333444,
html_url: "https://github.com/owner/repo/issues/6#issuecomment-333444",
updated_at: "2024-01-06T00:00:00Z",
});
});
test("should handle markdown formatting in body", async () => {
const markdownBody = `
# Header
- List item 1
- List item 2
\`\`\`typescript
const code = "example";
\`\`\`
[Link](https://example.com)
`.trim();
const mockResponse = {
data: {
id: 555666,
html_url: "https://github.com/owner/repo/issues/7#issuecomment-555666",
updated_at: "2024-01-07T00:00:00Z",
body: markdownBody,
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 555666,
body: markdownBody,
isPullRequestReviewComment: false,
};
const result = await updateClaudeComment(mockOctokit, params);
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",
comment_id: 555666,
body: markdownBody,
});
expect(result).toEqual({
id: 555666,
html_url: "https://github.com/owner/repo/issues/7#issuecomment-555666",
updated_at: "2024-01-07T00:00:00Z",
});
});
test("should handle different response data fields", async () => {
const mockResponse = {
data: {
id: 777888,
html_url: "https://github.com/owner/repo/pull/8#discussion_r777888",
updated_at: "2024-01-08T12:30:45Z",
body: "Updated",
// Additional fields that might be in the response
created_at: "2024-01-01T00:00:00Z",
user: { login: "bot" },
node_id: "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDc3Nzg4OA==",
},
};
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockResolvedValue(mockResponse);
const params: UpdateClaudeCommentParams = {
owner: "testowner",
repo: "testrepo",
commentId: 777888,
body: "Updated",
isPullRequestReviewComment: true,
};
const result = await updateClaudeComment(mockOctokit, params);
// Should only return the specific fields we care about
expect(result).toEqual({
id: 777888,
html_url: "https://github.com/owner/repo/pull/8#discussion_r777888",
updated_at: "2024-01-08T12:30:45Z",
});
});
});