41 Commits

Author SHA1 Message Date
Mark Wylde
01c923b4bd Attempt to make this work 2025-05-31 01:13:13 +01:00
Mark Wylde
dea3e23b34 Attempt to make this work 2025-05-31 01:11:46 +01:00
Mark Wylde
45ee2dca55 Attempt to make this work 2025-05-31 01:10:59 +01:00
Mark Wylde
e2f737a753 Attempt to make this work 2025-05-31 01:05:34 +01:00
Mark Wylde
1c309c8d10 Attempt to make this work 2025-05-31 00:59:31 +01:00
Mark Wylde
5171232878 Attempt to make this work 2025-05-31 00:56:07 +01:00
Mark Wylde
ea134ca929 Attempt to make this work 2025-05-31 00:50:13 +01:00
Mark Wylde
72aa15ac4f Attempt to make this work 2025-05-31 00:44:27 +01:00
Mark Wylde
87a39e8fbc Attempt to make this work 2025-05-31 00:35:22 +01:00
Mark Wylde
44d513b712 Attempt to make this work 2025-05-31 00:16:57 +01:00
Mark Wylde
3afac506b2 Attempt to make this work 2025-05-31 00:13:14 +01:00
Mark Wylde
436046a0ff Attempt to make this work 2025-05-31 00:09:10 +01:00
Mark Wylde
12940797c7 Attempt to make this work 2025-05-31 00:05:18 +01:00
Mark Wylde
59a6b568a6 Attempt to make this work 2025-05-31 00:02:45 +01:00
Mark Wylde
ed04634119 Attempt to make this work 2025-05-30 23:59:23 +01:00
Mark Wylde
8de76049e1 Attempt to make this work 2025-05-30 23:55:03 +01:00
Mark Wylde
bbf8371776 Attempt to make this work 2025-05-30 22:52:37 +01:00
Mark Wylde
e1be245c51 Attempt to make this work 2025-05-30 22:46:03 +01:00
Mark Wylde
0bb118b1a2 Attempt to make this work 2025-05-30 22:44:19 +01:00
Mark Wylde
4b69e8485a Attempt to make this work 2025-05-30 22:41:48 +01:00
Mark Wylde
4b1c3d000d Attempt to make this work 2025-05-30 22:31:06 +01:00
Mark Wylde
b41b7ecd9f Attempt to make this work 2025-05-30 22:18:35 +01:00
Mark Wylde
11685fc8c1 Attempt to make this work 2025-05-30 22:11:20 +01:00
Mark Wylde
87c1a97c6e Attempt to make this work 2025-05-30 22:02:19 +01:00
Mark Wylde
7018095f9a Attempt to make this work 2025-05-30 21:54:22 +01:00
Mark Wylde
e079f18247 Attempt to make this work 2025-05-30 21:49:42 +01:00
Mark Wylde
c0d1a3fc4c Attempt to make this work 2025-05-30 21:47:12 +01:00
Mark Wylde
c77bb0e4b3 Attempt to make this work 2025-05-30 21:20:59 +01:00
Mark Wylde
01602be052 Attempt to make this work 2025-05-30 21:12:47 +01:00
Mark Wylde
f2f966c77e Attempt to make this work 2025-05-30 21:00:03 +01:00
Mark Wylde
80886e1c8e Attempt to make this work 2025-05-30 20:49:55 +01:00
Mark Wylde
e2d102aadd Attempt to make this work 2025-05-30 20:40:22 +01:00
Mark Wylde
c004bcdb83 Attempt to make this work 2025-05-30 20:37:47 +01:00
Mark Wylde
2f36d061b3 Attempt to make this work 2025-05-30 20:34:43 +01:00
Mark Wylde
9d64c62a2e Attempt to make this work 2025-05-30 20:32:40 +01:00
Mark Wylde
828076e411 Attempt to make this work 2025-05-30 20:29:15 +01:00
Mark Wylde
9986f4d1a3 Attempt to make this work 2025-05-30 20:25:16 +01:00
Mark Wylde
406208cf7a Attempt to make this work 2025-05-30 20:20:51 +01:00
Mark Wylde
6410e33591 Attempt to make this work 2025-05-30 20:17:34 +01:00
Mark Wylde
f598608bb4 Attempt to make this work 2025-05-30 20:10:31 +01:00
Mark Wylde
e474962b0d First attempt 2025-05-30 20:02:39 +01:00
20 changed files with 377 additions and 366 deletions

