Merge pull request #1 from markwylde/feat/give-claude-access-to-switch-branch
Give claude access to switch branch
This commit is contained in:
@@ -56,3 +56,12 @@ src/
|
|||||||
- The action creates branches for issues and pushes to PR branches directly
|
- The action creates branches for issues and pushes to PR branches directly
|
||||||
- All actions create OIDC tokens for secure authentication
|
- All actions create OIDC tokens for secure authentication
|
||||||
- Progress is tracked through dynamic comment updates with checkboxes
|
- Progress is tracked through dynamic comment updates with checkboxes
|
||||||
|
|
||||||
|
## MCP Tool Development
|
||||||
|
|
||||||
|
When adding new MCP tools:
|
||||||
|
|
||||||
|
1. **Add to MCP Server**: Implement the tool in the appropriate MCP server file (e.g., `src/mcp/local-git-ops-server.ts`)
|
||||||
|
2. **Expose to Claude**: Add the tool name to `BASE_ALLOWED_TOOLS` array in `src/create-prompt/index.ts`
|
||||||
|
3. **Tool Naming**: Follow the pattern `mcp__server_name__tool_name` (e.g., `mcp__local_git_ops__checkout_branch`)
|
||||||
|
4. **Documentation**: Update the prompt's "What You CAN Do" section if the tool adds new capabilities
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
| Input | Description | Required | Default |
|
| Input | Description | Required | Default |
|
||||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | ---------- |
|
| --------------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
||||||
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const BASE_ALLOWED_TOOLS = [
|
|||||||
"mcp__local_git_ops__delete_files",
|
"mcp__local_git_ops__delete_files",
|
||||||
"mcp__local_git_ops__push_branch",
|
"mcp__local_git_ops__push_branch",
|
||||||
"mcp__local_git_ops__create_pull_request",
|
"mcp__local_git_ops__create_pull_request",
|
||||||
|
"mcp__local_git_ops__checkout_branch",
|
||||||
|
"mcp__local_git_ops__create_branch",
|
||||||
"mcp__local_git_ops__git_status",
|
"mcp__local_git_ops__git_status",
|
||||||
];
|
];
|
||||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||||
@@ -512,7 +514,20 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- For implementation requests, assess if they are straightforward or complex.
|
- For implementation requests, assess if they are straightforward or complex.
|
||||||
- Mark this todo as complete by checking the box.
|
- Mark this todo as complete by checking the box.
|
||||||
|
|
||||||
4. Execute Actions:
|
${
|
||||||
|
!eventData.isPR || !eventData.claudeBranch
|
||||||
|
? `
|
||||||
|
4. Check for Existing Branch (for issues and closed PRs):
|
||||||
|
- Before implementing changes, check if there's already a claude branch for this ${eventData.isPR ? "PR" : "issue"}.
|
||||||
|
- Use Bash to run \`git branch -r | grep "claude/${eventData.isPR ? "pr" : "issue"}-${eventData.isPR ? eventData.prNumber : eventData.issueNumber}"\` to search for existing branches.
|
||||||
|
- If found, use mcp__local_git_ops__checkout_branch to switch to the existing branch (set fetch_remote=true).
|
||||||
|
- If not found, you'll create a new branch when making changes (see Execute Actions section).
|
||||||
|
- Mark this todo as complete by checking the box.
|
||||||
|
|
||||||
|
5. Execute Actions:`
|
||||||
|
: `
|
||||||
|
4. Execute Actions:`
|
||||||
|
}
|
||||||
- Continually update your todo list as you discover new requirements or realize tasks can be broken down.
|
- Continually update your todo list as you discover new requirements or realize tasks can be broken down.
|
||||||
|
|
||||||
A. For Answering Questions and Code Reviews:
|
A. For Answering Questions and Code Reviews:
|
||||||
@@ -537,15 +552,14 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch
|
- CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch
|
||||||
- When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.local>" 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.local>" line in the commit message.`
|
||||||
: `
|
: eventData.claudeBranch
|
||||||
- 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}). Do not create a new branch.
|
||||||
- Commit changes using mcp__local_git_ops__commit_files (works for both new and existing files)
|
- Commit changes using mcp__local_git_ops__commit_files (works for both new and existing files)
|
||||||
- Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch
|
- CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch
|
||||||
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.local>" 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.local>" line in the commit message.
|
||||||
${
|
- Provide a URL to create a PR manually in this format:
|
||||||
eventData.claudeBranch
|
|
||||||
? `- Provide a URL to create a PR manually in this format:
|
|
||||||
[Create a PR](${GITEA_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
[Create a PR](${GITEA_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
||||||
- IMPORTANT: Use THREE dots (...) between branch names, not two (..)
|
- IMPORTANT: Use THREE dots (...) between branch names, not two (..)
|
||||||
Example: ${GITEA_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
|
Example: ${GITEA_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
|
||||||
@@ -559,8 +573,34 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- Reference to the original ${eventData.isPR ? "PR" : "issue"}
|
- Reference to the original ${eventData.isPR ? "PR" : "issue"}
|
||||||
- The signature: "Generated with [Claude Code](https://claude.ai/code)"
|
- The signature: "Generated with [Claude Code](https://claude.ai/code)"
|
||||||
- Just include the markdown link with text "Create a PR" - do not add explanatory text before it like "You can create a PR using this link"`
|
- Just include the markdown link with text "Create a PR" - do not add explanatory text before it like "You can create a PR using this link"`
|
||||||
: ""
|
: `
|
||||||
}`
|
- IMPORTANT: You are currently on the base branch (${eventData.baseBranch}). Before making changes, you should first check if there's already an existing claude branch for this ${eventData.isPR ? "PR" : "issue"}.
|
||||||
|
- FIRST: Use Bash to run \`git branch -r | grep "claude/${eventData.isPR ? "pr" : "issue"}-${eventData.isPR ? eventData.prNumber : eventData.issueNumber}"\` to check for existing branches.
|
||||||
|
- If an existing claude branch is found:
|
||||||
|
- Use mcp__local_git_ops__checkout_branch to switch to the existing branch (set fetch_remote=true)
|
||||||
|
- Continue working on that branch rather than creating a new one
|
||||||
|
- If NO existing claude branch is found:
|
||||||
|
- Create a new branch using mcp__local_git_ops__create_branch
|
||||||
|
- Use a descriptive branch name following the pattern: claude/${eventData.isPR ? "pr" : "issue"}-${eventData.isPR ? eventData.prNumber : eventData.issueNumber}-<short-description>
|
||||||
|
- Example: claude/issue-123-fix-login-bug or claude/issue-456-add-user-profile
|
||||||
|
- After being on the correct branch (existing or new), commit changes using mcp__local_git_ops__commit_files (works for both new and existing files)
|
||||||
|
- Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
|
- CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch
|
||||||
|
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.local>" line in the commit message.
|
||||||
|
- Provide a URL to create a PR manually in this format:
|
||||||
|
[Create a PR](${GITEA_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
||||||
|
- IMPORTANT: Use THREE dots (...) between branch names, not two (..)
|
||||||
|
Example: ${GITEA_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
|
||||||
|
NOT: ${GITEA_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect)
|
||||||
|
- IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces
|
||||||
|
Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message"
|
||||||
|
- The target-branch should be '${eventData.baseBranch}'.
|
||||||
|
- The branch-name is your created branch name
|
||||||
|
- The body should include:
|
||||||
|
- A clear description of the changes
|
||||||
|
- Reference to the original ${eventData.isPR ? "PR" : "issue"}
|
||||||
|
- The signature: "Generated with [Claude Code](https://claude.ai/code)"
|
||||||
|
- Just include the markdown link with text "Create a PR" - do not add explanatory text before it like "You can create a PR using this link"`
|
||||||
}
|
}
|
||||||
|
|
||||||
C. For Complex Changes:
|
C. For Complex Changes:
|
||||||
@@ -572,12 +612,12 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- Follow the same pushing strategy as for straightforward changes (see section B above).
|
- Follow the same pushing strategy as for straightforward changes (see section B above).
|
||||||
- Or explain why it's too complex: mark todo as completed in checklist with explanation.
|
- Or explain why it's too complex: mark todo as completed in checklist with explanation.
|
||||||
|
|
||||||
5. Final Update:
|
${!eventData.isPR || !eventData.claudeBranch ? `6. Final Update:` : `5. Final Update:`}
|
||||||
- Always update the GitHub comment to reflect the current todo state.
|
- Always update the GitHub comment to reflect the current todo state.
|
||||||
- When all todos are completed, remove the spinner and add a brief summary of what was accomplished, and what was not done.
|
- When all todos are completed, remove the spinner and add a brief summary of what was accomplished, and what was not done.
|
||||||
- Note: If you see previous Claude comments with headers like "**Claude finished @user's task**" followed by "---", do not include this in your comment. The system adds this automatically.
|
- Note: If you see previous Claude comments with headers like "**Claude finished @user's task**" followed by "---", do not include this in your comment. The system adds this automatically.
|
||||||
- If you changed any files locally, you must commit them using mcp__local_git_ops__commit_files AND push the branch using mcp__local_git_ops__push_branch before saying that you're done.
|
- If you changed any files locally, you must commit them using mcp__local_git_ops__commit_files AND push the branch using mcp__local_git_ops__push_branch before saying that you're done.
|
||||||
${eventData.claudeBranch ? `- If you created anything in your branch, your comment must include the PR URL with prefilled title and body mentioned above.` : ""}
|
${!eventData.isPR || !eventData.claudeBranch ? `- If you created a branch and made changes, your comment must include the PR URL with prefilled title and body mentioned above.` : ""}
|
||||||
|
|
||||||
Important Notes:
|
Important Notes:
|
||||||
- All communication must happen through GitHub PR comments.
|
- All communication must happen through GitHub PR comments.
|
||||||
@@ -585,7 +625,7 @@ Important Notes:
|
|||||||
- 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__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://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
|
- Use this spinner HTML when work is in progress: <img src="https://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" 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.` : eventData.claudeBranch ? `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch}). Do not create additional branches.` : `- IMPORTANT: You are currently on the base branch (${eventData.baseBranch}). First check for existing claude branches for this ${eventData.isPR ? "PR" : "issue"} and use them if found, otherwise create a new branch using mcp__local_git_ops__create_branch.`}
|
||||||
- Use mcp__local_git_ops__commit_files for making commits (works for both new and existing files, single or multiple). Use mcp__local_git_ops__delete_files for deleting files (supports deleting single or multiple files atomically), or mcp__github__delete_file for deleting a single file. Edit files locally, and the tool will read the content from the same path on disk.
|
- Use mcp__local_git_ops__commit_files for making commits (works for both new and existing files, single or multiple). Use mcp__local_git_ops__delete_files for deleting files (supports deleting single or multiple files atomically), or mcp__github__delete_file for deleting a single file. Edit files locally, and the tool will read the content from the same path on disk.
|
||||||
Tool usage examples:
|
Tool usage examples:
|
||||||
- mcp__local_git_ops__commit_files: {"files": ["path/to/file1.js", "path/to/file2.py"], "message": "feat: add new feature"}
|
- mcp__local_git_ops__commit_files: {"files": ["path/to/file1.js", "path/to/file2.py"], "message": "feat: add new feature"}
|
||||||
@@ -606,9 +646,10 @@ What You CAN Do:
|
|||||||
- Implement code changes (simple to moderate complexity) when explicitly requested
|
- Implement code changes (simple to moderate complexity) when explicitly requested
|
||||||
- Create pull requests for changes to human-authored code
|
- Create pull requests for changes to human-authored code
|
||||||
- Smart branch handling:
|
- Smart branch handling:
|
||||||
- When triggered on an issue: Always create a new branch
|
- When triggered on an issue: Create a new branch using mcp__local_git_ops__create_branch
|
||||||
- When triggered on an open PR: Always push directly to the existing PR branch
|
- When triggered on an open PR: Push directly to the existing PR branch
|
||||||
- When triggered on a closed PR: Create a new branch
|
- When triggered on a closed PR: Create a new branch using mcp__local_git_ops__create_branch
|
||||||
|
- Create new branches when needed using the create_branch tool
|
||||||
|
|
||||||
What You CANNOT Do:
|
What You CANNOT Do:
|
||||||
- Submit formal GitHub PR reviews
|
- Submit formal GitHub PR reviews
|
||||||
@@ -616,7 +657,7 @@ What You CANNOT Do:
|
|||||||
- Post multiple comments (you only update your initial comment)
|
- Post multiple comments (you only update your initial comment)
|
||||||
- Execute commands outside the repository context
|
- Execute commands outside the repository context
|
||||||
- Run arbitrary Bash commands (unless explicitly allowed via allowed_tools configuration)
|
- Run arbitrary Bash commands (unless explicitly allowed via allowed_tools configuration)
|
||||||
- Perform branch operations (cannot merge branches, rebase, or perform other git operations beyond pushing commits)
|
- Perform advanced branch operations (cannot merge branches, rebase, or perform other complex git operations beyond creating, checking out, and pushing branches)
|
||||||
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
|
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
|
||||||
- View CI/CD results or workflow run outputs (cannot access GitHub Actions logs or test results)
|
- View CI/CD results or workflow run outputs (cannot access GitHub Actions logs or test results)
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ async function run() {
|
|||||||
core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch);
|
core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 9: Update initial comment with branch link (only for issues that created a new branch)
|
// Step 9: Update initial comment with branch link (only if a claude branch was created)
|
||||||
if (branchInfo.claudeBranch) {
|
if (branchInfo.claudeBranch) {
|
||||||
await updateTrackingComment(
|
await updateTrackingComment(
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async function run() {
|
|||||||
const client = createClient(githubToken);
|
const client = createClient(githubToken);
|
||||||
|
|
||||||
const serverUrl = GITEA_SERVER_URL;
|
const serverUrl = GITEA_SERVER_URL;
|
||||||
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_NUMBER}`;
|
||||||
|
|
||||||
let comment;
|
let comment;
|
||||||
let isPRReviewComment = false;
|
let isPRReviewComment = false;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
const context = github.context;
|
const context = github.context;
|
||||||
|
|
||||||
const commonFields = {
|
const commonFields = {
|
||||||
runId: process.env.GITHUB_RUN_ID!,
|
runId: process.env.GITHUB_RUN_NUMBER!,
|
||||||
eventName: context.eventName,
|
eventName: context.eventName,
|
||||||
eventAction: context.payload.action,
|
eventAction: context.payload.action,
|
||||||
repository: {
|
repository: {
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ export async function setupBranch(
|
|||||||
const { baseBranch } = context.inputs;
|
const { baseBranch } = context.inputs;
|
||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
|
// Determine base branch - use baseBranch if provided, otherwise fetch default
|
||||||
|
let sourceBranch: string;
|
||||||
|
|
||||||
|
if (baseBranch) {
|
||||||
|
// Use provided base branch for source
|
||||||
|
sourceBranch = baseBranch;
|
||||||
|
} else {
|
||||||
|
// No base branch provided, fetch the default branch to use as source
|
||||||
|
const repoResponse = await client.api.getRepo(owner, repo);
|
||||||
|
sourceBranch = repoResponse.data.default_branch;
|
||||||
|
}
|
||||||
|
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
const prData = githubData.contextData as GitHubPullRequest;
|
const prData = githubData.contextData as GitHubPullRequest;
|
||||||
const prState = prData.state;
|
const prState = prData.state;
|
||||||
@@ -36,9 +48,18 @@ export async function setupBranch(
|
|||||||
// Check if PR is closed or merged
|
// Check if PR is closed or merged
|
||||||
if (prState === "CLOSED" || prState === "MERGED") {
|
if (prState === "CLOSED" || prState === "MERGED") {
|
||||||
console.log(
|
console.log(
|
||||||
`PR #${entityNumber} is ${prState}, creating new branch from source...`,
|
`PR #${entityNumber} is ${prState}, will let Claude create a new branch when needed`,
|
||||||
);
|
);
|
||||||
// Fall through to create a new branch like we do for issues
|
|
||||||
|
// Check out the base branch and let Claude create branches as needed
|
||||||
|
await $`git fetch origin ${sourceBranch}`;
|
||||||
|
await $`git checkout ${sourceBranch}`;
|
||||||
|
await $`git pull origin ${sourceBranch}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseBranch: sourceBranch,
|
||||||
|
currentBranch: sourceBranch,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// Handle open PR: Checkout the PR branch
|
// Handle open PR: Checkout the PR branch
|
||||||
console.log("This is an open PR, checking out PR branch...");
|
console.log("This is an open PR, checking out PR branch...");
|
||||||
@@ -62,44 +83,16 @@ export async function setupBranch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine source branch - use baseBranch if provided, otherwise fetch default
|
// For issues, check out the base branch and let Claude create branches as needed
|
||||||
let sourceBranch: string;
|
|
||||||
|
|
||||||
if (baseBranch) {
|
|
||||||
// Use provided base branch for source
|
|
||||||
sourceBranch = baseBranch;
|
|
||||||
} else {
|
|
||||||
// No base branch provided, fetch the default branch to use as source
|
|
||||||
const repoResponse = await client.api.getRepo(owner, repo);
|
|
||||||
sourceBranch = repoResponse.data.default_branch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creating a new branch for either an issue or closed/merged PR
|
|
||||||
const entityType = isPR ? "pr" : "issue";
|
|
||||||
console.log(
|
console.log(
|
||||||
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
`Setting up base branch ${sourceBranch} for issue #${entityNumber}, Claude will create branch when needed...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const timestamp = new Date()
|
|
||||||
.toISOString()
|
|
||||||
.replace(/[:-]/g, "")
|
|
||||||
.replace(/\.\d{3}Z/, "")
|
|
||||||
.split("T")
|
|
||||||
.join("_");
|
|
||||||
|
|
||||||
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use local git operations instead of API since Gitea's API is unreliable
|
|
||||||
console.log(
|
|
||||||
`Setting up local git branch: ${newBranch} from: ${sourceBranch}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure we're in the repository directory
|
// Ensure we're in the repository directory
|
||||||
const repoDir = process.env.GITHUB_WORKSPACE || process.cwd();
|
const repoDir = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||||
console.log(`Working in directory: ${repoDir}`);
|
console.log(`Working in directory: ${repoDir}`);
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if we're in a git repository
|
// Check if we're in a git repository
|
||||||
console.log(`Checking if we're in a git repository...`);
|
console.log(`Checking if we're in a git repository...`);
|
||||||
await $`git status`;
|
await $`git status`;
|
||||||
@@ -116,43 +109,28 @@ export async function setupBranch(
|
|||||||
console.log(`Pulling latest changes for ${sourceBranch}...`);
|
console.log(`Pulling latest changes for ${sourceBranch}...`);
|
||||||
await $`git pull origin ${sourceBranch}`;
|
await $`git pull origin ${sourceBranch}`;
|
||||||
|
|
||||||
// Create and checkout the new branch
|
// Verify the branch was checked out
|
||||||
console.log(`Creating new branch: ${newBranch}`);
|
|
||||||
await $`git checkout -b ${newBranch}`;
|
|
||||||
|
|
||||||
// Verify the branch was created
|
|
||||||
const currentBranch = await $`git branch --show-current`;
|
const currentBranch = await $`git branch --show-current`;
|
||||||
const branchName = currentBranch.text().trim();
|
const branchName = currentBranch.text().trim();
|
||||||
console.log(`Current branch after creation: ${branchName}`);
|
console.log(`Current branch: ${branchName}`);
|
||||||
|
|
||||||
if (branchName === newBranch) {
|
if (branchName === sourceBranch) {
|
||||||
console.log(
|
console.log(`✅ Successfully checked out base branch: ${sourceBranch}`);
|
||||||
`✅ Successfully created and checked out branch: ${newBranch}`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Branch creation failed. Expected ${newBranch}, got ${branchName}`,
|
`Branch checkout failed. Expected ${sourceBranch}, got ${branchName}`,
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (gitError: any) {
|
|
||||||
console.error(`❌ Git operations failed:`, gitError);
|
|
||||||
console.error(`Error message: ${gitError.message || gitError}`);
|
|
||||||
|
|
||||||
// This is a critical failure - the branch MUST be created for Claude to work
|
|
||||||
throw new Error(
|
|
||||||
`Failed to create branch ${newBranch}: ${gitError.message || gitError}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Branch setup completed for: ${newBranch}`);
|
console.log(
|
||||||
|
`Branch setup completed, ready for Claude to create branches as needed`,
|
||||||
|
);
|
||||||
|
|
||||||
// Set outputs for GitHub Actions
|
// Set outputs for GitHub Actions
|
||||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
|
||||||
core.setOutput("BASE_BRANCH", sourceBranch);
|
core.setOutput("BASE_BRANCH", sourceBranch);
|
||||||
return {
|
return {
|
||||||
baseBranch: sourceBranch,
|
baseBranch: sourceBranch,
|
||||||
claudeBranch: newBranch,
|
currentBranch: sourceBranch,
|
||||||
currentBranch: newBranch,
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting up branch:", error);
|
console.error("Error setting up branch:", error);
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// GitHub File Operations MCP Server
|
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { readFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { GITEA_API_URL } from "../github/api/config";
|
|
||||||
|
|
||||||
type GitHubRef = {
|
|
||||||
object: {
|
|
||||||
sha: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type GitHubCommit = {
|
|
||||||
tree: {
|
|
||||||
sha: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type GitHubTree = {
|
|
||||||
sha: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type GitHubNewCommit = {
|
|
||||||
sha: string;
|
|
||||||
message: string;
|
|
||||||
author: {
|
|
||||||
name: string;
|
|
||||||
date: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get repository information from environment variables
|
|
||||||
const REPO_OWNER = process.env.REPO_OWNER;
|
|
||||||
const REPO_NAME = process.env.REPO_NAME;
|
|
||||||
const BRANCH_NAME = process.env.BRANCH_NAME;
|
|
||||||
const REPO_DIR = process.env.REPO_DIR || process.cwd();
|
|
||||||
|
|
||||||
if (!REPO_OWNER || !REPO_NAME || !BRANCH_NAME) {
|
|
||||||
console.error(
|
|
||||||
"Error: REPO_OWNER, REPO_NAME, and BRANCH_NAME environment variables are required",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new McpServer({
|
|
||||||
name: "GitHub File Operations Server",
|
|
||||||
version: "0.0.1",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Commit files tool
|
|
||||||
server.tool(
|
|
||||||
"commit_files",
|
|
||||||
"Commit one or more files to a repository in a single commit (this will commit them atomically in the remote repository)",
|
|
||||||
{
|
|
||||||
files: z
|
|
||||||
.array(z.string())
|
|
||||||
.describe(
|
|
||||||
'Array of file paths relative to repository root (e.g. ["src/main.js", "README.md"]). All files must exist locally.',
|
|
||||||
),
|
|
||||||
message: z.string().describe("Commit message"),
|
|
||||||
},
|
|
||||||
async ({ files, message }) => {
|
|
||||||
const owner = REPO_OWNER;
|
|
||||||
const repo = REPO_NAME;
|
|
||||||
const branch = BRANCH_NAME;
|
|
||||||
try {
|
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
|
||||||
if (!githubToken) {
|
|
||||||
throw new Error("GITHUB_TOKEN environment variable is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedFiles = files.map((filePath) => {
|
|
||||||
if (filePath.startsWith("/")) {
|
|
||||||
return filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: Gitea does not support GitHub's low-level git API operations
|
|
||||||
// (creating trees, commits, etc.). We need to use the contents API instead.
|
|
||||||
// For now, throw an error indicating this functionality is not available.
|
|
||||||
throw new Error(
|
|
||||||
"Multi-file commits are not supported with Gitea. " +
|
|
||||||
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
|
||||||
"that are required for atomic multi-file commits. " +
|
|
||||||
"Please commit files individually using the contents API.",
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(simplifiedResult, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Error: ${errorMessage}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
error: errorMessage,
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete files tool
|
|
||||||
server.tool(
|
|
||||||
"delete_files",
|
|
||||||
"Delete one or more files from a repository in a single commit",
|
|
||||||
{
|
|
||||||
paths: z
|
|
||||||
.array(z.string())
|
|
||||||
.describe(
|
|
||||||
'Array of file paths to delete relative to repository root (e.g. ["src/old-file.js", "docs/deprecated.md"])',
|
|
||||||
),
|
|
||||||
message: z.string().describe("Commit message"),
|
|
||||||
},
|
|
||||||
async ({ paths, message }) => {
|
|
||||||
const owner = REPO_OWNER;
|
|
||||||
const repo = REPO_NAME;
|
|
||||||
const branch = BRANCH_NAME;
|
|
||||||
try {
|
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
|
||||||
if (!githubToken) {
|
|
||||||
throw new Error("GITHUB_TOKEN environment variable is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert absolute paths to relative if they match CWD
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const processedPaths = paths.map((filePath) => {
|
|
||||||
if (filePath.startsWith("/")) {
|
|
||||||
if (filePath.startsWith(cwd)) {
|
|
||||||
// Strip CWD from absolute path
|
|
||||||
return filePath.slice(cwd.length + 1);
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Path '${filePath}' must be relative to repository root or within current working directory`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: Gitea does not support GitHub's low-level git API operations
|
|
||||||
// (creating trees, commits, etc.). We need to use the contents API instead.
|
|
||||||
// For now, throw an error indicating this functionality is not available.
|
|
||||||
throw new Error(
|
|
||||||
"Multi-file deletions are not supported with Gitea. " +
|
|
||||||
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
|
||||||
"that are required for atomic multi-file operations. " +
|
|
||||||
"Please delete files individually using the contents API.",
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(simplifiedResult, 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);
|
|
||||||
process.on("exit", () => {
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
runServer().catch(console.error);
|
|
||||||
@@ -127,6 +127,102 @@ server.tool(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Checkout branch tool
|
||||||
|
server.tool(
|
||||||
|
"checkout_branch",
|
||||||
|
"Checkout an existing branch using local git operations",
|
||||||
|
{
|
||||||
|
branch_name: z.string().describe("Name of the existing branch to checkout"),
|
||||||
|
create_if_missing: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Create branch if it doesn't exist locally (defaults to false)",
|
||||||
|
),
|
||||||
|
fetch_remote: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Fetch from remote if branch doesn't exist locally (defaults to true)",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
async ({ branch_name, create_if_missing = false, fetch_remote = true }) => {
|
||||||
|
try {
|
||||||
|
// Check if branch exists locally
|
||||||
|
let branchExists = false;
|
||||||
|
try {
|
||||||
|
runGitCommand(`git rev-parse --verify ${branch_name}`);
|
||||||
|
branchExists = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
`[LOCAL-GIT-MCP] Branch ${branch_name} doesn't exist locally`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If branch doesn't exist locally, try to fetch from remote
|
||||||
|
if (!branchExists && fetch_remote) {
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
`[LOCAL-GIT-MCP] Attempting to fetch ${branch_name} from remote`,
|
||||||
|
);
|
||||||
|
runGitCommand(`git fetch origin ${branch_name}:${branch_name}`);
|
||||||
|
branchExists = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
`[LOCAL-GIT-MCP] Branch ${branch_name} doesn't exist on remote`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If branch still doesn't exist and create_if_missing is true, create it
|
||||||
|
if (!branchExists && create_if_missing) {
|
||||||
|
console.log(`[LOCAL-GIT-MCP] Creating new branch ${branch_name}`);
|
||||||
|
runGitCommand(`git checkout -b ${branch_name}`);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully created and checked out new branch: ${branch_name}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If branch doesn't exist and we can't/won't create it, throw error
|
||||||
|
if (!branchExists) {
|
||||||
|
throw new Error(
|
||||||
|
`Branch '${branch_name}' does not exist locally or on remote. Use create_if_missing=true to create it.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkout the existing branch
|
||||||
|
runGitCommand(`git checkout ${branch_name}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully checked out branch: ${branch_name}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error checking out branch: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Commit files tool
|
// Commit files tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"commit_files",
|
"commit_files",
|
||||||
|
|||||||
Reference in New Issue
Block a user