32 Commits

Author SHA1 Message Date
woehrer12
1fd3bbc91b chore: claude-code to v0.0.24 (#3) 2025-07-06 22:48:46 +01:00
Mark Wylde
a8399fe052 Merge pull request #2 from zinglax/shallow-fetch
Adding --depth=1 to fetchs to save time for large repos
2025-06-06 08:54:50 +01:00
zinglax
f640f38102 Adding --depth=1 to fetchs to save time
See https://github.com/anthropics/claude-code-action/issues/52
2025-06-04 09:44:05 -04:00
Mark Wylde
54147e92b6 fix: follow commit convention 2025-06-01 12:21:05 +01:00
Mark Wylde
42d3e56b56 fix: encourage use of mcp__gitea__list_branches 2025-05-31 13:54:12 +01:00
Mark Wylde
2c9043d65f fix: prompt 2025-05-31 13:46:05 +01:00
Mark Wylde
957f0ddc6f fix: encourage tool use over link 2025-05-31 13:35:56 +01:00
Mark Wylde
4005d690a8 fix: syntax 2025-05-31 13:30:39 +01:00
Mark Wylde
319e236b21 chore: remove stuff from prompt 2025-05-31 13:23:04 +01:00
Mark Wylde
b4448a4e51 fix: explain how to create pull request 2025-05-31 13:17:39 +01:00
Mark Wylde
163b16a5a5 chore: remove generated by 2025-05-31 13:07:49 +01:00
Mark Wylde
fd513046fa chore: remove generated by 2025-05-31 13:07:20 +01:00
Mark Wylde
aaeb014ca6 chore: refactor prompt for gitea 2025-05-31 13:00:23 +01:00
Mark Wylde
56b03c7993 feat: add more gitea mcp tools 2025-05-31 12:47:44 +01:00
Mark Wylde
46a306ccf2 chore: reduce readme 2025-05-31 11:33:49 +01:00
Mark Wylde
c6c6a613c8 chore: update screenshot for readme 2025-05-31 11:16:32 +01:00
Mark Wylde
2d1c93ebd2 chore: update screenshot for readme 2025-05-31 11:14:53 +01:00
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
Ashwin Bhat
a8a36ced96 fix mistake in FAQ (#100) 2025-05-30 12:33:15 -07:00
26 changed files with 1010 additions and 1089 deletions

View File

@@ -56,3 +56,20 @@ src/
- The action creates branches for issues and pushes to PR branches directly
- All actions create OIDC tokens for secure authentication
- 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

View File

@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
claude-code-action-coc@anthropic.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,136 +0,0 @@
# Contributing to Claude Code Action
Thank you for your interest in contributing to Claude Code Action! This document provides guidelines and instructions for contributing to the project.
## Getting Started
### Prerequisites
- [Bun](https://bun.sh/) runtime
- [Docker](https://www.docker.com/) (for running GitHub Actions locally)
- [act](https://github.com/nektos/act) (installed automatically by our test script)
- An Anthropic API key (for testing)
### Setup
1. Fork the repository on GitHub and clone your fork:
```bash
git clone https://github.com/your-username/claude-code-action.git
cd claude-code-action
```
2. Install dependencies:
```bash
bun install
```
3. Set up your Anthropic API key:
```bash
export ANTHROPIC_API_KEY="your-api-key-here"
```
## Development
### Available Scripts
- `bun test` - Run all tests
- `bun run typecheck` - Type check the code
- `bun run format` - Format code with Prettier
- `bun run format:check` - Check code formatting
## Testing
### Running Tests Locally
1. **Unit Tests**:
```bash
bun test
```
2. **Integration Tests** (using GitHub Actions locally):
```bash
./test-local.sh
```
This script:
- Installs `act` if not present (requires Homebrew on macOS)
- Runs the GitHub Action workflow locally using Docker
- Requires your `ANTHROPIC_API_KEY` to be set
On Apple Silicon Macs, the script automatically adds the `--container-architecture linux/amd64` flag to avoid compatibility issues.
## Pull Request Process
1. Create a new branch from `main`:
```bash
git checkout -b feature/your-feature-name
```
2. Make your changes and commit them:
```bash
git add .
git commit -m "feat: add new feature"
```
3. Run tests and formatting:
```bash
bun test
bun run typecheck
bun run format:check
```
4. Push your branch and create a Pull Request:
```bash
git push origin feature/your-feature-name
```
5. Ensure all CI checks pass
6. Request review from maintainers
## Action Development
### Testing Your Changes
When modifying the action:
1. Test locally with the test script:
```bash
./test-local.sh
```
2. Test in a real GitHub Actions workflow by:
- Creating a test repository
- Using your branch as the action source:
```yaml
uses: your-username/claude-code-action@your-branch
```
### Debugging
- Use `console.log` for debugging in development
- Check GitHub Actions logs for runtime issues
- Use `act` with `-v` flag for verbose output:
```bash
act push -v --secret ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
```
## Common Issues
### Docker Issues
Make sure Docker is running before using `act`. You can check with:
```bash
docker ps
```

156
FAQ.md
View File

@@ -1,156 +0,0 @@
# Frequently Asked Questions (FAQ)
This FAQ addresses common questions and gotchas when using the Claude Code GitHub Action.
## Triggering and Authentication
### 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.
### Why does Claude say I don't have permission to trigger it?
Only users with **write permissions** to the repository can trigger Claude. This is a security feature to prevent unauthorized use. Make sure the user commenting has at least write access to the repository.
### Why am I getting OIDC authentication errors?
If you're using the default GitHub App authentication, you must add the `id-token: write` permission to your workflow:
```yaml
permissions:
contents: read
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.
## Claude's Capabilities and Limitations
### Why won't Claude update workflow files when I ask it to?
The GitHub App for Claude doesn't have workflow write access for security reasons. This prevents Claude from modifying CI/CD configurations that could potentially create unintended consequences. This is something we may reconsider in the future.
### Why won't Claude rebase my branch?
By default, Claude only uses commit tools for non-destructive changes to the branch. Claude is configured to:
- Never push to branches other than where it was invoked (either its own branch or the PR branch)
- Never force push or perform destructive operations
You can grant additional tools via the `allowed_tools` input if needed:
```yaml
allowed_tools: "Bash(git rebase:*)" # Use with caution
```
### Why won't Claude create a pull request?
Claude doesn't create PRs by default. Instead, it pushes commits to a branch and provides a link to a pre-filled PR submission page. This approach ensures your repository's branch protection rules are still adhered to and gives you final control over PR creation.
### Why can't Claude run my tests or see CI results?
Claude cannot access GitHub Actions logs, test results, or other CI/CD outputs by default. It only has access to the repository files. If you need Claude to see test results, you can either:
1. Instruct Claude to run tests before making commits
2. Copy and paste CI results into a comment for Claude to analyze
This limitation exists for security reasons but may be reconsidered in the future based on user feedback.
### Why does Claude only update one comment instead of creating new ones?
Claude is configured to update a single comment to avoid cluttering PR/issue discussions. All of Claude's responses, including progress updates and final results, will appear in the same comment with checkboxes showing task progress.
## Branch and Commit Behavior
### Why did Claude create a new branch when commenting on a closed PR?
Claude's branch behavior depends on the context:
- **Open PRs**: Pushes directly to the existing PR branch
- **Closed/Merged PRs**: Creates a new branch (cannot push to closed PR branches)
- **Issues**: Always creates a new branch with a timestamp
### Why are my commits shallow/missing history?
For performance, Claude uses shallow clones:
- PRs: `--depth=20` (last 20 commits)
- New branches: `--depth=1` (single commit)
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
```
- uses: actions/checkout@v4
depth: 0 # will fetch full repo history
```
## Configuration and Tools
### What's the difference between `direct_prompt` and `custom_instructions`?
These inputs serve different purposes in how Claude responds:
- **`direct_prompt`**: Bypasses trigger detection entirely. When provided, Claude executes this exact instruction regardless of comments or mentions. Perfect for automated workflows where you want Claude to perform a specific task on every run (e.g., "Update the API documentation based on changes in this PR").
- **`custom_instructions`**: Additional context added to Claude's system prompt while still respecting normal triggers. These instructions modify Claude's behavior but don't replace the triggering comment. Use this to give Claude standing instructions like "You have been granted additional tools for ...".
Example:
```yaml
# Using direct_prompt - runs automatically without @claude mention
direct_prompt: "Review this PR for security vulnerabilities"
# Using custom_instructions - still requires @claude trigger
custom_instructions: "Focus on performance implications and suggest optimizations"
```
### Why doesn't Claude execute my bash commands?
The Bash tool is **disabled by default** for security. To enable individual bash commands:
```yaml
allowed_tools: "Bash(npm:*),Bash(git:*)" # Allows only npm and git commands
```
### Can Claude work across multiple repositories?
No, Claude's GitHub app token is sandboxed to the current repository only. It cannot push to any other repositories. It can, however, read public repositories, but to get access to this, you must configure it with tools to do so.
## MCP Servers and Extended Functionality
### What MCP servers are available by default?
Claude Code Action automatically configures two MCP servers:
1. **GitHub MCP server**: For GitHub API operations
2. **File operations server**: For advanced file manipulation
However, tools from these servers still need to be explicitly allowed via `allowed_tools`.
## Troubleshooting
### How can I debug what Claude is doing?
Check the GitHub Action log for Claude's run for the full execution trace.
### Why can't I trigger Claude with `@claude-mention` or `claude!`?
The trigger uses word boundaries, so `@claude` must be a complete word. Variations like `@claude-bot`, `@claude!`, or `claude@mention` won't work unless you customize the `trigger_phrase`.
## Best Practices
1. **Always specify permissions explicitly** in your workflow file
2. **Use GitHub Secrets** for API keys - never hardcode them
3. **Be specific with `allowed_tools`** - only enable what's necessary
4. **Test in a separate branch** before using on important PRs
5. **Monitor Claude's token usage** to avoid hitting API limits
6. **Review Claude's changes** carefully before merging
## Getting Help
If you encounter issues not covered here:
1. Check the [GitHub Issues](https://github.com/anthropics/claude-code-action/issues)
2. Review the [example workflows](https://github.com/anthropics/claude-code-action#examples)
[perms]: https://docs.anthropic.com/en/docs/claude-code/settings#permissions

View File

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

340
README.md
View File

@@ -1,45 +1,31 @@
# Claude Code Action (Gitea Fork)
# Claude Code Action for Gitea
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.
![Claude Code Action in action](assets/preview.png)
> **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.
A Gitea action that 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 action is designed specifically for Gitea installations, using local git operations for optimal compatibility with Gitea's API capabilities.
## Features
- 🤖 **Interactive Code Assistant**: Claude can answer questions about code, architecture, and programming
- 🔍 **Code Review**: Analyzes PR changes and suggests improvements
-**Code Implementation**: Can implement simple fixes, refactoring, and even new features
- 💬 **PR/Issue Integration**: Works seamlessly with GitHub comments and PR reviews
- 🛠️ **Flexible Tool Access**: Access to GitHub APIs and file operations (additional tools can be enabled via configuration)
- 💬 **PR/Issue Integration**: Works seamlessly with Gitea comments and PR reviews
- 🛠️ **Flexible Tool Access**: Access to Gitea APIs and file operations (additional tools can be enabled via configuration)
- 📋 **Progress Tracking**: Visual progress indicators with checkboxes that dynamically update as Claude completes tasks
- 🏃 **Runs on Your Infrastructure**: The action executes entirely on your own GitHub runner (Anthropic API calls go to your chosen provider)
## Quickstart
The easiest way to set up this action is through [Claude Code](https://claude.ai/code) in the terminal. Just open `claude` and run `/install-github-app`.
This command will guide you through setting up the GitHub app and required secrets.
**Note**:
- You must be a repository admin to install the GitHub app and add secrets
- This quickstart method is only available for direct Anthropic API users. If you're using AWS Bedrock, please see the instructions below.
### Manual Setup (Direct API)
## Setup
**Requirements**: You must be a repository admin to complete these steps.
1. Install the Claude GitHub app to your repository: https://github.com/apps/claude
2. Add `ANTHROPIC_API_KEY` to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions))
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
## 📚 FAQ
Having issues or questions? Check out our [Frequently Asked Questions](./FAQ.md) for solutions to common problems and detailed explanations of Claude's capabilities and limitations.
1. Add `ANTHROPIC_API_KEY` or `CLAUDE_CREDENTIALS` to your repository secrets
2. Add `GITEA_TOKEN` to your repository secrets (a personal access token with repository read/write permissions)
3. Copy the workflow file from [`examples/gitea-claude.yml`](./examples/gitea-claude.yml) into your repository's `.gitea/workflows/`
## Usage
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
Add a workflow file to your repository (e.g., `.gitea/workflows/claude.yml`):
```yaml
name: Claude Assistant
@@ -58,36 +44,79 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: markwylde/claude-code-gitea-action@v1.0.1
- uses: markwylde/claude-code-gitea-action@v1.0.5
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITEA_TOKEN }}
gitea_api_url: https://gitea.example.com
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # if you want to use direct API
claude_credentials: ${{ secrets.CLAUDE_CREDENTIALS }} # if you have a Claude Max subscription
gitea_token: ${{ secrets.GITEA_TOKEN }} # could be another users token (specific Claude user?)
claude_git_name: Claude # optional
claude_git_email: claude@anthropic.com # optional
```
## Inputs
| Input | Description | Required | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | ---------- |
| `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 | - |
| `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_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 | - |
| `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_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
| `disallowed_tools` | Tools that Claude should never use | 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 | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| Input | Description | Required | Default |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- |
| `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 | - |
| `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 | - |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | 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_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
| `disallowed_tools` | Tools that Claude should never use | 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 | - |
| `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)
> **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`.
It should look like this:
```json
{
"claudeAiOauth": {
"accessToken": "sk-ant-xxx",
"refreshToken": "sk-ant-xxx",
"expiresAt": 1748707000000,
"scopes": ["user:inference", "user:profile"]
}
}
```
3. **Configure Workflow**: Set up your workflow to use OAuth authentication:
```yaml
- uses: markwylde/claude-code-gitea-action@v1.0.5
with:
anthropic_api_key: "use-oauth"
claude_credentials: ${{ secrets.CLAUDE_CREDENTIALS }}
gitea_token: ${{ secrets.GITEA_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
This action has been enhanced to work with Gitea installations. The main differences from GitHub are:
@@ -96,29 +125,9 @@ This action has been enhanced to work with Gitea installations. The main differe
2. **API URL Configuration**: You must specify your Gitea server URL using the `gitea_api_url` input.
### Example Gitea Workflow
```yaml
name: Claude Assistant for Gitea
on:
issue_comment:
types: [created]
jobs:
claude-response:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
gitea_api_url: "https://gitea.example.com"
github_token: ${{ secrets.GITEA_TOKEN }}
```
### Gitea Setup Notes
- Use a Gitea personal access token instead of `GITHUB_TOKEN`
- Use a Gitea personal access token "GITEA_TOKEN"
- The token needs repository read/write permissions
- Claude will use local git operations for file changes and branch creation
- Only PR creation and comment updates use the Gitea API
@@ -171,11 +180,11 @@ Claude can see and analyze images, making it easy to fix visual bugs or UI issue
### Custom Automations
These examples show how to configure Claude to act automatically based on GitHub events, without requiring manual @mentions.
These examples show how to configure Claude to act automatically based on Gitea events, without requiring manual @mentions.
#### Supported GitHub Events
#### Supported Gitea Events
This action supports the following GitHub events ([learn more GitHub event triggers](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)):
This action supports the following Gitea events:
- `pull_request` - When PRs are opened or synchronized
- `issue_comment` - When comments are created on issues or PRs
@@ -197,7 +206,7 @@ on:
- "src/api/**/*.ts"
steps:
- uses: anthropics/claude-code-action@beta
- uses: markwylde/claude-code-gitea-action@v1.0.5
with:
direct_prompt: |
Update the API documentation in README.md to reflect
@@ -221,7 +230,7 @@ jobs:
github.event.pull_request.user.login == 'developer1' ||
github.event.pull_request.user.login == 'external-contributor'
steps:
- uses: anthropics/claude-code-action@beta
- uses: markwylde/claude-code-gitea-action@v1
with:
direct_prompt: |
Please provide a thorough review of this pull request.
@@ -239,7 +248,7 @@ Perfect for automatically reviewing PRs from new team members, external contribu
4. **Branch Management**: Creates new PRs for human authors, pushes directly for Claude's own PRs
5. **Communication**: Posts updates at every step to keep you informed
This action is built on top of [`anthropics/claude-code-base-action`](https://github.com/anthropics/claude-code-base-action).
This action is built specifically for Gitea environments with local git operations support.
## Capabilities and Limitations
@@ -257,7 +266,7 @@ This action is built on top of [`anthropics/claude-code-base-action`](https://gi
### What Claude Cannot Do
- **Submit PR Reviews**: Claude cannot submit formal GitHub PR reviews
- **Submit PR Reviews**: Claude cannot submit formal Gitea PR reviews
- **Approve PRs**: For security reasons, Claude cannot approve pull requests
- **Post Multiple Comments**: Claude only acts by updating its initial comment
- **Execute Commands Outside Its Context**: Claude only has access to the repository and PR/issue context it's triggered in
@@ -273,28 +282,28 @@ By default, Claude only has access to:
- File operations (reading, committing, editing files, read-only git commands)
- Comment management (creating/updating comments)
- Basic GitHub operations
- Basic Gitea operations
Claude does **not** have access to execute arbitrary Bash commands by default. If you want Claude to run specific commands (e.g., npm install, npm test), you must explicitly allow them using the `allowed_tools` configuration:
**Note**: If your repository has a `.mcp.json` file in the root directory, Claude will automatically detect and use the MCP server tools defined there. However, these tools still need to be explicitly allowed via the `allowed_tools` configuration.
```yaml
- uses: anthropics/claude-code-action@beta
- uses: markwylde/claude-code-gitea-action@v1
with:
allowed_tools: "Bash(npm install),Bash(npm run test),Edit,Replace,NotebookEditCell"
disallowed_tools: "TaskOutput,KillTask"
# ... other inputs
```
**Note**: The base GitHub tools are always included. Use `allowed_tools` to add additional tools (including specific Bash commands), and `disallowed_tools` to prevent specific tools from being used.
**Note**: The base Gitea tools are always included. Use `allowed_tools` to add additional tools (including specific Bash commands), and `disallowed_tools` to prevent specific tools from being used.
### Custom Model
Use a specific Claude model:
```yaml
- uses: anthropics/claude-code-action@beta
- uses: markwylde/claude-code-gitea-action@v1
with:
# model: "claude-3-5-sonnet-20241022" # Optional: specify a different model
# ... other inputs
@@ -305,187 +314,22 @@ Use a specific Claude model:
You can authenticate with Claude using any of these three methods:
1. Direct Anthropic API (default)
2. Amazon Bedrock with OIDC authentication
3. Google Vertex AI with OIDC authentication
For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions#using-with-aws-bedrock-%26-google-vertex-ai).
**Note**:
- Bedrock and Vertex use OIDC authentication exclusively
- AWS Bedrock automatically uses cross-region inference profiles for certain models
- For cross-region inference profile models, you need to request and be granted access to the Claude models in all regions that the inference profile uses
### Model Configuration
Use provider-specific model names based on your chosen provider:
```yaml
# For direct Anthropic API (default)
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# ... other inputs
# For Amazon Bedrock with OIDC
- uses: anthropics/claude-code-action@beta
with:
model: "anthropic.claude-3-7-sonnet-20250219-beta:0" # Cross-region inference
use_bedrock: "true"
# ... other inputs
# For Google Vertex AI with OIDC
- uses: anthropics/claude-code-action@beta
with:
model: "claude-3-7-sonnet@20250219"
use_vertex: "true"
# ... other inputs
```
### OIDC Authentication for Bedrock and Vertex
Both AWS Bedrock and GCP Vertex AI require OIDC authentication.
```yaml
# For AWS Bedrock with OIDC
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: us-west-2
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: anthropics/claude-code-action@beta
with:
model: "anthropic.claude-3-7-sonnet-20250219-beta:0"
use_bedrock: "true"
# ... other inputs
permissions:
id-token: write # Required for OIDC
```
```yaml
# For GCP Vertex AI with OIDC
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: anthropics/claude-code-action@beta
with:
model: "claude-3-7-sonnet@20250219"
use_vertex: "true"
# ... other inputs
permissions:
id-token: write # Required for OIDC
```
2. Anthropic OAuth credentials (Claude Max subscription)
## Security
### Access Control
- **Repository Access**: The action can only be triggered by users with write access to the repository
- **No Bot Triggers**: GitHub Apps and bots cannot trigger this action
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
- **No Bot Triggers**: Bots cannot trigger this action
- **Token Permissions**: The Gitea token is scoped specifically to the repository it's operating in
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
### GitHub App Permissions
### Gitea Token Permissions
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
The Gitea personal access token requires these permissions:
- **Pull Requests**: Read and write to create PRs and push changes
- **Issues**: Read and write to respond to issues
- **Contents**: Read and write to modify repository files
### Commit Signing
All commits made by Claude through this action are automatically signed with commit signatures. This ensures the authenticity and integrity of commits, providing a verifiable trail of changes made by the action.
### ⚠️ ANTHROPIC_API_KEY Protection
**CRITICAL: Never hardcode your Anthropic API key in workflow files!**
Your ANTHROPIC_API_KEY must always be stored in GitHub secrets to prevent unauthorized access:
```yaml
# CORRECT ✅
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# NEVER DO THIS ❌
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
```
### Setting Up GitHub Secrets
1. Go to your repository's Settings
2. Click on "Secrets and variables" → "Actions"
3. Click "New repository secret"
4. Name: `ANTHROPIC_API_KEY`
5. Value: Your Anthropic API key (starting with `sk-ant-`)
6. Click "Add secret"
### Best Practices for ANTHROPIC_API_KEY
1. ✅ Always use `${{ secrets.ANTHROPIC_API_KEY }}` in workflows
2. ✅ Never commit API keys to version control
3. ✅ Regularly rotate your API keys
4. ✅ Use environment secrets for organization-wide access
5. ❌ Never share API keys in pull requests or issues
6. ❌ Avoid logging workflow variables that might contain keys
## Security Best Practices
**⚠️ IMPORTANT: Never commit API keys directly to your repository! Always use GitHub Actions secrets.**
To securely use your Anthropic API key:
1. Add your API key as a repository secret:
- Go to your repository's Settings
- Navigate to "Secrets and variables" → "Actions"
- Click "New repository secret"
- Name it `ANTHROPIC_API_KEY`
- Paste your API key as the value
2. Reference the secret in your workflow:
```yaml
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
**Never do this:**
```yaml
# ❌ WRONG - Exposes your API key
anthropic_api_key: "sk-ant-..."
```
**Always do this:**
```yaml
# ✅ CORRECT - Uses GitHub secrets
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
This applies to all sensitive values including API keys, access tokens, and credentials.
We also recommend that you always use short-lived tokens when possible
## License
This project is licensed under the MIT License—see the LICENSE file for details.

View File

@@ -1,15 +0,0 @@
# Security Policy
Thank you for helping us keep this action and the systems they interact with secure.
## Reporting Security Issues
This repository is maintained by [Anthropic](https://www.anthropic.com/).
The security of our systems and user data is Anthropics top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
## Vulnerability Disclosure Program
Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).

View File

@@ -42,13 +42,13 @@ inputs:
# Auth configuration
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
github_token:
description: "GitHub token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
claude_credentials:
description: "Claude OAuth credentials JSON for Claude AI Max subscription authentication"
required: false
gitea_api_url:
description: "Gitea server URL (e.g., https://gitea.example.com, defaults to GitHub API)"
gitea_token:
description: "Gitea token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
required: false
use_bedrock:
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
@@ -63,6 +63,14 @@ inputs:
description: "Timeout in minutes for execution"
required: false
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:
execution_file:
@@ -95,15 +103,17 @@ runs:
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.gitea_token }}
GITHUB_TOKEN: ${{ github.token }}
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
id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true'
uses: anthropics/claude-code-base-action@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8
uses: anthropics/claude-code-base-action@v0.0.24
with:
prompt_file: /tmp/claude-prompts/claude-prompt.txt
allowed_tools: ${{ env.ALLOWED_TOOLS }}
@@ -126,10 +136,15 @@ runs:
USE_BEDROCK: ${{ inputs.use_bedrock }}
USE_VERTEX: ${{ inputs.use_vertex }}
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }}
# GitHub token for repository access
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)
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
@@ -167,7 +182,7 @@ 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 || '' }}
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
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
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''

