16 Commits

Author SHA1 Message Date
Mark Wylde
87eac76ba0 feat: add optional claude name and email for git 2025-05-31 10:55:35 +01:00
Mark Wylde
96524bd1d8 chore: update readme with claude max info 2025-05-31 10:48:40 +01:00
Mark Wylde
0a1983379e Implement claude max auth 2025-05-31 10:14:51 +01:00
Mark Wylde
90c7a171fc Merge branch 'main' of github.com:anthropics/claude-code-action into gitea 2025-05-31 09:49:56 +01:00
Mark Wylde
07ce5612a4 chore: update readme version 2025-05-31 09:49:49 +01:00
Mark Wylde
d2b03c9183 Merge branch 'feat/give-claude-access-to-switch-branch' of github.com:markwylde/claude-code-gitea-action into gitea 2025-05-31 09:48:49 +01:00
Mark Wylde
05a2e7ea87 fix 2025-05-31 09:45:52 +01:00
Mark Wylde
4b26673a39 Merge pull request #1 from markwylde/feat/give-claude-access-to-switch-branch
Give claude access to switch branch
2025-05-31 09:38:17 +01:00
Mark Wylde
ccf7081358 improve git logic 2025-05-31 09:36:25 +01:00
Mark Wylde
5c040da573 Fix job id 2025-05-31 09:26:06 +01:00
Mark Wylde
e5b2574f8c Implement switch branch 2025-05-31 09:20:48 +01:00
Mark Wylde
799a5cd961 chore: update readme version 2025-05-31 09:08:46 +01:00
Mark Wylde
8406629c9f chore: add screenshot to readme 2025-05-31 09:07:46 +01:00
Mark Wylde
9714bd59a5 chore: simplify action config 2025-05-31 09:03:12 +01:00
Mark Wylde
fb6df649ed v1.0.1 2025-05-31 01:23:37 +01:00
Ashwin Bhat
a8a36ced96 fix mistake in FAQ (#100) 2025-05-30 12:33:15 -07:00
20 changed files with 429 additions and 381 deletions

View File

@@ -56,3 +56,19 @@ 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
## Feature Development Reminders
When implementing new features that add action inputs, configuration options, or capabilities:
1. Always update README.md to document new inputs in the inputs table
2. Update example workflows to show how new inputs can be used
3. Add appropriate defaults and descriptions to action.yml

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 (and other GitHub Apps/bots) cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow. The `github-actions` user cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user, or use a separate app token of your own. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow.
### Why does Claude say I don't have permission to trigger it? ### Why does Claude say I don't have permission to trigger it?
@@ -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 `github_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 `gitea_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:
github_token: ${{ secrets.GITHUB_TOKEN }} gitea_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:
github_token: ${{ secrets.GITHUB_TOKEN }} gitea_token: ${{ secrets.GITHUB_TOKEN }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
``` ```

View File

@@ -1,5 +1,7 @@
# 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.
@@ -58,22 +60,23 @@ 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.0 - uses: markwylde/claude-code-gitea-action@v1.0.3
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITEA_TOKEN }} gitea_token: ${{ secrets.GITEA_TOKEN }}
gitea_api_url: https://gitea.example.com claude_git_name: Claude # optional
claude_git_email: claude@anthropic.com # optional
``` ```
## 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). Set to 'use-oauth' when using claude_credentials | No\* | - |
| `claude_credentials` | Claude OAuth credentials JSON for Claude AI Max subscription authentication | 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` |
| `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_token` | Gitea 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` |
@@ -83,11 +86,39 @@ jobs:
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" | | `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | | `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | | `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `claude_git_name` | Git user.name for commits made by Claude | No | `Claude` |
| `claude_git_email` | Git user.email for commits made by Claude | No | `claude@anthropic.com` |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex) \*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration. > **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
## Claude Max Authentication
This action supports authentication using Claude Max OAuth credentials. This allows users with Claude Max subscriptions to use their existing authentication.
### Setup
1. **Get OAuth Credentials**: Use Claude Code to generate OAuth credentials:
```
/auth-setup
```
2. **Add Credentials to Repository**: Add the generated JSON credentials as a repository secret named `CLAUDE_CREDENTIALS`.
3. **Configure Workflow**: Set up your workflow to use OAuth authentication:
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: "use-oauth"
claude_credentials: ${{ secrets.CLAUDE_CREDENTIALS }}
gitea_token: ${{ secrets.GITHUB_TOKEN }}
```
When `anthropic_api_key` is set to `'use-oauth'`, the action will use the OAuth credentials provided in `claude_credentials` instead of a direct API key.
## Gitea Configuration ## Gitea Configuration
This action has been enhanced to work with Gitea installations. The main differences from GitHub are: This action has been enhanced to work with Gitea installations. The main differences from GitHub are:
@@ -112,8 +143,7 @@ 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_api_url: "https://gitea.example.com" gitea_token: ${{ secrets.GITEA_TOKEN }}
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)" description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex). Set to 'use-oauth' when using claude_credentials"
required: false required: false
github_token: claude_credentials:
description: "GitHub token with repo and pull request permissions (defaults to GITHUB_TOKEN)" description: "Claude OAuth credentials JSON for Claude AI Max subscription authentication"
required: false required: false
gitea_api_url: gitea_token:
description: "Gitea server URL (e.g., https://gitea.example.com, defaults to GitHub API)" description: "Gitea token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
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"
@@ -63,6 +63,14 @@ inputs:
description: "Timeout in minutes for execution" description: "Timeout in minutes for execution"
required: false required: false
default: "30" default: "30"
claude_git_name:
description: "Git user.name for commits made by Claude"
required: false
default: "Claude"
claude_git_email:
description: "Git user.email for commits made by Claude"
required: false
default: "claude@anthropic.com"
outputs: outputs:
execution_file: execution_file:
@@ -95,10 +103,12 @@ 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.github_token }} OVERRIDE_GITHUB_TOKEN: ${{ inputs.gitea_token }}
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
GITEA_API_URL: ${{ inputs.gitea_api_url }} GITEA_API_URL: ${{ env.GITHUB_SERVER_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
@@ -126,10 +136,15 @@ 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: ${{ inputs.gitea_api_url }} GITEA_API_URL: ${{ env.GITHUB_SERVER_URL }}
# Git configuration
CLAUDE_GIT_NAME: ${{ inputs.claude_git_name }}
CLAUDE_GIT_EMAIL: ${{ inputs.claude_git_email }}
# 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 }}
@@ -167,13 +182,17 @@ 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: ${{ inputs.gitea_api_url }} GITEA_API_URL: ${{ env.GITHUB_SERVER_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

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

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:
github_token: ${{ secrets.GITHUB_TOKEN }} gitea_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:
github_token: ${{ secrets.GITHUB_TOKEN }} # Use standard workflow token gitea_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"

63
src/claude/oauth-setup.ts Normal file
View File

@@ -0,0 +1,63 @@
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,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"];
@@ -217,8 +219,6 @@ 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,9 +251,6 @@ 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) {
@@ -267,8 +264,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 = {
@@ -277,7 +274,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}`);
@@ -512,7 +509,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 +547,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 +568,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 +607,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 +620,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 +641,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 +652,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)

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,17 +18,29 @@ 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 GitHub token // Step 1: Setup OAuth credentials if provided
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 2: Parse GitHub context (once for all operations) // Step 3: Parse GitHub context (once for all operations)
const context = parseGitHubContext(); const context = parseGitHubContext();
// Step 3: Check write permissions // Step 4: Check write permissions
const hasWritePermissions = await checkWritePermissions( const hasWritePermissions = await checkWritePermissions(
client.api, client.api,
context, context,
@@ -39,7 +51,7 @@ async function run() {
); );
} }
// Step 4: Check trigger conditions // Step 5: 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
@@ -51,14 +63,14 @@ async function run() {
return; return;
} }
// Step 5: Check if actor is human // Step 6: Check if actor is human
await checkHumanActor(client.api, context); await checkHumanActor(client.api, context);
// Step 6: Create initial tracking comment // Step 7: 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 7: Fetch GitHub data (once for both branch setup and prompt creation) // Step 8: 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}`,
@@ -66,14 +78,14 @@ async function run() {
isPR: context.isPR, isPR: context.isPR,
}); });
// Step 8: Setup branch // Step 9: 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 9: Update initial comment with branch link (only for issues that created a new branch) // Step 10: 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,
@@ -83,7 +95,7 @@ async function run() {
); );
} }
// Step 10: Create prompt file // Step 11: Create prompt file
await createPrompt( await createPrompt(
commentId, commentId,
branchInfo.baseBranch, branchInfo.baseBranch,
@@ -92,7 +104,7 @@ async function run() {
context, context,
); );
// Step 11: Get MCP configuration // Step 12: 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_ID}`; const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_NUMBER}`;
let comment; let comment;
let isPRReviewComment = false; let isPRReviewComment = false;

View File

@@ -1,14 +1,14 @@
export const GITEA_API_URL = // Derive API URL from server URL for Gitea instances
process.env.GITEA_API_URL || "https://api.github.com"; function deriveApiUrl(serverUrl: string): string {
if (serverUrl.includes("github.com")) {
// Derive server URL from API URL for Gitea instances return "https://api.github.com";
function deriveServerUrl(apiUrl: string): string {
if (apiUrl.includes("api.github.com")) {
return "https://github.com";
} }
// For Gitea, remove /api/v1 from the API URL to get the server URL // For Gitea, add /api/v1 to the server URL to get the API URL
return apiUrl.replace(/\/api\/v1\/?$/, ""); return `${serverUrl}/api/v1`;
} }
export const GITEA_SERVER_URL = export const GITEA_SERVER_URL =
process.env.GITEA_SERVER_URL || deriveServerUrl(GITEA_API_URL); process.env.GITHUB_SERVER_URL || "https://github.com";
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_ID!, runId: process.env.GITHUB_RUN_NUMBER!,
eventName: context.eventName, eventName: context.eventName,
eventAction: context.payload.action, eventAction: context.payload.action,
repository: { repository: {

View File

@@ -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);

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 github_token input or ensure GITHUB_TOKEN is available in the workflow environment.", "No GitHub token available. Please provide a gitea_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 \`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.`, `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.`,
); );
process.exit(1); process.exit(1);
} }