View File

@@ -56,12 +56,3 @@ 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

4
FAQ.md
View File

@@ -6,7 +6,7 @@ This FAQ addresses common questions and gotchas when using the Claude Code GitHu
### Why doesn't tagging @claude from my automated workflow work? ### 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?
@@ -22,7 +22,7 @@ permissions:
id-token: write # Required for OIDC authentication id-token: write # Required for OIDC authentication
``` ```
The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `gitea_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more. The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `github_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more.
## Claude's Capabilities and Limitations ## Claude's Capabilities and Limitations

View File

@@ -56,7 +56,7 @@ Now required to explicitly provide a GitHub token:
# After (required) # After (required)
- uses: anthropics/claude-code-action@beta - uses: anthropics/claude-code-action@beta
with: with:
gitea_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
``` ```
@@ -94,7 +94,7 @@ jobs:
- name: Run Claude Assistant - name: Run Claude Assistant
uses: ./ # Adjust path as needed for your Gitea setup uses: ./ # Adjust path as needed for your Gitea setup
with: with:
gitea_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
``` ```

View File

@@ -1,7 +1,5 @@
# Claude Code Action (Gitea Fork) # Claude Code Action (Gitea Fork)
![Claude Code Action in action](assets/screenshot.png)
A fork of the [Claude Code Action](https://github.com/anthropics/claude-code-action) that adds support for Gitea alongside GitHub. This action provides a general-purpose [Claude Code](https://claude.ai/code) assistant for PRs and issues that can answer questions and implement code changes. It listens for a trigger phrase in comments and activates Claude to act on the request. Supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, and Google Vertex AI. A fork of the [Claude Code Action](https://github.com/anthropics/claude-code-action) that adds support for Gitea alongside GitHub. This action provides a general-purpose [Claude Code](https://claude.ai/code) assistant for PRs and issues that can answer questions and implement code changes. It listens for a trigger phrase in comments and activates Claude to act on the request. Supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, and Google Vertex AI.
> **Note**: This is an unofficial fork that extends the original action to work with Gitea installations. The core functionality remains the same, with additional support for Gitea APIs and local git operations. > **Note**: This is an unofficial fork that extends the original action to work with Gitea installations. The core functionality remains the same, with additional support for Gitea APIs and local git operations.
@@ -60,20 +58,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: markwylde/claude-code-gitea-action@v1.0.3 - uses: markwylde/claude-code-gitea-action@v1.0.0
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
gitea_token: ${{ secrets.GITEA_TOKEN }} github_token: ${{ secrets.GITEA_TOKEN }}
gitea_api_url: https://gitea.example.com
``` ```
## 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` |
| `gitea_token` | Gitea token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | | `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `gitea_api_url` | Gitea server URL (e.g., `https://gitea.example.com`) for Gitea installations. Leave empty for GitHub. | No | GitHub API |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | | `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | | `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | | `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
@@ -112,7 +112,8 @@ jobs:
- uses: anthropics/claude-code-action@beta - uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
gitea_token: ${{ secrets.GITEA_TOKEN }} gitea_api_url: "https://gitea.example.com"
github_token: ${{ secrets.GITEA_TOKEN }}
``` ```
### Gitea Setup Notes ### Gitea Setup Notes

View File

@@ -42,13 +42,13 @@ inputs:
# Auth configuration # Auth configuration
anthropic_api_key: anthropic_api_key:
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex). Set to 'use-oauth' when using claude_credentials" description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)"
required: false required: false
claude_credentials: github_token:
description: "Claude OAuth credentials JSON for Claude AI Max subscription authentication" description: "GitHub token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
required: false required: false
gitea_token: gitea_api_url:
description: "Gitea token with repo and pull request permissions (defaults to GITHUB_TOKEN)" description: "Gitea server URL (e.g., https://gitea.example.com, defaults to GitHub API)"
required: false required: false
use_bedrock: use_bedrock:
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API" description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
@@ -95,12 +95,10 @@ runs:
ALLOWED_TOOLS: ${{ inputs.allowed_tools }} ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }} CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
DIRECT_PROMPT: ${{ inputs.direct_prompt }} DIRECT_PROMPT: ${{ inputs.direct_prompt }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.gitea_token }} OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
GITEA_API_URL: ${{ env.GITHUB_SERVER_URL }} GITEA_API_URL: ${{ inputs.gitea_api_url }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }}
- name: Run Claude Code - name: Run Claude Code
id: claude-code id: claude-code
@@ -128,11 +126,10 @@ runs:
USE_BEDROCK: ${{ inputs.use_bedrock }} USE_BEDROCK: ${{ inputs.use_bedrock }}
USE_VERTEX: ${{ inputs.use_vertex }} USE_VERTEX: ${{ inputs.use_vertex }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }}
# GitHub token for repository access # GitHub token for repository access
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
GITEA_API_URL: ${{ env.GITHUB_SERVER_URL }} GITEA_API_URL: ${{ inputs.gitea_api_url }}
# Provider configuration (for future cloud provider support) # Provider configuration (for future cloud provider support)
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }} ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
@@ -170,17 +167,13 @@ runs:
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }} TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }} PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }} PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
GITEA_API_URL: ${{ env.GITHUB_SERVER_URL }} GITEA_API_URL: ${{ inputs.gitea_api_url }}
- name: Display Claude Code Report - name: Display Claude Code Report
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != '' if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
shell: bash shell: bash
run: | run: |
if [ -f "${{ steps.claude-code.outputs.execution_file }}" ]; then
echo "## Claude Code Report" >> $GITHUB_STEP_SUMMARY echo "## Claude Code Report" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY echo '```json' >> $GITHUB_STEP_SUMMARY
cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
else
echo "⚠️ Claude Code execution completed but no report file was generated" >> $GITHUB_STEP_SUMMARY
fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -31,6 +31,6 @@ jobs:
- name: Run Claude PR Action - name: Run Claude PR Action
uses: anthropics/claude-code-action@beta uses: anthropics/claude-code-action@beta
with: with:
gitea_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60" timeout_minutes: "60"

