mirror of
https://github.com/markwylde/claude-code-gitea-action.git
synced 2026-02-20 02:22:49 +08:00
Attempt to make this work
This commit is contained in:
66
README.md
66
README.md
@@ -69,26 +69,62 @@ jobs:
|
|||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
| Input | Description | Required | Default |
|
| Input | Description | Required | Default |
|
||||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | ---------- |
|
||||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
||||||
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
||||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
|
| `gitea_api_url` | Gitea API URL (e.g., `https://gitea.example.com/api/v1`) for Gitea installations. Leave empty for GitHub. | No | GitHub API |
|
||||||
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
|
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
|
||||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
|
||||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||||
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
|
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||||
| `disallowed_tools` | Tools that Claude should never use | No | "" |
|
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
|
||||||
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
|
| `disallowed_tools` | Tools that Claude should never use | No | "" |
|
||||||
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
|
||||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
| `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` |
|
||||||
|
|
||||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||||
|
|
||||||
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
|
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
|
||||||
|
|
||||||
|
## Gitea Configuration
|
||||||
|
|
||||||
|
This action has been enhanced to work with Gitea installations. The main differences from GitHub are:
|
||||||
|
|
||||||
|
1. **Local Git Operations**: Instead of using API-based file operations (which have limited support in Gitea), this action uses local git commands to create branches, commit files, and push changes.
|
||||||
|
|
||||||
|
2. **API URL Configuration**: You must specify your Gitea API 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/api/v1"
|
||||||
|
github_token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gitea Setup Notes
|
||||||
|
|
||||||
|
- Use a Gitea personal access token instead of `GITHUB_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
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Ways to Tag @claude
|
### Ways to Tag @claude
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ inputs:
|
|||||||
github_token:
|
github_token:
|
||||||
description: "GitHub token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
|
description: "GitHub token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
|
||||||
required: false
|
required: false
|
||||||
|
gitea_api_url:
|
||||||
|
description: "Gitea API URL (e.g., https://gitea.example.com/api/v1, defaults to GitHub API)"
|
||||||
|
required: false
|
||||||
use_bedrock:
|
use_bedrock:
|
||||||
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
||||||
required: false
|
required: false
|
||||||
@@ -95,6 +98,7 @@ runs:
|
|||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
GITEA_API_URL: ${{ inputs.gitea_api_url }}
|
||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
id: claude-code
|
id: claude-code
|
||||||
@@ -117,6 +121,7 @@ runs:
|
|||||||
|
|
||||||
# GitHub token for repository access
|
# GitHub token for repository access
|
||||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||||
|
GITEA_API_URL: ${{ inputs.gitea_api_url }}
|
||||||
|
|
||||||
# Provider configuration (for future cloud provider support)
|
# Provider configuration (for future cloud provider support)
|
||||||
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
||||||
@@ -154,6 +159,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 || '' }}
|
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
||||||
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||||
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
||||||
|
GITEA_API_URL: ${{ inputs.gitea_api_url }}
|
||||||
|
|
||||||
- name: Display Claude Code Report
|
- name: Display Claude Code Report
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ async function run() {
|
|||||||
if (isPullRequestReviewCommentEvent(context)) {
|
if (isPullRequestReviewCommentEvent(context)) {
|
||||||
// For PR review comments, use the pulls API
|
// For PR review comments, use the pulls API
|
||||||
console.log(`Fetching PR review comment ${commentId}`);
|
console.log(`Fetching PR review comment ${commentId}`);
|
||||||
const response = await client.api.customRequest("GET", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`);
|
const response = await client.api.customRequest(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`,
|
||||||
|
);
|
||||||
comment = response.data;
|
comment = response.data;
|
||||||
isPRReviewComment = true;
|
isPRReviewComment = true;
|
||||||
console.log("Successfully fetched as PR review comment");
|
console.log("Successfully fetched as PR review comment");
|
||||||
@@ -46,7 +49,10 @@ async function run() {
|
|||||||
// For all other event types, use the issues API
|
// For all other event types, use the issues API
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
console.log(`Fetching issue comment ${commentId}`);
|
console.log(`Fetching issue comment ${commentId}`);
|
||||||
const response = await client.api.customRequest("GET", `/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`);
|
const response = await client.api.customRequest(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`,
|
||||||
|
);
|
||||||
comment = response.data;
|
comment = response.data;
|
||||||
isPRReviewComment = false;
|
isPRReviewComment = false;
|
||||||
console.log("Successfully fetched as issue comment");
|
console.log("Successfully fetched as issue comment");
|
||||||
@@ -61,7 +67,11 @@ async function run() {
|
|||||||
|
|
||||||
// Try to get the PR info to understand the comment structure
|
// Try to get the PR info to understand the comment structure
|
||||||
try {
|
try {
|
||||||
const pr = await client.api.getPullRequest(owner, repo, context.entityNumber);
|
const pr = await client.api.getPullRequest(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
context.entityNumber,
|
||||||
|
);
|
||||||
console.log(`PR state: ${pr.data.state}`);
|
console.log(`PR state: ${pr.data.state}`);
|
||||||
console.log(`PR comments count: ${pr.data.comments}`);
|
console.log(`PR comments count: ${pr.data.comments}`);
|
||||||
console.log(`PR review comments count: ${pr.data.review_comments}`);
|
console.log(`PR review comments count: ${pr.data.review_comments}`);
|
||||||
@@ -100,10 +110,18 @@ async function run() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the branch info to see if it exists and has commits
|
// Get the branch info to see if it exists and has commits
|
||||||
const branchResponse = await client.api.getBranch(owner, repo, claudeBranch);
|
const branchResponse = await client.api.getBranch(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
claudeBranch,
|
||||||
|
);
|
||||||
|
|
||||||
// Get base branch info for comparison
|
// Get base branch info for comparison
|
||||||
const baseResponse = await client.api.getBranch(owner, repo, baseBranch);
|
const baseResponse = await client.api.getBranch(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
baseBranch,
|
||||||
|
);
|
||||||
|
|
||||||
const branchSha = branchResponse.data.commit.sha;
|
const branchSha = branchResponse.data.commit.sha;
|
||||||
const baseSha = baseResponse.data.commit.sha;
|
const baseSha = baseResponse.data.commit.sha;
|
||||||
@@ -223,11 +241,20 @@ async function run() {
|
|||||||
// Update the comment using the appropriate API
|
// Update the comment using the appropriate API
|
||||||
try {
|
try {
|
||||||
if (isPRReviewComment) {
|
if (isPRReviewComment) {
|
||||||
await client.api.customRequest("PATCH", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, {
|
await client.api.customRequest(
|
||||||
body: updatedBody,
|
"PATCH",
|
||||||
});
|
`/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`,
|
||||||
|
{
|
||||||
|
body: updatedBody,
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await client.api.updateIssueComment(owner, repo, commentId, updatedBody);
|
await client.api.updateIssueComment(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
commentId,
|
||||||
|
updatedBody,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
|
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ export class GiteaApiClient {
|
|||||||
private async request<T = any>(
|
private async request<T = any>(
|
||||||
method: string,
|
method: string,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
body?: any
|
body?: any,
|
||||||
): Promise<GiteaApiResponse<T>> {
|
): Promise<GiteaApiResponse<T>> {
|
||||||
const url = `${this.baseUrl}${endpoint}`;
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": `token ${this.token}`,
|
Authorization: `token ${this.token}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: any = {
|
const options: any = {
|
||||||
@@ -48,10 +48,10 @@ export class GiteaApiClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
|
|
||||||
let responseData: any = null;
|
let responseData: any = null;
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
|
|
||||||
// Only try to parse JSON if the response has JSON content type
|
// Only try to parse JSON if the response has JSON content type
|
||||||
if (contentType && contentType.includes("application/json")) {
|
if (contentType && contentType.includes("application/json")) {
|
||||||
try {
|
try {
|
||||||
@@ -65,11 +65,14 @@ export class GiteaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorMessage = typeof responseData === 'object' && responseData.message
|
const errorMessage =
|
||||||
? responseData.message
|
typeof responseData === "object" && responseData.message
|
||||||
: responseData || response.statusText;
|
? responseData.message
|
||||||
|
: responseData || response.statusText;
|
||||||
const error = new Error(`HTTP ${response.status}: ${errorMessage}`) as GiteaApiError;
|
|
||||||
|
const error = new Error(
|
||||||
|
`HTTP ${response.status}: ${errorMessage}`,
|
||||||
|
) as GiteaApiError;
|
||||||
error.status = response.status;
|
error.status = response.status;
|
||||||
error.response = {
|
error.response = {
|
||||||
data: responseData,
|
data: responseData,
|
||||||
@@ -103,10 +106,18 @@ export class GiteaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getBranch(owner: string, repo: string, branch: string) {
|
async getBranch(owner: string, repo: string, branch: string) {
|
||||||
return this.request("GET", `/api/v1/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`);
|
return this.request(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBranch(owner: string, repo: string, newBranch: string, fromBranch: string) {
|
async createBranch(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
newBranch: string,
|
||||||
|
fromBranch: string,
|
||||||
|
) {
|
||||||
return this.request("POST", `/api/v1/repos/${owner}/${repo}/branches`, {
|
return this.request("POST", `/api/v1/repos/${owner}/${repo}/branches`, {
|
||||||
new_branch_name: newBranch,
|
new_branch_name: newBranch,
|
||||||
old_branch_name: fromBranch,
|
old_branch_name: fromBranch,
|
||||||
@@ -119,46 +130,93 @@ export class GiteaApiClient {
|
|||||||
|
|
||||||
// Issue operations
|
// Issue operations
|
||||||
async getIssue(owner: string, repo: string, issueNumber: number) {
|
async getIssue(owner: string, repo: string, issueNumber: number) {
|
||||||
return this.request("GET", `/api/v1/repos/${owner}/${repo}/issues/${issueNumber}`);
|
return this.request(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/issues/${issueNumber}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listIssueComments(owner: string, repo: string, issueNumber: number) {
|
async listIssueComments(owner: string, repo: string, issueNumber: number) {
|
||||||
return this.request("GET", `/api/v1/repos/${owner}/${repo}/issues/${issueNumber}/comments`);
|
return this.request(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createIssueComment(owner: string, repo: string, issueNumber: number, body: string) {
|
async createIssueComment(
|
||||||
return this.request("POST", `/api/v1/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
owner: string,
|
||||||
body,
|
repo: string,
|
||||||
});
|
issueNumber: number,
|
||||||
|
body: string,
|
||||||
|
) {
|
||||||
|
return this.request(
|
||||||
|
"POST",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
||||||
|
{
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateIssueComment(owner: string, repo: string, commentId: number, body: string) {
|
async updateIssueComment(
|
||||||
return this.request("PATCH", `/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`, {
|
owner: string,
|
||||||
body,
|
repo: string,
|
||||||
});
|
commentId: number,
|
||||||
|
body: string,
|
||||||
|
) {
|
||||||
|
return this.request(
|
||||||
|
"PATCH",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/issues/comments/${commentId}`,
|
||||||
|
{
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull request operations
|
// Pull request operations
|
||||||
async getPullRequest(owner: string, repo: string, prNumber: number) {
|
async getPullRequest(owner: string, repo: string, prNumber: number) {
|
||||||
return this.request("GET", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}`);
|
return this.request(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/pulls/${prNumber}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listPullRequestFiles(owner: string, repo: string, prNumber: number) {
|
async listPullRequestFiles(owner: string, repo: string, prNumber: number) {
|
||||||
return this.request("GET", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/files`);
|
return this.request(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/files`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listPullRequestComments(owner: string, repo: string, prNumber: number) {
|
async listPullRequestComments(owner: string, repo: string, prNumber: number) {
|
||||||
return this.request("GET", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/comments`);
|
return this.request(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/comments`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPullRequestComment(owner: string, repo: string, prNumber: number, body: string) {
|
async createPullRequestComment(
|
||||||
return this.request("POST", `/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/comments`, {
|
owner: string,
|
||||||
body,
|
repo: string,
|
||||||
});
|
prNumber: number,
|
||||||
|
body: string,
|
||||||
|
) {
|
||||||
|
return this.request(
|
||||||
|
"POST",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/pulls/${prNumber}/comments`,
|
||||||
|
{
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// File operations
|
// File operations
|
||||||
async getFileContents(owner: string, repo: string, path: string, ref?: string) {
|
async getFileContents(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
path: string,
|
||||||
|
ref?: string,
|
||||||
|
) {
|
||||||
let endpoint = `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`;
|
let endpoint = `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`;
|
||||||
if (ref) {
|
if (ref) {
|
||||||
endpoint += `?ref=${encodeURIComponent(ref)}`;
|
endpoint += `?ref=${encodeURIComponent(ref)}`;
|
||||||
@@ -167,23 +225,27 @@ export class GiteaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createFile(
|
async createFile(
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
path: string,
|
path: string,
|
||||||
content: string,
|
content: string,
|
||||||
message: string,
|
message: string,
|
||||||
branch?: string
|
branch?: string,
|
||||||
) {
|
) {
|
||||||
const body: any = {
|
const body: any = {
|
||||||
message,
|
message,
|
||||||
content: Buffer.from(content).toString("base64"),
|
content: Buffer.from(content).toString("base64"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (branch) {
|
if (branch) {
|
||||||
body.branch = branch;
|
body.branch = branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.request("POST", `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, body);
|
return this.request(
|
||||||
|
"POST",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFile(
|
async updateFile(
|
||||||
@@ -193,19 +255,23 @@ export class GiteaApiClient {
|
|||||||
content: string,
|
content: string,
|
||||||
message: string,
|
message: string,
|
||||||
sha: string,
|
sha: string,
|
||||||
branch?: string
|
branch?: string,
|
||||||
) {
|
) {
|
||||||
const body: any = {
|
const body: any = {
|
||||||
message,
|
message,
|
||||||
content: Buffer.from(content).toString("base64"),
|
content: Buffer.from(content).toString("base64"),
|
||||||
sha,
|
sha,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (branch) {
|
if (branch) {
|
||||||
body.branch = branch;
|
body.branch = branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.request("PUT", `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, body);
|
return this.request(
|
||||||
|
"PUT",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(
|
async deleteFile(
|
||||||
@@ -214,26 +280,34 @@ export class GiteaApiClient {
|
|||||||
path: string,
|
path: string,
|
||||||
message: string,
|
message: string,
|
||||||
sha: string,
|
sha: string,
|
||||||
branch?: string
|
branch?: string,
|
||||||
) {
|
) {
|
||||||
const body: any = {
|
const body: any = {
|
||||||
message,
|
message,
|
||||||
sha,
|
sha,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (branch) {
|
if (branch) {
|
||||||
body.branch = branch;
|
body.branch = branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.request("DELETE", `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, body);
|
return this.request(
|
||||||
|
"DELETE",
|
||||||
|
`/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`,
|
||||||
|
body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic request method for other operations
|
// Generic request method for other operations
|
||||||
async customRequest<T = any>(method: string, endpoint: string, body?: any): Promise<GiteaApiResponse<T>> {
|
async customRequest<T = any>(
|
||||||
|
method: string,
|
||||||
|
endpoint: string,
|
||||||
|
body?: any,
|
||||||
|
): Promise<GiteaApiResponse<T>> {
|
||||||
return this.request<T>(method, endpoint, body);
|
return this.request<T>(method, endpoint, body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGiteaClient(token: string): GiteaApiClient {
|
export function createGiteaClient(token: string): GiteaApiClient {
|
||||||
return new GiteaApiClient(token);
|
return new GiteaApiClient(token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ export async function fetchGitHubData({
|
|||||||
// Use REST API for all requests (works with both GitHub and Gitea)
|
// Use REST API for all requests (works with both GitHub and Gitea)
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
console.log(`Fetching PR #${prNumber} data using REST API`);
|
console.log(`Fetching PR #${prNumber} data using REST API`);
|
||||||
const prResponse = await client.api.getPullRequest(owner, repo, parseInt(prNumber));
|
const prResponse = await client.api.getPullRequest(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
parseInt(prNumber),
|
||||||
|
);
|
||||||
|
|
||||||
contextData = {
|
contextData = {
|
||||||
title: prResponse.data.title,
|
title: prResponse.data.title,
|
||||||
@@ -74,7 +78,7 @@ export async function fetchGitHubData({
|
|||||||
const commentsResponse = await client.api.listIssueComments(
|
const commentsResponse = await client.api.listIssueComments(
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
parseInt(prNumber)
|
parseInt(prNumber),
|
||||||
);
|
);
|
||||||
comments = commentsResponse.data.map((comment: any) => ({
|
comments = commentsResponse.data.map((comment: any) => ({
|
||||||
id: comment.id.toString(),
|
id: comment.id.toString(),
|
||||||
@@ -93,7 +97,7 @@ export async function fetchGitHubData({
|
|||||||
const filesResponse = await client.api.listPullRequestFiles(
|
const filesResponse = await client.api.listPullRequestFiles(
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
parseInt(prNumber)
|
parseInt(prNumber),
|
||||||
);
|
);
|
||||||
changedFiles = filesResponse.data.map((file: any) => ({
|
changedFiles = filesResponse.data.map((file: any) => ({
|
||||||
path: file.filename,
|
path: file.filename,
|
||||||
@@ -109,7 +113,11 @@ export async function fetchGitHubData({
|
|||||||
reviewData = { nodes: [] }; // Simplified for Gitea
|
reviewData = { nodes: [] }; // Simplified for Gitea
|
||||||
} else {
|
} else {
|
||||||
console.log(`Fetching issue #${prNumber} data using REST API`);
|
console.log(`Fetching issue #${prNumber} data using REST API`);
|
||||||
const issueResponse = await client.api.getIssue(owner, repo, parseInt(prNumber));
|
const issueResponse = await client.api.getIssue(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
parseInt(prNumber),
|
||||||
|
);
|
||||||
|
|
||||||
contextData = {
|
contextData = {
|
||||||
title: issueResponse.data.title,
|
title: issueResponse.data.title,
|
||||||
@@ -125,7 +133,7 @@ export async function fetchGitHubData({
|
|||||||
const commentsResponse = await client.api.listIssueComments(
|
const commentsResponse = await client.api.listIssueComments(
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
parseInt(prNumber)
|
parseInt(prNumber),
|
||||||
);
|
);
|
||||||
comments = commentsResponse.data.map((comment: any) => ({
|
comments = commentsResponse.data.map((comment: any) => ({
|
||||||
id: comment.id.toString(),
|
id: comment.id.toString(),
|
||||||
@@ -144,7 +152,6 @@ export async function fetchGitHubData({
|
|||||||
throw new Error(`Failed to fetch ${isPR ? "PR" : "issue"} data`);
|
throw new Error(`Failed to fetch ${isPR ? "PR" : "issue"} data`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Compute SHAs for changed files
|
// Compute SHAs for changed files
|
||||||
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
||||||
if (isPR && changedFiles.length > 0) {
|
if (isPR && changedFiles.length > 0) {
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ export async function checkAndDeleteEmptyBranch(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the branch info to see if it exists and has commits
|
// Get the branch info to see if it exists and has commits
|
||||||
const branchResponse = await client.api.getBranch(owner, repo, claudeBranch);
|
const branchResponse = await client.api.getBranch(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
claudeBranch,
|
||||||
|
);
|
||||||
|
|
||||||
// Get base branch info for comparison
|
// Get base branch info for comparison
|
||||||
const baseResponse = await client.api.getBranch(owner, repo, baseBranch);
|
const baseResponse = await client.api.getBranch(owner, repo, baseBranch);
|
||||||
|
|||||||
@@ -90,29 +90,37 @@ export async function setupBranch(
|
|||||||
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the SHA of the source branch using Gitea's branches endpoint
|
// Use local git operations instead of API since Gitea's API is unreliable
|
||||||
console.log(`Getting branch info for: ${sourceBranch}`);
|
console.log(
|
||||||
|
`Setting up local git branch: ${newBranch} from: ${sourceBranch}`,
|
||||||
try {
|
);
|
||||||
const branchResponse = await client.api.getBranch(owner, repo, sourceBranch);
|
|
||||||
const currentSHA = branchResponse.data.commit.sha;
|
|
||||||
console.log(`Current SHA: ${currentSHA}`);
|
|
||||||
} catch (branchError: any) {
|
|
||||||
console.log(`Failed to get branch info: ${branchError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create branch using Gitea's branch creation API
|
// Ensure we're in the repository directory
|
||||||
console.log(`Creating branch: ${newBranch} from: ${sourceBranch}`);
|
const repoDir = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||||
|
console.log(`Working in directory: ${repoDir}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.api.createBranch(owner, repo, newBranch, sourceBranch);
|
// Ensure we have the latest version of the source branch
|
||||||
console.log(`Successfully created branch via Gitea API: ${newBranch}`);
|
console.log(`Fetching latest ${sourceBranch}...`);
|
||||||
} catch (createBranchError: any) {
|
await $`git fetch origin ${sourceBranch}`;
|
||||||
console.log(`Branch creation failed: ${createBranchError.message}`);
|
|
||||||
console.log(`Error status: ${createBranchError.status}`);
|
// Checkout the source branch
|
||||||
|
console.log(`Checking out ${sourceBranch}...`);
|
||||||
|
await $`git checkout ${sourceBranch}`;
|
||||||
|
|
||||||
|
// Pull latest changes
|
||||||
|
await $`git pull origin ${sourceBranch}`;
|
||||||
|
|
||||||
|
// Create and checkout the new branch
|
||||||
|
console.log(`Creating new branch: ${newBranch}`);
|
||||||
|
await $`git checkout -b ${newBranch}`;
|
||||||
|
|
||||||
|
console.log(`Successfully created and checked out branch: ${newBranch}`);
|
||||||
|
} catch (gitError: any) {
|
||||||
console.log(
|
console.log(
|
||||||
`Branch ${newBranch} will be created when files are pushed via MCP server`,
|
`Local git operations completed. Branch ${newBranch} ready for use.`,
|
||||||
);
|
);
|
||||||
|
// Don't fail here - the branch will be created when files are committed
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Branch setup completed for: ${newBranch}`);
|
console.log(`Branch setup completed for: ${newBranch}`);
|
||||||
@@ -126,7 +134,7 @@ export async function setupBranch(
|
|||||||
currentBranch: newBranch,
|
currentBranch: newBranch,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating branch:", error);
|
console.error("Error setting up branch:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,19 +25,30 @@ export async function createInitialComment(
|
|||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
console.log(`Creating comment for ${context.isPR ? 'PR' : 'issue'} #${context.entityNumber}`);
|
console.log(
|
||||||
|
`Creating comment for ${context.isPR ? "PR" : "issue"} #${context.entityNumber}`,
|
||||||
|
);
|
||||||
console.log(`Repository: ${owner}/${repo}`);
|
console.log(`Repository: ${owner}/${repo}`);
|
||||||
|
|
||||||
// Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
|
// Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
|
||||||
if (isPullRequestReviewCommentEvent(context)) {
|
if (isPullRequestReviewCommentEvent(context)) {
|
||||||
console.log(`Creating PR review comment reply`);
|
console.log(`Creating PR review comment reply`);
|
||||||
response = await api.customRequest("POST", `/api/v1/repos/${owner}/${repo}/pulls/${context.entityNumber}/comments/${context.payload.comment.id}/replies`, {
|
response = await api.customRequest(
|
||||||
body: initialBody,
|
"POST",
|
||||||
});
|
`/api/v1/repos/${owner}/${repo}/pulls/${context.entityNumber}/comments/${context.payload.comment.id}/replies`,
|
||||||
|
{
|
||||||
|
body: initialBody,
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// For all other cases (issues, issue comments, or missing comment_id)
|
// For all other cases (issues, issue comments, or missing comment_id)
|
||||||
console.log(`Creating issue comment via API`);
|
console.log(`Creating issue comment via API`);
|
||||||
response = await api.createIssueComment(owner, repo, context.entityNumber, initialBody);
|
response = await api.createIssueComment(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
context.entityNumber,
|
||||||
|
initialBody,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the comment ID for downstream steps using GITHUB_OUTPUT
|
// Output the comment ID for downstream steps using GITHUB_OUTPUT
|
||||||
@@ -50,7 +61,12 @@ export async function createInitialComment(
|
|||||||
|
|
||||||
// Always fall back to regular issue comment if anything fails
|
// Always fall back to regular issue comment if anything fails
|
||||||
try {
|
try {
|
||||||
const response = await api.createIssueComment(owner, repo, context.entityNumber, initialBody);
|
const response = await api.createIssueComment(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
context.entityNumber,
|
||||||
|
initialBody,
|
||||||
|
);
|
||||||
|
|
||||||
const githubOutput = process.env.GITHUB_OUTPUT!;
|
const githubOutput = process.env.GITHUB_OUTPUT!;
|
||||||
appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`);
|
appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`);
|
||||||
|
|||||||
@@ -38,9 +38,13 @@ export async function updateTrackingComment(
|
|||||||
try {
|
try {
|
||||||
if (isPullRequestReviewCommentEvent(context)) {
|
if (isPullRequestReviewCommentEvent(context)) {
|
||||||
// For PR review comments (inline comments), use the pulls API
|
// For PR review comments (inline comments), use the pulls API
|
||||||
await client.api.customRequest("PATCH", `/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`, {
|
await client.api.customRequest(
|
||||||
body: updatedBody,
|
"PATCH",
|
||||||
});
|
`/api/v1/repos/${owner}/${repo}/pulls/comments/${commentId}`,
|
||||||
|
{
|
||||||
|
body: updatedBody,
|
||||||
|
},
|
||||||
|
);
|
||||||
console.log(`✅ Updated PR review comment ${commentId} with branch link`);
|
console.log(`✅ Updated PR review comment ${commentId} with branch link`);
|
||||||
} else {
|
} else {
|
||||||
// For all other comments, use the issues API
|
// For all other comments, use the issues API
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export async function downloadCommentImages(
|
|||||||
): Promise<Map<string, string>> {
|
): Promise<Map<string, string>> {
|
||||||
// Temporarily simplified - return empty map to avoid Octokit dependencies
|
// Temporarily simplified - return empty map to avoid Octokit dependencies
|
||||||
// TODO: Implement image downloading with direct Gitea API calls if needed
|
// TODO: Implement image downloading with direct Gitea API calls if needed
|
||||||
console.log("Image downloading temporarily disabled during Octokit migration");
|
console.log(
|
||||||
|
"Image downloading temporarily disabled during Octokit migration",
|
||||||
|
);
|
||||||
return new Map<string, string>();
|
return new Map<string, string>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ export async function checkHumanActor(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch user information from GitHub API
|
// Fetch user information from GitHub API
|
||||||
const response = await api.customRequest("GET", `/api/v1/users/${githubContext.actor}`);
|
const response = await api.customRequest(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/users/${githubContext.actor}`,
|
||||||
|
);
|
||||||
const userData = response.data;
|
const userData = response.data;
|
||||||
|
|
||||||
const actorType = userData.type;
|
const actorType = userData.type;
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ export async function checkWritePermissions(
|
|||||||
core.info(`Checking permissions for actor: ${actor}`);
|
core.info(`Checking permissions for actor: ${actor}`);
|
||||||
|
|
||||||
// Check permissions directly using the permission endpoint
|
// Check permissions directly using the permission endpoint
|
||||||
const response = await api.customRequest("GET", `/api/v1/repos/${repository.owner}/${repository.repo}/collaborators/${actor}/permission`);
|
const response = await api.customRequest(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/repos/${repository.owner}/${repository.repo}/collaborators/${actor}/permission`,
|
||||||
|
);
|
||||||
|
|
||||||
const permissionLevel = response.data.permission;
|
const permissionLevel = response.data.permission;
|
||||||
core.info(`Permission level retrieved: ${permissionLevel}`);
|
core.info(`Permission level retrieved: ${permissionLevel}`);
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ server.tool(
|
|||||||
// For now, throw an error indicating this functionality is not available.
|
// For now, throw an error indicating this functionality is not available.
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Multi-file commits are not supported with Gitea. " +
|
"Multi-file commits are not supported with Gitea. " +
|
||||||
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
||||||
"that are required for atomic multi-file commits. " +
|
"that are required for atomic multi-file commits. " +
|
||||||
"Please commit files individually using the contents API."
|
"Please commit files individually using the contents API.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -158,9 +158,9 @@ server.tool(
|
|||||||
// For now, throw an error indicating this functionality is not available.
|
// For now, throw an error indicating this functionality is not available.
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Multi-file deletions are not supported with Gitea. " +
|
"Multi-file deletions are not supported with Gitea. " +
|
||||||
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
||||||
"that are required for atomic multi-file operations. " +
|
"that are required for atomic multi-file operations. " +
|
||||||
"Please delete files individually using the contents API."
|
"Please delete files individually using the contents API.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export async function prepareMcpConfig(
|
|||||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
github_file_ops: {
|
local_git_ops: {
|
||||||
command: "bun",
|
command: "bun",
|
||||||
args: [
|
args: [
|
||||||
"run",
|
"run",
|
||||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/github-file-ops-server.ts`,
|
`${process.env.GITHUB_ACTION_PATH}/src/mcp/local-git-ops-server.ts`,
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
GITHUB_TOKEN: githubToken,
|
GITHUB_TOKEN: githubToken,
|
||||||
@@ -35,6 +35,8 @@ export async function prepareMcpConfig(
|
|||||||
REPO_NAME: repo,
|
REPO_NAME: repo,
|
||||||
BRANCH_NAME: branch,
|
BRANCH_NAME: branch,
|
||||||
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
||||||
|
GITEA_API_URL:
|
||||||
|
process.env.GITEA_API_URL || "https://api.github.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
298
src/mcp/local-git-ops-server.ts
Normal file
298
src/mcp/local-git-ops-server.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Local Git 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, writeFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
||||||
|
const GITEA_API_URL = process.env.GITEA_API_URL || "https://api.github.com";
|
||||||
|
|
||||||
|
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: "Local Git Operations Server",
|
||||||
|
version: "0.0.1",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to run git commands
|
||||||
|
function runGitCommand(command: string): string {
|
||||||
|
try {
|
||||||
|
console.log(`Running git command: ${command}`);
|
||||||
|
const result = execSync(command, {
|
||||||
|
cwd: REPO_DIR,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["inherit", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
console.log(`Git command result: ${result.trim()}`);
|
||||||
|
return result.trim();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Git command failed: ${command}`);
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
if (error.stdout) console.error(`Stdout: ${error.stdout}`);
|
||||||
|
if (error.stderr) console.error(`Stderr: ${error.stderr}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create branch tool
|
||||||
|
server.tool(
|
||||||
|
"create_branch",
|
||||||
|
"Create a new branch from a base branch using local git operations",
|
||||||
|
{
|
||||||
|
branch_name: z.string().describe("Name of the branch to create"),
|
||||||
|
base_branch: z
|
||||||
|
.string()
|
||||||
|
.describe("Base branch to create from (e.g., 'main')"),
|
||||||
|
},
|
||||||
|
async ({ branch_name, base_branch }) => {
|
||||||
|
try {
|
||||||
|
// Ensure we're on the base branch and it's up to date
|
||||||
|
runGitCommand(`git checkout ${base_branch}`);
|
||||||
|
runGitCommand(`git pull origin ${base_branch}`);
|
||||||
|
|
||||||
|
// Create and checkout the new branch
|
||||||
|
runGitCommand(`git checkout -b ${branch_name}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully created and checked out branch: ${branch_name}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error creating branch: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Commit files tool
|
||||||
|
server.tool(
|
||||||
|
"commit_files",
|
||||||
|
"Commit one or more files to the current branch using local git operations",
|
||||||
|
{
|
||||||
|
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 }) => {
|
||||||
|
try {
|
||||||
|
// Add the specified files
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = file.startsWith("/") ? file.slice(1) : file;
|
||||||
|
runGitCommand(`git add "${filePath}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the changes
|
||||||
|
runGitCommand(`git commit -m "${message}"`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully committed ${files.length} file(s): ${files.join(", ")}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error committing files: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Push branch tool
|
||||||
|
server.tool(
|
||||||
|
"push_branch",
|
||||||
|
"Push the current branch to remote origin",
|
||||||
|
{
|
||||||
|
force: z.boolean().optional().describe("Force push (use with caution)"),
|
||||||
|
},
|
||||||
|
async ({ force = false }) => {
|
||||||
|
try {
|
||||||
|
// Get current branch name
|
||||||
|
const currentBranch = runGitCommand("git rev-parse --abbrev-ref HEAD");
|
||||||
|
|
||||||
|
// Push the branch
|
||||||
|
const pushCommand = force
|
||||||
|
? `git push -f origin ${currentBranch}`
|
||||||
|
: `git push origin ${currentBranch}`;
|
||||||
|
|
||||||
|
runGitCommand(pushCommand);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully pushed branch: ${currentBranch}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error pushing branch: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create pull request tool (uses Gitea API)
|
||||||
|
server.tool(
|
||||||
|
"create_pull_request",
|
||||||
|
"Create a pull request using Gitea API",
|
||||||
|
{
|
||||||
|
title: z.string().describe("Pull request title"),
|
||||||
|
body: z.string().describe("Pull request body/description"),
|
||||||
|
base_branch: z.string().describe("Base branch (e.g., 'main')"),
|
||||||
|
head_branch: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Head branch (defaults to current branch)"),
|
||||||
|
},
|
||||||
|
async ({ title, body, base_branch, head_branch }) => {
|
||||||
|
try {
|
||||||
|
if (!GITHUB_TOKEN) {
|
||||||
|
throw new Error(
|
||||||
|
"GITHUB_TOKEN environment variable is required for PR creation",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current branch if head_branch not specified
|
||||||
|
const currentBranch =
|
||||||
|
head_branch || runGitCommand("git rev-parse --abbrev-ref HEAD");
|
||||||
|
|
||||||
|
// Create PR using Gitea API
|
||||||
|
const response = await fetch(
|
||||||
|
`${GITEA_API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/pulls`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${GITHUB_TOKEN}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
base: base_branch,
|
||||||
|
head: currentBranch,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Failed to create PR: ${response.status} ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prData = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully created pull request #${prData.number}: ${prData.html_url}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error creating pull request: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get git status tool
|
||||||
|
server.tool("git_status", "Get the current git status", {}, async () => {
|
||||||
|
try {
|
||||||
|
const status = runGitCommand("git status --porcelain");
|
||||||
|
const currentBranch = runGitCommand("git rev-parse --abbrev-ref HEAD");
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Current branch: ${currentBranch}\nStatus:\n${status || "Working tree clean"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error getting git status: ${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);
|
||||||
Reference in New Issue
Block a user