View File

@@ -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);

View File

@@ -60,15 +60,18 @@ function runGitCommand(command: string): string {
// Helper function to ensure git user is configured // Helper function to ensure git user is configured
function ensureGitUserConfigured(): void { function ensureGitUserConfigured(): void {
const gitName = process.env.CLAUDE_GIT_NAME || "Claude";
const gitEmail = process.env.CLAUDE_GIT_EMAIL || "claude@anthropic.com";
try { try {
// Check if user.email is already configured // Check if user.email is already configured
runGitCommand("git config user.email"); runGitCommand("git config user.email");
console.log(`[LOCAL-GIT-MCP] Git user.email already configured`); console.log(`[LOCAL-GIT-MCP] Git user.email already configured`);
} catch (error) { } catch (error) {
console.log( console.log(
`[LOCAL-GIT-MCP] Git user.email not configured, setting default`, `[LOCAL-GIT-MCP] Git user.email not configured, setting to: ${gitEmail}`,
); );
runGitCommand('git config user.email "claude@anthropic.com"'); runGitCommand(`git config user.email "${gitEmail}"`);
} }
try { try {
@@ -77,9 +80,9 @@ function ensureGitUserConfigured(): void {
console.log(`[LOCAL-GIT-MCP] Git user.name already configured`); console.log(`[LOCAL-GIT-MCP] Git user.name already configured`);
} catch (error) { } catch (error) {
console.log( console.log(
`[LOCAL-GIT-MCP] Git user.name not configured, setting default`, `[LOCAL-GIT-MCP] Git user.name not configured, setting to: ${gitName}`,
); );
runGitCommand('git config user.name "Claude"'); runGitCommand(`git config user.name "${gitName}"`);
} }
} }
@@ -127,6 +130,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",

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.github.com>", "Co-authored-by: johndoe <johndoe@users.noreply.local>",
); );
}); });
@@ -338,7 +338,7 @@ describe("generatePrompt", () => {
// Should contain PR-specific instructions // Should contain PR-specific instructions
expect(prompt).toContain( expect(prompt).toContain(
"Push directly using mcp__github_file_ops__commit_files to the existing branch", "Commit changes using mcp__local_git_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 anything in your branch, your comment must include the PR URL", "If you created a branch and made changes, 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(
"Push directly using mcp__github_file_ops__commit_files to the existing branch", "Commit changes using mcp__local_git_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,13 +449,10 @@ 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(
"Push directly using mcp__github_file_ops__commit_files to the existing branch", "Commit changes using mcp__local_git_ops__commit_files to the existing branch",
); );
}); });
@@ -478,7 +475,7 @@ describe("generatePrompt", () => {
// Should contain open PR instructions // Should contain open PR instructions
expect(prompt).toContain( expect(prompt).toContain(
"Push directly using mcp__github_file_ops__commit_files to the existing branch", "Commit changes using mcp__local_git_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",
@@ -543,9 +540,6 @@ 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", () => {
@@ -638,8 +632,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__github_file_ops__commit_files"); expect(result).toContain("mcp__local_git_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files"); expect(result).toContain("mcp__local_git_ops__delete_files");
}); });
test("should return PR comment tool for inline review comments", () => { test("should return PR comment tool for inline review comments", () => {
@@ -662,8 +656,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__github_file_ops__commit_files"); expect(result).toContain("mcp__local_git_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files"); expect(result).toContain("mcp__local_git_ops__delete_files");
}); });
test("should append custom tools when provided", () => { test("should append custom tools when provided", () => {