View File

@@ -31,7 +31,7 @@ jobs:
- name: Run Claude Assistant - name: Run Claude Assistant
uses: ./ # Use local action (adjust path as needed) uses: ./ # Use local action (adjust path as needed)
with: with:
gitea_token: ${{ secrets.GITHUB_TOKEN }} # Use standard workflow token github_token: ${{ secrets.GITHUB_TOKEN }} # Use standard workflow token
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60" timeout_minutes: "60"
trigger_phrase: "@claude" trigger_phrase: "@claude"

View File

@@ -1,59 +0,0 @@
import { mkdir, writeFile } from "fs/promises";
import { join } from "path";
import { homedir } from "os";
interface OAuthCredentials {
accessToken: string;
refreshToken: string;
expiresAt: string;
}
interface ClaudeCredentialsInput {
claudeAiOauth: {
accessToken: string;
refreshToken: string;
expiresAt: number;
scopes: string[];
};
}
export async function setupOAuthCredentials(credentialsJson: string) {
try {
// Parse the credentials JSON
const parsedCredentials: ClaudeCredentialsInput = JSON.parse(credentialsJson);
if (!parsedCredentials.claudeAiOauth) {
throw new Error("Invalid credentials format: missing claudeAiOauth");
}
const { accessToken, refreshToken, expiresAt } = parsedCredentials.claudeAiOauth;
if (!accessToken || !refreshToken || !expiresAt) {
throw new Error("Invalid credentials format: missing required OAuth fields");
}
const claudeDir = join(homedir(), ".claude");
const credentialsPath = join(claudeDir, ".credentials.json");
// Create the .claude directory if it doesn't exist
await mkdir(claudeDir, { recursive: true });
// Create the credentials JSON structure
const credentialsData = {
claudeAiOauth: {
accessToken,
refreshToken,
expiresAt,
scopes: ["user:inference", "user:profile"],
},
};
// Write the credentials file
await writeFile(credentialsPath, JSON.stringify(credentialsData, null, 2));
console.log(`OAuth credentials written to ${credentialsPath}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to setup OAuth credentials: ${errorMessage}`);
}
}

