Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/docker-publish-fork.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Docker Build and Publish Fork

on:
workflow_dispatch:
# Manual trigger only

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=raw,value=latest

- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
build-args: |
VERSION=${{ github.ref_name }}
70 changes: 70 additions & 0 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,76 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
}
}

// UpdateIssueComment creates a tool to update a comment on an issue.
func UpdateIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("update_issue_comment",
mcp.WithDescription(t("TOOL_UPDATE_ISSUE_COMMENT_DESCRIPTION", "Update a comment on an issue")),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithNumber("commentId",
mcp.Required(),
mcp.Description("Comment ID to update"),
),
mcp.WithString("body",
mcp.Required(),
mcp.Description("The new text for the comment"),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := requiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
repo, err := requiredParam[string](request, "repo")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
commentID, err := RequiredInt(request, "commentId")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
body, err := requiredParam[string](request, "body")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

comment := &github.IssueComment{
Body: github.Ptr(body),
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
updatedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, int64(commentID), comment)
if err != nil {
return nil, fmt.Errorf("failed to update issue comment: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to update issue comment: %s", string(body))), nil
}

r, err := json.Marshal(updatedComment)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}

// SearchIssues creates a tool to search for issues and pull requests.
func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("search_issues",
Expand Down
136 changes: 136 additions & 0 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1130,3 +1130,139 @@ func Test_GetIssueComments(t *testing.T) {
})
}
}

func Test_UpdateIssueComment(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
tool, _ := UpdateIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)

assert.Equal(t, "update_issue_comment", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "commentId")
assert.Contains(t, tool.InputSchema.Properties, "body")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "commentId", "body"})

// Setup mock comment for success case
mockUpdatedComment := &github.IssueComment{
ID: github.Ptr(int64(123)),
Body: github.Ptr("Updated issue comment text here"),
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/1#issuecomment-123"),
CreatedAt: &github.Timestamp{Time: time.Now().Add(-1 * time.Hour)},
UpdatedAt: &github.Timestamp{Time: time.Now()},
User: &github.User{
Login: github.Ptr("testuser"),
},
}

tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]interface{}
expectError bool
expectedComment *github.IssueComment
expectedErrMsg string
}{
{
name: "successful comment update",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
expectRequestBody(t, map[string]interface{}{
"body": "Updated issue comment text here",
}).andThen(
mockResponse(t, http.StatusOK, mockUpdatedComment),
),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"commentId": float64(123),
"body": "Updated issue comment text here",
},
expectError: false,
expectedComment: mockUpdatedComment,
},
{
name: "comment update fails - not found",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"message": "Comment not found"}`))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"commentId": float64(999),
"body": "This should fail",
},
expectError: true,
expectedErrMsg: "failed to update issue comment",
},
{
name: "comment update fails - validation error",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"commentId": float64(123),
"body": "Invalid body",
},
expectError: true,
expectedErrMsg: "failed to update issue comment",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
_, handler := UpdateIssueComment(stubGetClientFn(client), translations.NullTranslationHelper)

request := createMCPRequest(tc.requestArgs)

result, err := handler(context.Background(), request)

if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErrMsg)
return
}

require.NoError(t, err)
assert.NotNil(t, result)
require.Len(t, result.Content, 1)

textContent := getTextResult(t, result)

// For non-error cases, check the returned comment
var returnedComment github.IssueComment
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
require.NoError(t, err)

assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
if tc.expectedComment.HTMLURL != nil {
assert.Equal(t, *tc.expectedComment.HTMLURL, *returnedComment.HTMLURL)
}
if tc.expectedComment.User != nil && tc.expectedComment.User.Login != nil {
assert.Equal(t, *tc.expectedComment.User.Login, *returnedComment.User.Login)
}
})
}
}
70 changes: 70 additions & 0 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,76 @@ func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHe
}
}

// UpdatePullRequestComment creates a tool to update a review comment on a pull request.
func UpdatePullRequestComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("update_pull_request_comment",
mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_COMMENT_DESCRIPTION", "Update a review comment on a pull request")),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
mcp.WithNumber("commentId",
mcp.Required(),
mcp.Description("Comment ID to update"),
),
mcp.WithString("body",
mcp.Required(),
mcp.Description("The new text for the comment"),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := requiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
repo, err := requiredParam[string](request, "repo")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
commentID, err := RequiredInt(request, "commentId")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
body, err := requiredParam[string](request, "body")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

comment := &github.PullRequestComment{
Body: github.Ptr(body),
}

client, err := getClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
updatedComment, resp, err := client.PullRequests.EditComment(ctx, owner, repo, int64(commentID), comment)
if err != nil {
return nil, fmt.Errorf("failed to update pull request comment: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request comment: %s", string(body))), nil
}

r, err := json.Marshal(updatedComment)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}

// CreatePullRequest creates a tool to create a new pull request.
func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("create_pull_request",
Expand Down
Loading
Loading