BIN
assets/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

View File

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

View File

@@ -29,9 +29,9 @@ jobs:
fetch-depth: 0
- name: Run Claude Assistant
uses: ./ # Use local action (adjust path as needed)
uses: markwylde/claude-code-gitea-action
with:
github_token: ${{ secrets.GITHUB_TOKEN }} # Use standard workflow token
gitea_token: ${{ secrets.GITEA_TOKEN }} # Use standard workflow token
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
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,7 +33,31 @@ const BASE_ALLOWED_TOOLS = [
"mcp__local_git_ops__delete_files",
"mcp__local_git_ops__push_branch",
"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__gitea__get_issue",
"mcp__gitea__get_issue_comments",
"mcp__gitea__add_issue_comment",
"mcp__gitea__update_issue_comment",
"mcp__gitea__delete_issue_comment",
"mcp__gitea__get_comment",
"mcp__gitea__list_issues",
"mcp__gitea__create_issue",
"mcp__gitea__update_issue",
"mcp__gitea__get_repository",
"mcp__gitea__list_pull_requests",
"mcp__gitea__get_pull_request",
"mcp__gitea__create_pull_request",
"mcp__gitea__update_pull_request",
"mcp__gitea__update_pull_request_comment",
"mcp__gitea__merge_pull_request",
"mcp__gitea__update_pull_request_branch",
"mcp__gitea__check_pull_request_merged",
"mcp__gitea__set_issue_branch",
"mcp__gitea__list_branches",
"mcp__gitea__get_branch",
"mcp__gitea__delete_file",
];
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
@@ -46,10 +70,10 @@ export function buildAllowedToolsString(
// Add the appropriate comment tool based on event type
if (eventData.eventName === "pull_request_review_comment") {
// For inline PR review comments, only use PR comment tool
baseTools.push("mcp__github__update_pull_request_comment");
baseTools.push("mcp__gitea__update_pull_request_comment");
} else {
// For all other events (issue comments, PR reviews, issues), use issue comment tool
baseTools.push("mcp__github__update_issue_comment");
baseTools.push("mcp__gitea__update_issue_comment");
}
let allAllowedTools = baseTools.join(",");
@@ -217,8 +241,6 @@ export function prepareContext(
...(baseBranch && { baseBranch }),
};
break;
} else if (!claudeBranch) {
throw new Error("CLAUDE_BRANCH is required for issue_comment event");
} else if (!baseBranch) {
throw new Error("BASE_BRANCH is required for issue_comment event");
} else if (!issueNumber) {
@@ -231,10 +253,10 @@ export function prepareContext(
eventName: "issue_comment",
commentId,
isPR: false,
claudeBranch: claudeBranch,
baseBranch,
issueNumber,
commentBody,
...(claudeBranch && { claudeBranch }),
};
break;
@@ -251,9 +273,6 @@ export function prepareContext(
if (!baseBranch) {
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 (!assigneeTrigger) {
@@ -267,8 +286,8 @@ export function prepareContext(
isPR: false,
issueNumber,
baseBranch,
claudeBranch,
assigneeTrigger,
...(claudeBranch && { claudeBranch }),
};
} else if (eventAction === "opened") {
eventData = {
@@ -277,7 +296,7 @@ export function prepareContext(
isPR: false,
issueNumber,
baseBranch,
claudeBranch,
...(claudeBranch && { claudeBranch }),
};
} else {
throw new Error(`Unsupported issue action: ${eventAction}`);
@@ -391,7 +410,7 @@ export function generatePrompt(
? `
<images_info>
Images have been downloaded from GitHub comments and saved to disk. Their file paths are included in the formatted comments and body above. You can use the Read tool to view these images.
Images have been downloaded from Gitea comments and saved to disk. Their file paths are included in the formatted comments and body above. You can use the Read tool to view these images.
</images_info>`
: "";
@@ -399,7 +418,7 @@ Images have been downloaded from GitHub comments and saved to disk. Their file p
? formatBody(contextData.body, imageUrlMap)
: "No description provided";
let promptContent = `You are Claude, an AI assistant designed to help with GitHub issues and pull requests. Think carefully as you analyze the context and respond appropriately. Here's the context for your current task:
let promptContent = `You are Claude, an AI assistant designed to help with Gitea issues and pull requests. Think carefully as you analyze the context and respond appropriately. Here's the context for your current task:
<formatted_context>
${formattedContext}
@@ -453,9 +472,9 @@ ${sanitizeContent(context.directPrompt)}
${
eventData.eventName === "pull_request_review_comment"
? `<comment_tool_info>
IMPORTANT: For this inline PR review comment, you have been provided with ONLY the mcp__github__update_pull_request_comment tool to update this specific review comment.
IMPORTANT: For this inline PR review comment, you have been provided with ONLY the mcp__gitea__update_pull_request_comment tool to update this specific review comment.
Tool usage example for mcp__github__update_pull_request_comment:
Tool usage example for mcp__gitea__update_pull_request_comment:
{
"owner": "${context.repository.split("/")[0]}",
"repo": "${context.repository.split("/")[1]}",
@@ -465,9 +484,9 @@ Tool usage example for mcp__github__update_pull_request_comment:
All four parameters (owner, repo, commentId, body) are required.
</comment_tool_info>`
: `<comment_tool_info>
IMPORTANT: For this event type, you have been provided with ONLY the mcp__github__update_issue_comment tool to update comments.
IMPORTANT: For this event type, you have been provided with ONLY the mcp__gitea__update_issue_comment tool to update comments.
Tool usage example for mcp__github__update_issue_comment:
Tool usage example for mcp__gitea__update_issue_comment:
{
"owner": "${context.repository.split("/")[0]}",
"repo": "${context.repository.split("/")[1]}",
@@ -483,21 +502,21 @@ Your task is to analyze the context, understand the request, and provide helpful
IMPORTANT CLARIFICATIONS:
- When asked to "review" code, read the code and provide review feedback (do not implement changes unless explicitly asked)${eventData.isPR ? "\n- For PR reviews: Your review will be posted when you update the comment. Focus on providing comprehensive review feedback." : ""}
- Your console outputs and tool results are NOT visible to the user
- ALL communication happens through your GitHub comment - that's how users see your feedback, answers, and progress. your normal responses are not seen.
- ALL communication happens through your Gitea comment - that's how users see your feedback, answers, and progress. your normal responses are not seen.
Follow these steps:
1. Create a Todo List:
- Use your GitHub comment to maintain a detailed task list based on the request.
- Use your Gitea comment to maintain a detailed task list based on the request.
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
- Update the comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__github__update_pull_request_comment" : "mcp__github__update_issue_comment"} with each task completion.
- Update the comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__gitea__update_pull_request_comment" : "mcp__gitea__update_issue_comment"} with each task completion.
2. Gather Context:
- Analyze the pre-fetched data provided above.
- For ISSUE_CREATED: Read the issue body to find the request after the trigger phrase.
- For ISSUE_ASSIGNED: Read the entire issue body to understand the task.
${eventData.eventName === "issue_comment" || eventData.eventName === "pull_request_review_comment" || eventData.eventName === "pull_request_review" ? ` - For comment/review events: Your instructions are in the <trigger_comment> tag above.` : ""}
${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was provided and is shown in the <direct_prompt> tag above. This is not from any GitHub comment but a direct instruction to execute.` : ""}
${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was provided and is shown in the <direct_prompt> tag above. This is not from any Gitea comment but a direct instruction to execute.` : ""}
- IMPORTANT: Only the comment/issue containing '${context.triggerPhrase}' has your instructions.
- Other comments may contain requests from other users, but DO NOT act on those unless the trigger comment explicitly asks you to.
- Use the Read tool to look at relevant files for better context.
@@ -512,7 +531,20 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
- For implementation requests, assess if they are straightforward or complex.
- 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 the mcp__gitea__list_branches tool to list 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.
A. For Answering Questions and Code Reviews:
@@ -520,11 +552,11 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
- Look for bugs, security issues, performance problems, and other issues
- Suggest improvements for readability and maintainability
- Check for best practices and coding standards
- Reference specific code sections with file paths and line numbers${eventData.isPR ? "\n - AFTER reading files and analyzing code, you MUST call mcp__github__update_issue_comment to post your review" : ""}
- Reference specific code sections with file paths and line numbers${eventData.isPR ? "\n - AFTER reading files and analyzing code, you MUST call mcp__gitea__update_issue_comment to post your review" : ""}
- Formulate a concise, technical, and helpful response based on the context.
- Reference specific code with inline formatting or code blocks.
- Include relevant file paths and line numbers when applicable.
- ${eventData.isPR ? "IMPORTANT: Submit your review feedback by updating the Claude comment. This will be displayed as your PR review." : "Remember that this feedback must be posted to the GitHub comment."}
- ${eventData.isPR ? "IMPORTANT: Submit your review feedback by updating the Claude comment. This will be displayed as your PR review." : "Remember that this feedback must be posted to the Gitea comment."}
B. For Straightforward Changes:
- Use file system tools to make the change locally.
@@ -534,33 +566,34 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
eventData.isPR && !eventData.claudeBranch
? `
- Commit changes using mcp__local_git_ops__commit_files to the existing branch (works for both new and existing files).
- Make sure commits follow the same convention as other commits in the repository.
- 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
- After pushing, you MUST create a PR using mcp__local_git_ops__create_pull_request.
- 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.`
: `
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
: eventData.claudeBranch
? `
- 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)
- Make sure commits follow the same convention as other commits in the repository.
- 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.
${
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>)
- 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 the current branch: ${eventData.claudeBranch}
- 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"`
: ""
}`
`
: `
- 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
- After pushing, you should create a PR using mcp__local_git_ops__create_pull_request unless one already exists for that branch.
`
}
C. For Complex Changes:
@@ -572,26 +605,26 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
- 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.
5. Final Update:
- Always update the GitHub comment to reflect the current todo state.
${!eventData.isPR || !eventData.claudeBranch ? `6. Final Update:` : `5. Final Update:`}
- Always update the Gitea 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.
- 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.
${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, you must create a PR using mcp__local_git_ops__create_pull_request.` : ""}
Important Notes:
- All communication must happen through GitHub PR comments.
- Never create new comments. Only update the existing comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__github__update_pull_request_comment" : "mcp__github__update_issue_comment"} with comment_id: ${context.claudeCommentId}.
- 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." : ""}
- All communication must happen through Gitea PR comments.
- Never create new comments. Only update the existing comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__gitea__update_pull_request_comment" : "mcp__gitea__update_issue_comment"} with comment_id: ${context.claudeCommentId}.
- This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__gitea__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.
- 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.`}
- 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.
${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__gitea__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:
- mcp__local_git_ops__commit_files: {"files": ["path/to/file1.js", "path/to/file2.py"], "message": "feat: add new feature"}
- mcp__local_git_ops__push_branch: {"branch": "branch-name"} (REQUIRED after committing to push changes to remote)
- mcp__local_git_ops__delete_files: {"files": ["path/to/old.js"], "message": "chore: remove deprecated file"}
- Display the todo list as a checklist in the GitHub comment and mark things off as you go.
- Display the todo list as a checklist in the Gitea comment and mark things off as you go.
- REPOSITORY SETUP INSTRUCTIONS: The repository's CLAUDE.md file(s) contain critical repo-specific setup instructions, development guidelines, and preferences. Always read and follow these files, particularly the root CLAUDE.md, as they provide essential context for working with the codebase effectively.
- Use h3 headers (###) for section titles in your comments, not h1 headers (#).
- Your comment must always include the job run link (and branch link if there is one) at the bottom.
@@ -606,19 +639,16 @@ What You CAN Do:
- Implement code changes (simple to moderate complexity) when explicitly requested
- Create pull requests for changes to human-authored code
- Smart branch handling:
- When triggered on an issue: Always create a new 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
- When triggered on an issue: Create a new branch using mcp__local_git_ops__create_branch
- When triggered on an open PR: Push directly to the existing PR 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:
- Submit formal GitHub PR reviews
- Approve pull requests (for security reasons)
- Post multiple comments (you only update your initial comment)
- Execute commands outside the repository context
- 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)
- 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)
- 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 (Gitea App permissions do not allow workflow modifications)
- View CI/CD results or workflow run outputs (cannot access Gitea Actions logs or test results)
When users ask you to perform actions you cannot do, politely explain the limitation and, when applicable, direct them to the FAQ for more information and workarounds:
"I'm unable to [specific action] due to [reason]. Please check the documentation for more information and potential workarounds."

View File

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

View File

@@ -18,17 +18,29 @@ import { createPrompt } from "../create-prompt";
import { createClient } from "../github/api/client";
import { fetchGitHubData } from "../github/data/fetcher";
import { parseGitHubContext } from "../github/context";
import { setupOAuthCredentials } from "../claude/oauth-setup";
async function run() {
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 client = createClient(githubToken);
// Step 2: Parse GitHub context (once for all operations)
// Step 3: Parse GitHub context (once for all operations)
const context = parseGitHubContext();
// Step 3: Check write permissions
// Step 4: Check write permissions
const hasWritePermissions = await checkWritePermissions(
client.api,
context,
@@ -39,7 +51,7 @@ async function run() {
);
}
// Step 4: Check trigger conditions
// Step 5: Check trigger conditions
const containsTrigger = await checkTriggerAction(context);
// Set outputs that are always needed
@@ -51,14 +63,14 @@ async function run() {
return;
}
// Step 5: Check if actor is human
// Step 6: Check if actor is human
await checkHumanActor(client.api, context);
// Step 6: Create initial tracking comment
// Step 7: Create initial tracking comment
const commentId = await createInitialComment(client.api, context);
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({
client: client,
repository: `${context.repository.owner}/${context.repository.repo}`,
@@ -66,14 +78,14 @@ async function run() {
isPR: context.isPR,
});
// Step 8: Setup branch
// Step 9: Setup branch
const branchInfo = await setupBranch(client, githubData, context);
core.setOutput("BASE_BRANCH", branchInfo.baseBranch);
if (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) {
await updateTrackingComment(
client,
@@ -83,7 +95,7 @@ async function run() {
);
}
// Step 10: Create prompt file
// Step 11: Create prompt file
await createPrompt(
commentId,
branchInfo.baseBranch,
@@ -92,7 +104,7 @@ async function run() {
context,
);
// Step 11: Get MCP configuration
// Step 12: Get MCP configuration
const mcpConfig = await prepareMcpConfig(
githubToken,
context.repository.owner,

View File

@@ -32,7 +32,7 @@ async function run() {
const client = createClient(githubToken);
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 isPRReviewComment = false;
@@ -146,7 +146,7 @@ async function run() {
`${entityType} #${context.entityNumber}: Changes from Claude`,
);
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
@@ -169,7 +169,7 @@ async function run() {
`${entityType} #${context.entityNumber}: Changes from Claude`,
);
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
@@ -189,7 +189,7 @@ async function run() {
`${entityType} #${context.entityNumber}: Changes from Claude`,
);
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
@@ -226,7 +226,7 @@ async function run() {
`${entityType} #${context.entityNumber}: Changes from Claude`,
);
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
@@ -253,7 +253,7 @@ async function run() {
`${entityType} #${context.entityNumber}: Changes from Claude`,
);
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;

View File

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

View File

@@ -29,6 +29,18 @@ export async function setupBranch(
const { baseBranch } = context.inputs;
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) {
const prData = githubData.contextData as GitHubPullRequest;
const prState = prData.state;
@@ -36,9 +48,18 @@ export async function setupBranch(
// Check if PR is closed or merged
if (prState === "CLOSED" || prState === "MERGED") {
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 --depth=1 ${sourceBranch}`;
await $`git checkout ${sourceBranch}`;
await $`git pull origin ${sourceBranch}`;
return {
baseBranch: sourceBranch,
currentBranch: sourceBranch,
};
} else {
// Handle open PR: Checkout the PR branch
console.log("This is an open PR, checking out PR branch...");
@@ -62,97 +83,54 @@ export async function setupBranch(
}
}
// 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";
// For issues, check out the base branch and let Claude create branches as needed
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 {
// 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
const repoDir = process.env.GITHUB_WORKSPACE || process.cwd();
console.log(`Working in directory: ${repoDir}`);
try {
// Check if we're in a git repository
console.log(`Checking if we're in a git repository...`);
await $`git status`;
// Check if we're in a git repository
console.log(`Checking if we're in a git repository...`);
await $`git status`;
// Ensure we have the latest version of the source branch
console.log(`Fetching latest ${sourceBranch}...`);
await $`git fetch origin ${sourceBranch}`;
// Ensure we have the latest version of the source branch
console.log(`Fetching latest ${sourceBranch}...`);
await $`git fetch origin --depth=1 ${sourceBranch}`;
// Checkout the source branch
console.log(`Checking out ${sourceBranch}...`);
await $`git checkout ${sourceBranch}`;
// Checkout the source branch
console.log(`Checking out ${sourceBranch}...`);
await $`git checkout ${sourceBranch}`;
// Pull latest changes
console.log(`Pulling latest changes for ${sourceBranch}...`);
await $`git pull origin ${sourceBranch}`;
// Pull latest changes
console.log(`Pulling latest changes for ${sourceBranch}...`);
await $`git pull origin ${sourceBranch}`;
// Create and checkout the new branch
console.log(`Creating new branch: ${newBranch}`);
await $`git checkout -b ${newBranch}`;
// Verify the branch was checked out
const currentBranch = await $`git branch --show-current`;
const branchName = currentBranch.text().trim();
console.log(`Current branch: ${branchName}`);
// Verify the branch was created
const currentBranch = await $`git branch --show-current`;
const branchName = currentBranch.text().trim();
console.log(`Current branch after creation: ${branchName}`);
if (branchName === newBranch) {
console.log(
`✅ Successfully created and checked out branch: ${newBranch}`,
);
} else {
throw new Error(
`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
if (branchName === sourceBranch) {
console.log(`✅ Successfully checked out base branch: ${sourceBranch}`);
} else {
throw new Error(
`Failed to create branch ${newBranch}: ${gitError.message || gitError}`,
`Branch checkout failed. Expected ${sourceBranch}, got ${branchName}`,
);
}
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
core.setOutput("CLAUDE_BRANCH", newBranch);
core.setOutput("BASE_BRANCH", sourceBranch);
return {
baseBranch: sourceBranch,
claudeBranch: newBranch,
currentBranch: newBranch,
currentBranch: sourceBranch,
};
} catch (error) {
console.error("Error setting up branch:", error);

View File

@@ -23,11 +23,11 @@ export async function setupGitHubToken(): Promise<string> {
}
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) {
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);
}

View File

@@ -85,7 +85,7 @@ export async function branchHasChanges(
*/
export async function fetchBranch(branchName: string): Promise<boolean> {
try {
await $`git fetch origin ${branchName}`;
await $`git fetch origin --depth=1 ${branchName}`;
return true;
} catch (error) {
console.log(

View File

@@ -213,13 +213,15 @@ server.tool(
"update_issue_comment",
"Update an existing issue comment",
{
comment_id: z.number().describe("The comment ID to update"),
owner: z.string().describe("Repository owner"),
repo: z.string().describe("Repository name"),
commentId: z.number().describe("The comment ID to update"),
body: z.string().describe("The new comment body content"),
},
async ({ comment_id, body }) => {
async ({ owner, repo, commentId, body }) => {
try {
const comment = await giteaRequest(
`/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/comments/${comment_id}`,
`/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`,
"PATCH",
{ body },
);
@@ -758,6 +760,507 @@ server.tool(
},
);
// Update a pull request
server.tool(
"update_pull_request",
"Update an existing pull request",
{
pull_number: z.number().describe("The pull request number to update"),
title: z.string().optional().describe("New pull request title"),
body: z.string().optional().describe("New pull request body/description"),
base: z.string().optional().describe("New base branch name"),
assignee: z
.string()
.optional()
.describe("Username to assign the pull request to"),
assignees: z
.array(z.string())
.optional()
.describe("Array of usernames to assign the pull request to"),
milestone: z
.number()
.optional()
.describe("Milestone ID to associate with the pull request"),
labels: z
.array(z.string())
.optional()
.describe("Array of label names to apply to the pull request"),
state: z.enum(["open", "closed"]).optional().describe("Pull request state"),
allow_maintainer_edit: z
.boolean()
.optional()
.describe("Allow maintainer edits"),
},
async ({
pull_number,
title,
body,
base,
assignee,
assignees,
milestone,
labels,
state,
allow_maintainer_edit,
}) => {
try {
const updateData: any = {};
if (title) updateData.title = title;
if (body !== undefined) updateData.body = body;
if (base) updateData.base = base;
if (assignee) updateData.assignee = assignee;
if (assignees) updateData.assignees = assignees;
if (milestone) updateData.milestone = milestone;
if (labels) updateData.labels = labels;
if (state) updateData.state = state;
if (allow_maintainer_edit !== undefined)
updateData.allow_maintainer_edit = allow_maintainer_edit;
const pull = await giteaRequest(
`/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${pull_number}`,
"PATCH",
updateData,
);
return {
content: [
{
type: "text",
text: JSON.stringify(pull, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[GITEA-MCP] Error updating pull request: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error updating pull request: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// Merge a pull request
server.tool(
"merge_pull_request",
"Merge a pull request",
{
pull_number: z.number().describe("The pull request number to merge"),
merge_method: z
.enum([
"merge",
"rebase",
"rebase-merge",
"squash",
"fast-forward-only",
"manually-merged",
])
.optional()
.default("merge")
.describe("Merge strategy to use"),
merge_commit_id: z
.string()
.optional()
.describe("Specific commit ID to merge"),
merge_message: z
.string()
.optional()
.describe("Custom merge commit message"),
merge_title: z.string().optional().describe("Custom merge commit title"),
},
async ({
pull_number,
merge_method = "merge",
merge_commit_id,
merge_message,
merge_title,
}) => {
try {
const mergeData: any = { Do: merge_method };
if (merge_commit_id) mergeData.MergeCommitID = merge_commit_id;
if (merge_message) mergeData.MergeMessageField = merge_message;
if (merge_title) mergeData.MergeTitleField = merge_title;
const result = await giteaRequest(
`/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${pull_number}/merge`,
"POST",
mergeData,
);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[GITEA-MCP] Error merging pull request: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error merging pull request: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// Update pull request branch
server.tool(
"update_pull_request_branch",
"Update a pull request branch to latest base",
{
pull_number: z.number().describe("The pull request number to update"),
style: z
.enum(["merge", "rebase"])
.optional()
.default("merge")
.describe("How to update the pull request branch"),
},
async ({ pull_number, style = "merge" }) => {
try {
let endpoint = `/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${pull_number}/update`;
if (style) {
endpoint += `?style=${style}`;
}
const result = await giteaRequest(endpoint, "POST");
return {
content: [
{
type: "text",
text: `Successfully updated pull request ${pull_number} branch using ${style} strategy`,
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
`[GITEA-MCP] Error updating pull request branch: ${errorMessage}`,
);
return {
content: [
{
type: "text",
text: `Error updating pull request branch: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// Check if pull request is merged
server.tool(
"check_pull_request_merged",
"Check if a pull request is merged",
{
pull_number: z.number().describe("The pull request number to check"),
},
async ({ pull_number }) => {
try {
await giteaRequest(
`/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${pull_number}/merge`,
"GET",
);
return {
content: [
{
type: "text",
text: `Pull request ${pull_number} is merged`,
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (errorMessage.includes("404")) {
return {
content: [
{
type: "text",
text: `Pull request ${pull_number} is not merged`,
},
],
};
}
console.error(
`[GITEA-MCP] Error checking pull request merge status: ${errorMessage}`,
);
return {
content: [
{
type: "text",
text: `Error checking pull request merge status: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// Set the active branch of an issue
server.tool(
"set_issue_branch",
"Set the active branch reference for an issue",
{
issue_number: z.number().describe("The issue number to update"),
branch: z
.string()
.describe("The branch name to set as active for this issue"),
},
async ({ issue_number, branch }) => {
try {
const updateData = { ref: branch };
const issue = await giteaRequest(
`/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_number}`,
"PATCH",
updateData,
);
return {
content: [
{
type: "text",
text: JSON.stringify(issue, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[GITEA-MCP] Error setting issue branch: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error setting issue branch: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// List repository branches
server.tool(
"list_branches",
"List all branches in the repository",
{
page: z.number().optional().describe("Page number for pagination"),
limit: z.number().optional().describe("Number of items per page"),
},
async ({ page, limit }) => {
try {
let endpoint = `/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/branches`;
const params = new URLSearchParams();
if (page) params.append("page", page.toString());
if (limit) params.append("limit", limit.toString());
if (params.toString()) {
endpoint += `?${params.toString()}`;
}
const branches = await giteaRequest(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify(branches, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[GITEA-MCP] Error listing branches: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error listing branches: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// Get a specific branch
server.tool(
"get_branch",
"Get details of a specific branch",
{
branch_name: z.string().describe("The branch name to fetch"),
},
async ({ branch_name }) => {
try {
const branch = await giteaRequest(
`/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/branches/${encodeURIComponent(branch_name)}`,
);
return {
content: [
{
type: "text",
text: JSON.stringify(branch, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[GITEA-MCP] Error getting branch: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error getting branch: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// Update pull request comment
server.tool(
"update_pull_request_comment",
"Update a pull request review comment",
{
owner: z.string().describe("Repository owner"),
repo: z.string().describe("Repository name"),
commentId: z.number().describe("The comment ID to update"),
body: z.string().describe("The new comment body content"),
},
async ({ owner, repo, commentId, body }) => {
try {
const comment = await giteaRequest(
`/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`,
"PATCH",
{ body },
);
return {
content: [
{
type: "text",
text: JSON.stringify(comment, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
`[GITEA-MCP] Error updating pull request comment: ${errorMessage}`,
);
return {
content: [
{
type: "text",
text: `Error updating pull request comment: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
// Delete a file from repository
server.tool(
"delete_file",
"Delete a file from the repository",
{
owner: z.string().describe("Repository owner"),
repo: z.string().describe("Repository name"),
filepath: z.string().describe("Path to the file to delete"),
message: z.string().describe("Commit message for the deletion"),
branch: z
.string()
.optional()
.describe("Branch to delete from (defaults to default branch)"),
sha: z.string().describe("SHA of the file to delete"),
},
async ({ owner, repo, filepath, message, branch, sha }) => {
try {
const deleteData: any = {
message,
sha,
};
if (branch) {
deleteData.branch = branch;
}
const result = await giteaRequest(
`/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(filepath)}`,
"DELETE",
deleteData,
);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[GITEA-MCP] Error deleting file: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error deleting file: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
async function runServer() {
console.log(`[GITEA-MCP] Starting MCP server transport...`);
const transport = new StdioServerTransport();

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

@@ -23,7 +23,7 @@ export async function prepareMcpConfig(
try {
const mcpConfig = {
mcpServers: {
github: {
gitea: {
command: "bun",
args: [
"run",

View File

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

View File

@@ -317,7 +317,7 @@ describe("generatePrompt", () => {
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
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
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(
"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(
"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
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(
"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",
);
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
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
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(
"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("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", () => {
@@ -638,8 +632,8 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("Write");
expect(result).toContain("mcp__github__update_issue_comment");
expect(result).not.toContain("mcp__github__update_pull_request_comment");
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
expect(result).toContain("mcp__local_git_ops__commit_files");
expect(result).toContain("mcp__local_git_ops__delete_files");
});
test("should return PR comment tool for inline review comments", () => {
@@ -662,8 +656,8 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("Write");
expect(result).not.toContain("mcp__github__update_issue_comment");
expect(result).toContain("mcp__github__update_pull_request_comment");
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
expect(result).toContain("mcp__local_git_ops__commit_files");
expect(result).toContain("mcp__local_git_ops__delete_files");
});
test("should append custom tools when provided", () => {