View File

@@ -33,8 +33,6 @@ 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"];
@@ -219,6 +217,8 @@ export function prepareContext(
...(baseBranch && { baseBranch }), ...(baseBranch && { baseBranch }),
}; };
break; break;
} else if (!claudeBranch) {
throw new Error("CLAUDE_BRANCH is required for issue_comment event");
} else if (!baseBranch) { } else if (!baseBranch) {
throw new Error("BASE_BRANCH is required for issue_comment event"); throw new Error("BASE_BRANCH is required for issue_comment event");
} else if (!issueNumber) { } else if (!issueNumber) {
@@ -231,10 +231,10 @@ export function prepareContext(
eventName: "issue_comment", eventName: "issue_comment",
commentId, commentId,
isPR: false, isPR: false,
claudeBranch: claudeBranch,
baseBranch, baseBranch,
issueNumber, issueNumber,
commentBody, commentBody,
...(claudeBranch && { claudeBranch }),
}; };
break; break;
@@ -251,6 +251,9 @@ export function prepareContext(
if (!baseBranch) { if (!baseBranch) {
throw new Error("BASE_BRANCH is required for issues event"); throw new Error("BASE_BRANCH is required for issues event");
} }
if (!claudeBranch) {
throw new Error("CLAUDE_BRANCH is required for issues event");
}
if (eventAction === "assigned") { if (eventAction === "assigned") {
if (!assigneeTrigger) { if (!assigneeTrigger) {
@@ -264,8 +267,8 @@ export function prepareContext(
isPR: false, isPR: false,
issueNumber, issueNumber,
baseBranch, baseBranch,
claudeBranch,
assigneeTrigger, assigneeTrigger,
...(claudeBranch && { claudeBranch }),
}; };
} else if (eventAction === "opened") { } else if (eventAction === "opened") {
eventData = { eventData = {
@@ -274,7 +277,7 @@ export function prepareContext(
isPR: false, isPR: false,
issueNumber, issueNumber,
baseBranch, baseBranch,
...(claudeBranch && { claudeBranch }), claudeBranch,
}; };
} else { } else {
throw new Error(`Unsupported issue action: ${eventAction}`); throw new Error(`Unsupported issue action: ${eventAction}`);
@@ -509,20 +512,7 @@ ${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:
@@ -547,14 +537,15 @@ ${
- 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)
@@ -568,34 +559,8 @@ ${
- 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:
@@ -607,12 +572,12 @@ ${
- 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.
${!eventData.isPR || !eventData.claudeBranch ? `6. Final Update:` : `5. 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.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.` : ""} ${eventData.claudeBranch ? `- If you created anything in your branch, 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.
@@ -620,7 +585,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.` : 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.`} ${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.`}
- 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"}
@@ -641,10 +606,9 @@ 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: Create a new branch using mcp__local_git_ops__create_branch - When triggered on an issue: Always create a new branch
- When triggered on an open PR: Push directly to the existing PR branch - When triggered on an open PR: Always push directly to the existing PR branch
- When triggered on a closed PR: Create a new branch using mcp__local_git_ops__create_branch - When triggered on a closed PR: Create a new 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
@@ -652,7 +616,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 advanced branch operations (cannot merge branches, rebase, or perform other complex git operations beyond creating, checking out, and pushing branches) - Perform branch operations (cannot merge branches, rebase, or perform other git operations beyond pushing commits)
- 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)

View File

@@ -34,7 +34,7 @@ type IssueCommentEvent = {
issueNumber: string; issueNumber: string;
isPR: false; isPR: false;
baseBranch: string; baseBranch: string;
claudeBranch?: string; claudeBranch: string;
commentBody: string; commentBody: string;
}; };
@@ -55,7 +55,7 @@ type IssueOpenedEvent = {
isPR: false; isPR: false;
issueNumber: string; issueNumber: string;
baseBranch: string; baseBranch: string;
claudeBranch?: string; claudeBranch: string;
}; };
type IssueAssignedEvent = { type IssueAssignedEvent = {
@@ -64,7 +64,7 @@ type IssueAssignedEvent = {
isPR: false; isPR: false;
issueNumber: string; issueNumber: string;
baseBranch: string; baseBranch: string;
claudeBranch?: string; claudeBranch: string;
assigneeTrigger: string; assigneeTrigger: string;
}; };

View File

@@ -18,27 +18,17 @@ import { createPrompt } from "../create-prompt";
import { createClient } from "../github/api/client"; import { createClient } from "../github/api/client";
import { fetchGitHubData } from "../github/data/fetcher"; import { fetchGitHubData } from "../github/data/fetcher";
import { parseGitHubContext } from "../github/context"; import { parseGitHubContext } from "../github/context";
import { setupOAuthCredentials } from "../claude/oauth-setup";
async function run() { async function run() {
try { try {
// Step 1: Setup OAuth credentials if provided // Step 1: Setup GitHub token
const claudeCredentials = process.env.CLAUDE_CREDENTIALS;
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
if (claudeCredentials && anthropicApiKey === "use-oauth") {
await setupOAuthCredentials(claudeCredentials);
console.log("OAuth credentials configured for Claude AI Max subscription");
}
// Step 2: Setup GitHub token
const githubToken = await setupGitHubToken(); const githubToken = await setupGitHubToken();
const client = createClient(githubToken); const client = createClient(githubToken);
// Step 3: Parse GitHub context (once for all operations) // Step 2: Parse GitHub context (once for all operations)
const context = parseGitHubContext(); const context = parseGitHubContext();
// Step 4: Check write permissions // Step 3: Check write permissions
const hasWritePermissions = await checkWritePermissions( const hasWritePermissions = await checkWritePermissions(
client.api, client.api,
context, context,
@@ -49,7 +39,7 @@ async function run() {
); );
} }
// Step 5: Check trigger conditions // Step 4: Check trigger conditions
const containsTrigger = await checkTriggerAction(context); const containsTrigger = await checkTriggerAction(context);
// Set outputs that are always needed // Set outputs that are always needed
@@ -61,14 +51,14 @@ async function run() {
return; return;
} }
// Step 6: Check if actor is human // Step 5: Check if actor is human
await checkHumanActor(client.api, context); await checkHumanActor(client.api, context);
// Step 7: Create initial tracking comment // Step 6: Create initial tracking comment
const commentId = await createInitialComment(client.api, context); const commentId = await createInitialComment(client.api, context);
core.setOutput("claude_comment_id", commentId.toString()); core.setOutput("claude_comment_id", commentId.toString());
// Step 8: Fetch GitHub data (once for both branch setup and prompt creation) // Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
const githubData = await fetchGitHubData({ const githubData = await fetchGitHubData({
client: client, client: client,
repository: `${context.repository.owner}/${context.repository.repo}`, repository: `${context.repository.owner}/${context.repository.repo}`,
@@ -76,14 +66,14 @@ async function run() {
isPR: context.isPR, isPR: context.isPR,
}); });
// Step 9: Setup branch // Step 8: Setup branch
const branchInfo = await setupBranch(client, githubData, context); const branchInfo = await setupBranch(client, githubData, context);
core.setOutput("BASE_BRANCH", branchInfo.baseBranch); core.setOutput("BASE_BRANCH", branchInfo.baseBranch);
if (branchInfo.claudeBranch) { if (branchInfo.claudeBranch) {
core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch); core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch);
} }
// Step 10: Update initial comment with branch link (only if a claude branch was created) // Step 9: Update initial comment with branch link (only for issues that created a new branch)
if (branchInfo.claudeBranch) { if (branchInfo.claudeBranch) {
await updateTrackingComment( await updateTrackingComment(
client, client,
@@ -93,7 +83,7 @@ async function run() {
); );
} }
// Step 11: Create prompt file // Step 10: Create prompt file
await createPrompt( await createPrompt(
commentId, commentId,
branchInfo.baseBranch, branchInfo.baseBranch,
@@ -102,7 +92,7 @@ async function run() {
context, context,
); );
// Step 12: Get MCP configuration // Step 11: Get MCP configuration
const mcpConfig = await prepareMcpConfig( const mcpConfig = await prepareMcpConfig(
githubToken, githubToken,
context.repository.owner, context.repository.owner,

View File

@@ -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_NUMBER}`; const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
let comment; let comment;
let isPRReviewComment = false; let isPRReviewComment = false;

View File

@@ -1,14 +1,14 @@
// Derive API URL from server URL for Gitea instances export const GITEA_API_URL =
function deriveApiUrl(serverUrl: string): string { process.env.GITEA_API_URL || "https://api.github.com";
if (serverUrl.includes("github.com")) {
return "https://api.github.com"; // Derive server URL from API URL for Gitea instances
function deriveServerUrl(apiUrl: string): string {
if (apiUrl.includes("api.github.com")) {
return "https://github.com";
} }
// For Gitea, add /api/v1 to the server URL to get the API URL // For Gitea, remove /api/v1 from the API URL to get the server URL
return `${serverUrl}/api/v1`; return apiUrl.replace(/\/api\/v1\/?$/, "");
} }
export const GITEA_SERVER_URL = export const GITEA_SERVER_URL =
process.env.GITHUB_SERVER_URL || "https://github.com"; process.env.GITEA_SERVER_URL || deriveServerUrl(GITEA_API_URL);
export const GITEA_API_URL =
process.env.GITEA_API_URL || deriveApiUrl(GITEA_SERVER_URL);

View File

@@ -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_NUMBER!, runId: process.env.GITHUB_RUN_ID!,
eventName: context.eventName, eventName: context.eventName,
eventAction: context.payload.action, eventAction: context.payload.action,
repository: { repository: {

View File

@@ -29,18 +29,6 @@ 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;
@@ -48,18 +36,9 @@ 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}, will let Claude create a new branch when needed`, `PR #${entityNumber} is ${prState}, creating new branch from source...`,
); );
// 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...");
@@ -83,16 +62,44 @@ export async function setupBranch(
} }
} }
// For issues, check out the base branch and let Claude create branches as needed // Determine source 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;
}
// Creating a new branch for either an issue or closed/merged PR
const entityType = isPR ? "pr" : "issue";
console.log( console.log(
`Setting up base branch ${sourceBranch} for issue #${entityNumber}, Claude will create branch when needed...`, `Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
); );
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`;
@@ -109,28 +116,43 @@ 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}`;
// Verify the branch was checked out // Create and checkout the new branch
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: ${branchName}`); console.log(`Current branch after creation: ${branchName}`);
if (branchName === sourceBranch) { if (branchName === newBranch) {
console.log(`✅ Successfully checked out base branch: ${sourceBranch}`); console.log(
`✅ Successfully created and checked out branch: ${newBranch}`,
);
} else { } else {
throw new Error( throw new Error(
`Branch checkout failed. Expected ${sourceBranch}, got ${branchName}`, `Branch creation failed. Expected ${newBranch}, 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( console.log(`Branch setup completed for: ${newBranch}`);
`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,
currentBranch: sourceBranch, claudeBranch: newBranch,
currentBranch: newBranch,
}; };
} catch (error) { } catch (error) {
console.error("Error setting up branch:", error); console.error("Error setting up branch:", error);

View File

@@ -23,11 +23,11 @@ export async function setupGitHubToken(): Promise<string> {
} }
throw new Error( throw new Error(
"No GitHub token available. Please provide a gitea_token input or ensure GITHUB_TOKEN is available in the workflow environment.", "No GitHub token available. Please provide a github_token input or ensure GITHUB_TOKEN is available in the workflow environment.",
); );
} catch (error) { } catch (error) {
core.setFailed( core.setFailed(
`Failed to setup GitHub token: ${error}.\n\nPlease provide a \`gitea_token\` in the \`with\` section of the action in your workflow yml file, or ensure the workflow has access to the default GITHUB_TOKEN.`, `Failed to setup GitHub token: ${error}.\n\nPlease provide a \`github_token\` in the \`with\` section of the action in your workflow yml file, or ensure the workflow has access to the default GITHUB_TOKEN.`,
); );
process.exit(1); process.exit(1);
} }

View File

@@ -0,0 +1,199 @@
#!/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);

View File

@@ -127,102 +127,6 @@ 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",

View File

@@ -317,7 +317,7 @@ describe("generatePrompt", () => {
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>"); expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
expect(prompt).toContain( expect(prompt).toContain(
"Co-authored-by: johndoe <johndoe@users.noreply.local>", "Co-authored-by: johndoe <johndoe@users.noreply.github.com>",
); );
}); });
@@ -338,7 +338,7 @@ describe("generatePrompt", () => {
// Should contain PR-specific instructions // Should contain PR-specific instructions
expect(prompt).toContain( expect(prompt).toContain(
"Commit changes using mcp__local_git_ops__commit_files to the existing branch", "Push directly using mcp__github_file_ops__commit_files to the existing branch",
); );
expect(prompt).toContain( expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR", "Always push to the existing branch when triggered on a PR",
@@ -378,12 +378,12 @@ describe("generatePrompt", () => {
); );
expect(prompt).toContain("Create a PR](https://github.com/"); expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain( expect(prompt).toContain(
"If you created a branch and made changes, your comment must include the PR URL", "If you created anything in your branch, your comment must include the PR URL",
); );
// Should NOT contain PR-specific instructions // Should NOT contain PR-specific instructions
expect(prompt).not.toContain( expect(prompt).not.toContain(
"Commit changes using mcp__local_git_ops__commit_files to the existing branch", "Push directly using mcp__github_file_ops__commit_files to the existing branch",
); );
expect(prompt).not.toContain( expect(prompt).not.toContain(
"Always push to the existing branch when triggered on a PR", "Always push to the existing branch when triggered on a PR",
@@ -449,10 +449,13 @@ describe("generatePrompt", () => {
"The branch-name is the current branch: claude/pr-456-20240101_120000", "The branch-name is the current branch: claude/pr-456-20240101_120000",
); );
expect(prompt).toContain("Reference to the original PR"); expect(prompt).toContain("Reference to the original PR");
expect(prompt).toContain(
"If you created anything in your branch, your comment must include the PR URL",
);
// Should NOT contain open PR instructions // Should NOT contain open PR instructions
expect(prompt).not.toContain( expect(prompt).not.toContain(
"Commit changes using mcp__local_git_ops__commit_files to the existing branch", "Push directly using mcp__github_file_ops__commit_files to the existing branch",
); );
}); });
@@ -475,7 +478,7 @@ describe("generatePrompt", () => {
// Should contain open PR instructions // Should contain open PR instructions
expect(prompt).toContain( expect(prompt).toContain(
"Commit changes using mcp__local_git_ops__commit_files to the existing branch", "Push directly using mcp__github_file_ops__commit_files to the existing branch",
); );
expect(prompt).toContain( expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR", "Always push to the existing branch when triggered on a PR",
@@ -540,6 +543,9 @@ describe("generatePrompt", () => {
); );
expect(prompt).toContain("Create a PR](https://github.com/"); expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain("Reference to the original PR"); expect(prompt).toContain("Reference to the original PR");
expect(prompt).toContain(
"If you created anything in your branch, your comment must include the PR URL",
);
}); });
test("should handle pull_request event on closed PR with new branch", () => { test("should handle pull_request event on closed PR with new branch", () => {
@@ -632,8 +638,8 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("Write"); expect(result).toContain("Write");
expect(result).toContain("mcp__github__update_issue_comment"); expect(result).toContain("mcp__github__update_issue_comment");
expect(result).not.toContain("mcp__github__update_pull_request_comment"); expect(result).not.toContain("mcp__github__update_pull_request_comment");
expect(result).toContain("mcp__local_git_ops__commit_files"); expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__local_git_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", () => {
@@ -656,8 +662,8 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("Write"); expect(result).toContain("Write");
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).toContain("mcp__github__update_pull_request_comment");
expect(result).toContain("mcp__local_git_ops__commit_files"); expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__local_git_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", () => {