mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 15:34:13 +08:00
Compare commits
2 Commits
claude/iss
...
testbranch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b61c881f0e | ||
|
|
e98246e0d1 |
1
.github/workflows/claude.yml
vendored
1
.github/workflows/claude.yml
vendored
@@ -36,4 +36,3 @@ jobs:
|
|||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
|
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."
|
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
2
FAQ.md
@@ -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?
|
### 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?
|
### Why does Claude say I don't have permission to trigger it?
|
||||||
|
|
||||||
|
|||||||
@@ -31,24 +31,13 @@ const BASE_ALLOWED_TOOLS = [
|
|||||||
"Write",
|
"Write",
|
||||||
"mcp__github_file_ops__commit_files",
|
"mcp__github_file_ops__commit_files",
|
||||||
"mcp__github_file_ops__delete_files",
|
"mcp__github_file_ops__delete_files",
|
||||||
|
"mcp__github_file_ops__update_claude_comment",
|
||||||
];
|
];
|
||||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||||
|
|
||||||
export function buildAllowedToolsString(
|
export function buildAllowedToolsString(customAllowedTools?: string): string {
|
||||||
eventData: EventData,
|
|
||||||
customAllowedTools?: string,
|
|
||||||
): string {
|
|
||||||
let baseTools = [...BASE_ALLOWED_TOOLS];
|
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(",");
|
let allAllowedTools = baseTools.join(",");
|
||||||
if (customAllowedTools) {
|
if (customAllowedTools) {
|
||||||
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
|
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
|
||||||
@@ -361,7 +350,6 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
|||||||
export function generatePrompt(
|
export function generatePrompt(
|
||||||
context: PreparedContext,
|
context: PreparedContext,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
isReusedBranch?: boolean,
|
|
||||||
): string {
|
): string {
|
||||||
const {
|
const {
|
||||||
contextData,
|
contextData,
|
||||||
@@ -448,33 +436,15 @@ ${sanitizeContent(context.directPrompt)}
|
|||||||
</direct_prompt>`
|
</direct_prompt>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
${
|
${`<comment_tool_info>
|
||||||
eventData.eventName === "pull_request_review_comment"
|
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.
|
||||||
? `<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__update_pull_request_comment:
|
Tool usage example for mcp__github_file_ops__update_claude_comment:
|
||||||
{
|
{
|
||||||
"owner": "${context.repository.split("/")[0]}",
|
|
||||||
"repo": "${context.repository.split("/")[1]}",
|
|
||||||
"commentId": ${eventData.commentId || context.claudeCommentId},
|
|
||||||
"body": "Your comment text here"
|
"body": "Your comment text here"
|
||||||
}
|
}
|
||||||
All four parameters (owner, repo, commentId, body) are required.
|
Only the body parameter is required - the tool automatically knows which comment to update.
|
||||||
</comment_tool_info>`
|
</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.
|
Your task is to analyze the context, understand the request, and provide helpful responses and/or implement code changes as needed.
|
||||||
|
|
||||||
@@ -488,7 +458,7 @@ Follow these steps:
|
|||||||
1. Create a Todo List:
|
1. Create a Todo List:
|
||||||
- Use your GitHub comment to maintain a detailed task list based on the request.
|
- Use your GitHub comment to maintain a detailed task list based on the request.
|
||||||
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
|
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
|
||||||
- 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.
|
- Update the comment using mcp__github_file_ops__update_claude_comment with each task completion.
|
||||||
|
|
||||||
2. Gather Context:
|
2. Gather Context:
|
||||||
- Analyze the pre-fetched data provided above.
|
- Analyze the pre-fetched data provided above.
|
||||||
@@ -518,11 +488,11 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- Look for bugs, security issues, performance problems, and other issues
|
- Look for bugs, security issues, performance problems, and other issues
|
||||||
- Suggest improvements for readability and maintainability
|
- Suggest improvements for readability and maintainability
|
||||||
- Check for best practices and coding standards
|
- 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__update_issue_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_file_ops__update_claude_comment to post your review" : ""}
|
||||||
- Formulate a concise, technical, and helpful response based on the context.
|
- Formulate a concise, technical, and helpful response based on the context.
|
||||||
- Reference specific code with inline formatting or code blocks.
|
- Reference specific code with inline formatting or code blocks.
|
||||||
- Include relevant file paths and line numbers when applicable.
|
- Include relevant file paths and line numbers when applicable.
|
||||||
- ${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."}
|
- ${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."}
|
||||||
|
|
||||||
B. For Straightforward Changes:
|
B. For Straightforward Changes:
|
||||||
- Use file system tools to make the change locally.
|
- Use file system tools to make the change locally.
|
||||||
@@ -535,7 +505,7 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.`
|
- When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.`
|
||||||
: `
|
: `
|
||||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.${isReusedBranch ? `\n - NOTE: This branch (${eventData.claudeBranch}) was reused from a previous Claude invocation on this issue. It may already contain some work.` : ''}
|
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
||||||
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.
|
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.
|
||||||
@@ -577,8 +547,8 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
|
|
||||||
Important Notes:
|
Important Notes:
|
||||||
- All communication must happen through GitHub PR comments.
|
- All communication must happen through GitHub PR comments.
|
||||||
- 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}.
|
- 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__update_issue_comment. Do NOT just respond with a normal response, the user will not see it." : ""}
|
- 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." : ""}
|
||||||
- You communicate exclusively by editing your single comment - not through any other means.
|
- 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;" />
|
- 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.`}
|
${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.`}
|
||||||
@@ -642,7 +612,6 @@ export async function createPrompt(
|
|||||||
claudeBranch: string | undefined,
|
claudeBranch: string | undefined,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
isReusedBranch?: boolean,
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const preparedContext = prepareContext(
|
const preparedContext = prepareContext(
|
||||||
@@ -655,7 +624,7 @@ export async function createPrompt(
|
|||||||
await mkdir("/tmp/claude-prompts", { recursive: true });
|
await mkdir("/tmp/claude-prompts", { recursive: true });
|
||||||
|
|
||||||
// Generate the prompt
|
// Generate the prompt
|
||||||
const promptContent = generatePrompt(preparedContext, githubData, isReusedBranch);
|
const promptContent = generatePrompt(preparedContext, githubData);
|
||||||
|
|
||||||
// Log the final prompt to console
|
// Log the final prompt to console
|
||||||
console.log("===== FINAL PROMPT =====");
|
console.log("===== FINAL PROMPT =====");
|
||||||
@@ -667,7 +636,6 @@ export async function createPrompt(
|
|||||||
|
|
||||||
// Set allowed tools
|
// Set allowed tools
|
||||||
const allAllowedTools = buildAllowedToolsString(
|
const allAllowedTools = buildAllowedToolsString(
|
||||||
preparedContext.eventData,
|
|
||||||
preparedContext.allowedTools,
|
preparedContext.allowedTools,
|
||||||
);
|
);
|
||||||
const allDisallowedTools = buildDisallowedToolsString(
|
const allDisallowedTools = buildDisallowedToolsString(
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ async function run() {
|
|||||||
branchInfo.claudeBranch,
|
branchInfo.claudeBranch,
|
||||||
githubData,
|
githubData,
|
||||||
context,
|
context,
|
||||||
branchInfo.isReusedBranch,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 11: Get MCP configuration
|
// Step 11: Get MCP configuration
|
||||||
@@ -90,6 +89,7 @@ async function run() {
|
|||||||
context.repository.owner,
|
context.repository.owner,
|
||||||
context.repository.repo,
|
context.repository.repo,
|
||||||
branchInfo.currentBranch,
|
branchInfo.currentBranch,
|
||||||
|
commentId.toString(),
|
||||||
);
|
);
|
||||||
core.setOutput("mcp_config", mcpConfig);
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -87,29 +87,13 @@ async function run() {
|
|||||||
const currentBody = comment.body ?? "";
|
const currentBody = comment.body ?? "";
|
||||||
|
|
||||||
// Check if we need to add branch link for new branches
|
// Check if we need to add branch link for new branches
|
||||||
// For issues, we don't delete branches anymore to allow reuse
|
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
|
||||||
const skipBranchDeletion = !context.isPR && claudeBranch;
|
octokit,
|
||||||
|
owner,
|
||||||
let shouldDeleteBranch = false;
|
repo,
|
||||||
let branchLink = "";
|
claudeBranch,
|
||||||
|
baseBranch,
|
||||||
if (skipBranchDeletion) {
|
);
|
||||||
// For issue branches, just add the branch link without checking for deletion
|
|
||||||
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
|
|
||||||
branchLink = `\n[View branch](${branchUrl})`;
|
|
||||||
console.log(`Keeping issue branch ${claudeBranch} for potential reuse`);
|
|
||||||
} else {
|
|
||||||
// For PR branches, use the existing cleanup logic
|
|
||||||
const result = await checkAndDeleteEmptyBranch(
|
|
||||||
octokit,
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
claudeBranch,
|
|
||||||
baseBranch,
|
|
||||||
);
|
|
||||||
shouldDeleteBranch = result.shouldDeleteBranch;
|
|
||||||
branchLink = result.branchLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we need to add PR URL when we have a new branch
|
// Check if we need to add PR URL when we have a new branch
|
||||||
let prLink = "";
|
let prLink = "";
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export type BranchInfo = {
|
|||||||
baseBranch: string;
|
baseBranch: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
currentBranch: string;
|
currentBranch: string;
|
||||||
isReusedBranch?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function setupBranch(
|
export async function setupBranch(
|
||||||
@@ -80,127 +79,57 @@ export async function setupBranch(
|
|||||||
|
|
||||||
// Creating a new branch for either an issue or closed/merged PR
|
// Creating a new branch for either an issue or closed/merged PR
|
||||||
const entityType = isPR ? "pr" : "issue";
|
const entityType = isPR ? "pr" : "issue";
|
||||||
|
console.log(
|
||||||
// For issues, check if a Claude branch already exists
|
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
||||||
let branchToUse: string | null = null;
|
);
|
||||||
let isReusedBranch = false;
|
|
||||||
|
|
||||||
if (!isPR) {
|
|
||||||
// Check for existing Claude branches for this issue
|
|
||||||
try {
|
|
||||||
// Use GraphQL to efficiently search for branches with a specific prefix
|
|
||||||
const query = `
|
|
||||||
query($owner: String!, $repo: String!, $prefix: String!) {
|
|
||||||
repository(owner: $owner, name: $repo) {
|
|
||||||
refs(refPrefix: "refs/heads/", query: $prefix, first: 100) {
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const response = await octokits.graphql<{
|
|
||||||
repository: {
|
|
||||||
refs: {
|
|
||||||
nodes: Array<{ name: string }>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>(query, {
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
prefix: `claude/issue-${entityNumber}-`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const branches = response.repository.refs.nodes;
|
|
||||||
|
|
||||||
if (branches.length > 0) {
|
|
||||||
// Use the first matching branch (could be sorted by date in future)
|
|
||||||
branchToUse = branches[0].name;
|
|
||||||
isReusedBranch = true;
|
|
||||||
console.log(`Found existing Claude branch for issue #${entityNumber}: ${branchToUse}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error checking for existing branches:", error);
|
|
||||||
// Continue with new branch creation if check fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no existing branch found or this is a PR, create a new branch
|
|
||||||
if (!branchToUse) {
|
|
||||||
console.log(
|
|
||||||
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timestamp = new Date()
|
const timestamp = new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.replace(/[:-]/g, "")
|
.replace(/[:-]/g, "")
|
||||||
.replace(/\.\d{3}Z/, "")
|
.replace(/\.\d{3}Z/, "")
|
||||||
.split("T")
|
.split("T")
|
||||||
.join("_");
|
.join("_");
|
||||||
|
|
||||||
branchToUse = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isReusedBranch) {
|
// Get the SHA of the source branch
|
||||||
// For existing branches, just checkout
|
const sourceBranchRef = await octokits.rest.git.getRef({
|
||||||
console.log(`Checking out existing branch: ${branchToUse}`);
|
owner,
|
||||||
|
repo,
|
||||||
// Fetch the branch with more depth to allow for context
|
ref: `heads/${sourceBranch}`,
|
||||||
await $`git fetch origin --depth=20 ${branchToUse}`;
|
});
|
||||||
await $`git checkout ${branchToUse}`;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Successfully checked out existing branch: ${branchToUse}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`Note: This is a reused branch from a previous Claude invocation on issue #${entityNumber}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Get the SHA of the source branch
|
|
||||||
const sourceBranchRef = await octokits.rest.git.getRef({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
ref: `heads/${sourceBranch}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentSHA = sourceBranchRef.data.object.sha;
|
const currentSHA = sourceBranchRef.data.object.sha;
|
||||||
|
|
||||||
console.log(`Current SHA: ${currentSHA}`);
|
console.log(`Current SHA: ${currentSHA}`);
|
||||||
|
|
||||||
// Create branch using GitHub API
|
// Create branch using GitHub API
|
||||||
await octokits.rest.git.createRef({
|
await octokits.rest.git.createRef({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
ref: `refs/heads/${branchToUse}`,
|
ref: `refs/heads/${newBranch}`,
|
||||||
sha: currentSHA,
|
sha: currentSHA,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Checkout the new branch (shallow fetch for performance)
|
// Checkout the new branch (shallow fetch for performance)
|
||||||
await $`git fetch origin --depth=1 ${branchToUse}`;
|
await $`git fetch origin --depth=1 ${newBranch}`;
|
||||||
await $`git checkout ${branchToUse}`;
|
await $`git checkout ${newBranch}`;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Successfully created and checked out new branch: ${branchToUse}`,
|
`Successfully created and checked out new branch: ${newBranch}`,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Set outputs for GitHub Actions
|
// Set outputs for GitHub Actions
|
||||||
core.setOutput("CLAUDE_BRANCH", branchToUse);
|
core.setOutput("CLAUDE_BRANCH", newBranch);
|
||||||
core.setOutput("BASE_BRANCH", sourceBranch);
|
core.setOutput("BASE_BRANCH", sourceBranch);
|
||||||
if (isReusedBranch) {
|
|
||||||
core.setOutput("IS_REUSED_BRANCH", "true");
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
baseBranch: sourceBranch,
|
baseBranch: sourceBranch,
|
||||||
claudeBranch: branchToUse,
|
claudeBranch: newBranch,
|
||||||
currentBranch: branchToUse,
|
currentBranch: newBranch,
|
||||||
isReusedBranch,
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting up branch:", error);
|
console.error("Error creating branch:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { readFile } from "fs/promises";
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { GITHUB_API_URL } from "../github/api/config";
|
import { GITHUB_API_URL } from "../github/api/config";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
|
||||||
type GitHubRef = {
|
type GitHubRef = {
|
||||||
object: {
|
object: {
|
||||||
@@ -439,6 +440,106 @@ server.tool(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update Claude comment 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);
|
||||||
|
|
||||||
|
// Create Octokit instance
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: githubToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine if this is a PR review comment based on event type
|
||||||
|
const isPullRequestReviewComment =
|
||||||
|
eventName === "pull_request_review_comment";
|
||||||
|
|
||||||
|
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 {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
id: response.data.id,
|
||||||
|
html_url: response.data.html_url,
|
||||||
|
updated_at: response.data.updated_at,
|
||||||
|
},
|
||||||
|
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() {
|
async function runServer() {
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export async function prepareMcpConfig(
|
|||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
branch: string,
|
branch: string,
|
||||||
|
claudeCommentId?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const mcpConfig = {
|
const mcpConfig = {
|
||||||
@@ -35,6 +36,9 @@ export async function prepareMcpConfig(
|
|||||||
REPO_NAME: repo,
|
REPO_NAME: repo,
|
||||||
BRANCH_NAME: branch,
|
BRANCH_NAME: branch,
|
||||||
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
buildDisallowedToolsString,
|
buildDisallowedToolsString,
|
||||||
} from "../src/create-prompt";
|
} from "../src/create-prompt";
|
||||||
import type { PreparedContext } from "../src/create-prompt";
|
import type { PreparedContext } from "../src/create-prompt";
|
||||||
import type { EventData } from "../src/create-prompt/types";
|
|
||||||
|
|
||||||
describe("generatePrompt", () => {
|
describe("generatePrompt", () => {
|
||||||
const mockGitHubData = {
|
const mockGitHubData = {
|
||||||
@@ -619,15 +618,7 @@ describe("getEventTypeAndContext", () => {
|
|||||||
|
|
||||||
describe("buildAllowedToolsString", () => {
|
describe("buildAllowedToolsString", () => {
|
||||||
test("should return issue comment tool for regular events", () => {
|
test("should return issue comment tool for regular events", () => {
|
||||||
const mockEventData: EventData = {
|
const result = buildAllowedToolsString();
|
||||||
eventName: "issue_comment",
|
|
||||||
commentId: "123",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "456",
|
|
||||||
commentBody: "Test comment",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = buildAllowedToolsString(mockEventData);
|
|
||||||
|
|
||||||
// The base tools should be in the result
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
@@ -636,22 +627,15 @@ describe("buildAllowedToolsString", () => {
|
|||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
expect(result).toContain("Write");
|
||||||
expect(result).toContain("mcp__github__update_issue_comment");
|
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).not.toContain("mcp__github__update_pull_request_comment");
|
||||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return PR comment tool for inline review comments", () => {
|
test("should return PR comment tool for inline review comments", () => {
|
||||||
const mockEventData: EventData = {
|
const result = buildAllowedToolsString();
|
||||||
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
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
@@ -660,23 +644,16 @@ describe("buildAllowedToolsString", () => {
|
|||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
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_issue_comment");
|
||||||
expect(result).toContain("mcp__github__update_pull_request_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__commit_files");
|
||||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should append custom tools when provided", () => {
|
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 customTools = "Tool1,Tool2,Tool3";
|
||||||
const result = buildAllowedToolsString(mockEventData, customTools);
|
const result = buildAllowedToolsString(customTools);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
|
|||||||
Reference in New Issue
Block a user