mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 15:34:13 +08:00
Compare commits
4 Commits
testbranch
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ca8faf6b | ||
|
|
e8d2b8d5df | ||
|
|
a8a36ced96 | ||
|
|
180a1b6680 |
1
.github/workflows/claude.yml
vendored
1
.github/workflows/claude.yml
vendored
@@ -36,3 +36,4 @@ 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 (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.
|
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.
|
||||||
|
|
||||||
### 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,13 +31,24 @@ 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(customAllowedTools?: string): string {
|
export function buildAllowedToolsString(
|
||||||
|
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}`;
|
||||||
@@ -350,6 +361,7 @@ 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,
|
||||||
@@ -436,15 +448,33 @@ ${sanitizeContent(context.directPrompt)}
|
|||||||
</direct_prompt>`
|
</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"
|
"body": "Your comment text here"
|
||||||
}
|
}
|
||||||
Only the body parameter is required - the tool automatically knows which comment to update.
|
All four parameters (owner, repo, commentId, body) are required.
|
||||||
</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.
|
||||||
|
|
||||||
@@ -458,7 +488,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 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:
|
2. Gather Context:
|
||||||
- Analyze the pre-fetched data provided above.
|
- Analyze the pre-fetched data provided above.
|
||||||
@@ -488,11 +518,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_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.
|
- 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 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:
|
B. For Straightforward Changes:
|
||||||
- Use file system tools to make the change locally.
|
- Use file system tools to make the change locally.
|
||||||
@@ -505,7 +535,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.
|
- 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.` : ''}
|
||||||
- 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.
|
||||||
@@ -547,8 +577,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 mcp__github_file_ops__update_claude_comment.
|
- 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_file_ops__update_claude_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__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.
|
- 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.`}
|
||||||
@@ -612,6 +642,7 @@ 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(
|
||||||
@@ -624,7 +655,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);
|
const promptContent = generatePrompt(preparedContext, githubData, isReusedBranch);
|
||||||
|
|
||||||
// Log the final prompt to console
|
// Log the final prompt to console
|
||||||
console.log("===== FINAL PROMPT =====");
|
console.log("===== FINAL PROMPT =====");
|
||||||
@@ -636,6 +667,7 @@ 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,6 +81,7 @@ async function run() {
|
|||||||
branchInfo.claudeBranch,
|
branchInfo.claudeBranch,
|
||||||
githubData,
|
githubData,
|
||||||
context,
|
context,
|
||||||
|
branchInfo.isReusedBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 11: Get MCP configuration
|
// Step 11: Get MCP configuration
|
||||||
@@ -89,7 +90,6 @@ 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,13 +87,29 @@ 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
|
||||||
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
|
// For issues, we don't delete branches anymore to allow reuse
|
||||||
octokit,
|
const skipBranchDeletion = !context.isPR && claudeBranch;
|
||||||
owner,
|
|
||||||
repo,
|
let shouldDeleteBranch = false;
|
||||||
claudeBranch,
|
let branchLink = "";
|
||||||
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,6 +17,7 @@ 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(
|
||||||
@@ -79,57 +80,127 @@ 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(
|
|
||||||
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
// For issues, check if a Claude branch already exists
|
||||||
);
|
let branchToUse: string | null = null;
|
||||||
|
let isReusedBranch = false;
|
||||||
const timestamp = new Date()
|
|
||||||
.toISOString()
|
if (!isPR) {
|
||||||
.replace(/[:-]/g, "")
|
// Check for existing Claude branches for this issue
|
||||||
.replace(/\.\d{3}Z/, "")
|
try {
|
||||||
.split("T")
|
// Use GraphQL to efficiently search for branches with a specific prefix
|
||||||
.join("_");
|
const query = `
|
||||||
|
query($owner: String!, $repo: String!, $prefix: String!) {
|
||||||
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
repository(owner: $owner, name: $repo) {
|
||||||
|
refs(refPrefix: "refs/heads/", query: $prefix, first: 100) {
|
||||||
try {
|
nodes {
|
||||||
// Get the SHA of the source branch
|
name
|
||||||
const sourceBranchRef = await octokits.rest.git.getRef({
|
}
|
||||||
owner,
|
}
|
||||||
repo,
|
}
|
||||||
ref: `heads/${sourceBranch}`,
|
}
|
||||||
});
|
`;
|
||||||
|
|
||||||
const currentSHA = sourceBranchRef.data.object.sha;
|
const response = await octokits.graphql<{
|
||||||
|
repository: {
|
||||||
console.log(`Current SHA: ${currentSHA}`);
|
refs: {
|
||||||
|
nodes: Array<{ name: string }>;
|
||||||
// Create branch using GitHub API
|
};
|
||||||
await octokits.rest.git.createRef({
|
};
|
||||||
owner,
|
}>(query, {
|
||||||
repo,
|
owner,
|
||||||
ref: `refs/heads/${newBranch}`,
|
repo,
|
||||||
sha: currentSHA,
|
prefix: `claude/issue-${entityNumber}-`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Checkout the new branch (shallow fetch for performance)
|
const branches = response.repository.refs.nodes;
|
||||||
await $`git fetch origin --depth=1 ${newBranch}`;
|
|
||||||
await $`git checkout ${newBranch}`;
|
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(
|
console.log(
|
||||||
`Successfully created and checked out new branch: ${newBranch}`,
|
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[:-]/g, "")
|
||||||
|
.replace(/\.\d{3}Z/, "")
|
||||||
|
.split("T")
|
||||||
|
.join("_");
|
||||||
|
|
||||||
|
branchToUse = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isReusedBranch) {
|
||||||
|
// For existing branches, just checkout
|
||||||
|
console.log(`Checking out existing branch: ${branchToUse}`);
|
||||||
|
|
||||||
|
// Fetch the branch with more depth to allow for context
|
||||||
|
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;
|
||||||
|
|
||||||
|
console.log(`Current SHA: ${currentSHA}`);
|
||||||
|
|
||||||
|
// Create branch using GitHub API
|
||||||
|
await octokits.rest.git.createRef({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: `refs/heads/${branchToUse}`,
|
||||||
|
sha: currentSHA,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Checkout the new branch (shallow fetch for performance)
|
||||||
|
await $`git fetch origin --depth=1 ${branchToUse}`;
|
||||||
|
await $`git checkout ${branchToUse}`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Successfully created and checked out new branch: ${branchToUse}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Set outputs for GitHub Actions
|
// Set outputs for GitHub Actions
|
||||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
core.setOutput("CLAUDE_BRANCH", branchToUse);
|
||||||
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: newBranch,
|
claudeBranch: branchToUse,
|
||||||
currentBranch: newBranch,
|
currentBranch: branchToUse,
|
||||||
|
isReusedBranch,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating branch:", error);
|
console.error("Error setting up branch:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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: {
|
||||||
@@ -440,106 +439,6 @@ 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,7 +5,6 @@ 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 = {
|
||||||
@@ -36,9 +35,6 @@ 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,6 +8,7 @@ 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 = {
|
||||||
@@ -618,7 +619,15 @@ 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 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
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
@@ -627,15 +636,22 @@ 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).toContain("mcp__github__update_issue_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 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
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
@@ -644,16 +660,23 @@ 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).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__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(customTools);
|
const result = buildAllowedToolsString(mockEventData, 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