From eb3cfd77b05cb64a4041b0d7858977c0bc672095 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:16:14 +0000 Subject: [PATCH 1/4] Initial plan From 0756b1160cc1ebd5008a2feae8b1a3d1c4b858da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:28:32 +0000 Subject: [PATCH 2/4] Add auto-assign comments functionality for issues and PRs Co-authored-by: ybettan <29724509+ybettan@users.noreply.github.com> --- internal/github/data.go | 10 ++ internal/github/issue.go | 13 +++ internal/github/issue_test.go | 68 ++++++++++++ internal/github/mock_issue.go | 14 +++ internal/github/templates.go | 10 ++ .../github/templates/assignment_comment.tmpl | 31 ++++++ internal/github/templates_test.go | 101 ++++++++++++++++++ internal/gitstream/assign.go | 54 +++++++++- internal/gitstream/assign_test.go | 5 + 9 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 internal/github/templates/assignment_comment.tmpl create mode 100644 internal/github/templates_test.go diff --git a/internal/github/data.go b/internal/github/data.go index 0d21290..67deb82 100644 --- a/internal/github/data.go +++ b/internal/github/data.go @@ -34,3 +34,13 @@ func (is *IssueData) ProcessError() *process.Error { } type PRData BaseData + +type AssignmentCommentData struct { + AppName string + CommitSHAs []string + CommitAuthors []string + ApproverCommitAuthors []string + AssignedUsers []string + AssignmentReason string + IsRandomAssignment bool +} diff --git a/internal/github/issue.go b/internal/github/issue.go index e37a76e..eb9e7c4 100644 --- a/internal/github/issue.go +++ b/internal/github/issue.go @@ -16,6 +16,7 @@ type IssueHelper interface { Create(ctx context.Context, err error, upstreamURL string, commit *object.Commit) (*github.Issue, error) ListAllOpen(ctx context.Context, includePRs bool) ([]*github.Issue, error) Assign(ctx context.Context, issue *github.Issue, usersLogin ...string) error + Comment(ctx context.Context, issue *github.Issue, comment string) error } type IssueHelperImpl struct { @@ -112,3 +113,15 @@ func (ih *IssueHelperImpl) Assign(ctx context.Context, issue *github.Issue, user return nil } + +func (ih *IssueHelperImpl) Comment(ctx context.Context, issue *github.Issue, comment string) error { + commentRequest := &github.IssueComment{ + Body: github.String(comment), + } + + if _, _, err := ih.gc.Issues.CreateComment(ctx, ih.repoName.Owner, ih.repoName.Repo, *issue.Number, commentRequest); err != nil { + return fmt.Errorf("failed to create comment: %v", err) + } + + return nil +} diff --git a/internal/github/issue_test.go b/internal/github/issue_test.go index 1671d1e..abbf47b 100644 --- a/internal/github/issue_test.go +++ b/internal/github/issue_test.go @@ -222,3 +222,71 @@ func TestIssueHelper_Assign(t *testing.T) { assert.NoError(t, err) }) } + +func TestIssueHelper_Comment(t *testing.T) { + + const ( + repoOwner = "owner" + repoName = "repo" + ) + + var ( + issueNumber = 123 + commentBody = "This is a test comment" + + issue = &github.Issue{ + Number: &issueNumber, + } + + ghRepoName = &gh.RepoName{ + Owner: repoOwner, + Repo: repoName, + } + ) + + t.Run("API error", func(t *testing.T) { + c := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }), + ), + ) + + gc := github.NewClient(c) + + err := gh.NewIssueHelper(gc, "Markup", ghRepoName).Comment(context.Background(), issue, commentBody) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create comment") + }) + + t.Run("working as expected", func(t *testing.T) { + c := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + m := make(map[string]interface{}) + assert.NoError( + t, + json.NewDecoder(r.Body).Decode(&m), + ) + assert.Equal(t, commentBody, m["body"]) + w.WriteHeader(http.StatusCreated) + responseBody := commentBody + assert.NoError( + t, + json.NewEncoder(w).Encode(&github.IssueComment{Body: &responseBody}), + ) + }), + ), + ) + + gc := github.NewClient(c) + + err := gh.NewIssueHelper(gc, "Markup", ghRepoName).Comment(context.Background(), issue, commentBody) + + assert.NoError(t, err) + }) +} diff --git a/internal/github/mock_issue.go b/internal/github/mock_issue.go index a6d60db..5939691 100644 --- a/internal/github/mock_issue.go +++ b/internal/github/mock_issue.go @@ -55,6 +55,20 @@ func (mr *MockIssueHelperMockRecorder) Assign(ctx, issue interface{}, usersLogin return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Assign", reflect.TypeOf((*MockIssueHelper)(nil).Assign), varargs...) } +// Comment mocks base method. +func (m *MockIssueHelper) Comment(ctx context.Context, issue *github.Issue, comment string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Comment", ctx, issue, comment) + ret0, _ := ret[0].(error) + return ret0 +} + +// Comment indicates an expected call of Comment. +func (mr *MockIssueHelperMockRecorder) Comment(ctx, issue, comment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Comment", reflect.TypeOf((*MockIssueHelper)(nil).Comment), ctx, issue, comment) +} + // Create mocks base method. func (m *MockIssueHelper) Create(ctx context.Context, err error, upstreamURL string, commit *object.Commit) (*github.Issue, error) { m.ctrl.T.Helper() diff --git a/internal/github/templates.go b/internal/github/templates.go index b6b3791..f1abeb6 100644 --- a/internal/github/templates.go +++ b/internal/github/templates.go @@ -1,7 +1,9 @@ package github import ( + "bytes" "embed" + "fmt" "text/template" ) @@ -13,3 +15,11 @@ var ( template.ParseFS(tmplFS, "templates/*.tmpl"), ) ) + +// ExecuteAssignmentCommentTemplate executes the assignment comment template with the provided data +func ExecuteAssignmentCommentTemplate(buf *bytes.Buffer, data *AssignmentCommentData) error { + if err := templates.ExecuteTemplate(buf, "assignment_comment.tmpl", data); err != nil { + return fmt.Errorf("could not execute assignment comment template: %v", err) + } + return nil +} diff --git a/internal/github/templates/assignment_comment.tmpl b/internal/github/templates/assignment_comment.tmpl new file mode 100644 index 0000000..920ffcc --- /dev/null +++ b/internal/github/templates/assignment_comment.tmpl @@ -0,0 +1,31 @@ +{{- /*gotype: github.com/rh-ecosystem-edge/gitstream/internal/github.AssignmentCommentData*/ -}} +🤖 **{{ .AppName }} Assignment Explanation** + +{{- if .IsRandomAssignment }} + +I randomly assigned this {{ if .CommitSHAs }}issue{{ else }}item{{ end }} to **{{ range $i, $user := .AssignedUsers }}{{ if $i }}, {{ end }}@{{ $user }}{{ end }}** from the list of approvers. + +**Reason**: {{ .AssignmentReason }} + +{{- if .CommitSHAs }} +**Referenced commits**: {{ range $i, $sha := .CommitSHAs }}{{ if $i }}, {{ end }}`{{ $sha }}`{{ end }} +{{- if .CommitAuthors }} +**Commit authors found**: {{ range $i, $author := .CommitAuthors }}{{ if $i }}, {{ end }}@{{ $author }}{{ end }} +{{- end }} +{{- end }} + +{{- else }} + +I assigned this {{ if .CommitSHAs }}issue{{ else }}item{{ end }} to **{{ range $i, $user := .AssignedUsers }}{{ if $i }}, {{ end }}@{{ $user }}{{ end }}** because {{ .AssignmentReason }} + +{{- if .CommitSHAs }} +**Referenced commits**: {{ range $i, $sha := .CommitSHAs }}{{ if $i }}, {{ end }}`{{ $sha }}`{{ end }} +{{- if .CommitAuthors }} +**All commit authors**: {{ range $i, $author := .CommitAuthors }}{{ if $i }}, {{ end }}@{{ $author }}{{ end }} +{{- end }} +{{- if .ApproverCommitAuthors }} +**Authors who are approvers**: {{ range $i, $author := .ApproverCommitAuthors }}{{ if $i }}, {{ end }}@{{ $author }}{{ end }} +{{- end }} +{{- end }} + +{{- end }} \ No newline at end of file diff --git a/internal/github/templates_test.go b/internal/github/templates_test.go new file mode 100644 index 0000000..4e58b79 --- /dev/null +++ b/internal/github/templates_test.go @@ -0,0 +1,101 @@ +package github + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExecuteAssignmentCommentTemplate(t *testing.T) { + + t.Run("random assignment", func(t *testing.T) { + data := &AssignmentCommentData{ + AppName: "gitstream", + CommitSHAs: []string{"abc123", "def456"}, + CommitAuthors: []string{"user1", "user2"}, + ApproverCommitAuthors: []string{}, + AssignedUsers: []string{"approver1"}, + AssignmentReason: "none of the commit authors are approvers in the OWNERS file.", + IsRandomAssignment: true, + } + + var buf bytes.Buffer + err := ExecuteAssignmentCommentTemplate(&buf, data) + + assert.NoError(t, err) + result := buf.String() + assert.Contains(t, result, "🤖 **gitstream Assignment Explanation**") + assert.Contains(t, result, "I randomly assigned this issue to **@approver1**") + assert.Contains(t, result, "**Reason**: none of the commit authors are approvers in the OWNERS file.") + assert.Contains(t, result, "**Referenced commits**: `abc123`, `def456`") + assert.Contains(t, result, "**Commit authors found**: @user1, @user2") + }) + + t.Run("approver assignment", func(t *testing.T) { + data := &AssignmentCommentData{ + AppName: "gitstream", + CommitSHAs: []string{"abc123"}, + CommitAuthors: []string{"user1", "approver1"}, + ApproverCommitAuthors: []string{"approver1"}, + AssignedUsers: []string{"approver1"}, + AssignmentReason: "they are the author of a referenced commit and an approver.", + IsRandomAssignment: false, + } + + var buf bytes.Buffer + err := ExecuteAssignmentCommentTemplate(&buf, data) + + assert.NoError(t, err) + result := buf.String() + assert.Contains(t, result, "🤖 **gitstream Assignment Explanation**") + assert.Contains(t, result, "I assigned this issue to **@approver1** because they are the author of a referenced commit and an approver.") + assert.Contains(t, result, "**Referenced commits**: `abc123`") + assert.Contains(t, result, "**All commit authors**: @user1, @approver1") + assert.Contains(t, result, "**Authors who are approvers**: @approver1") + assert.NotContains(t, result, "randomly") + }) + + t.Run("multiple approvers assignment", func(t *testing.T) { + data := &AssignmentCommentData{ + AppName: "gitstream", + CommitSHAs: []string{"abc123", "def456"}, + CommitAuthors: []string{"approver1", "approver2"}, + ApproverCommitAuthors: []string{"approver1", "approver2"}, + AssignedUsers: []string{"approver1", "approver2"}, + AssignmentReason: "they are authors of referenced commits and approvers.", + IsRandomAssignment: false, + } + + var buf bytes.Buffer + err := ExecuteAssignmentCommentTemplate(&buf, data) + + assert.NoError(t, err) + result := buf.String() + assert.Contains(t, result, "I assigned this issue to **@approver1, @approver2** because they are authors of referenced commits and approvers.") + assert.Contains(t, result, "**Referenced commits**: `abc123`, `def456`") + assert.Contains(t, result, "**All commit authors**: @approver1, @approver2") + assert.Contains(t, result, "**Authors who are approvers**: @approver1, @approver2") + }) + + t.Run("no commit SHAs", func(t *testing.T) { + data := &AssignmentCommentData{ + AppName: "gitstream", + CommitSHAs: []string{}, + CommitAuthors: []string{}, + ApproverCommitAuthors: []string{}, + AssignedUsers: []string{"approver1"}, + AssignmentReason: "they were randomly selected.", + IsRandomAssignment: true, + } + + var buf bytes.Buffer + err := ExecuteAssignmentCommentTemplate(&buf, data) + + assert.NoError(t, err) + result := buf.String() + assert.Contains(t, result, "I randomly assigned this item to **@approver1**") + assert.NotContains(t, result, "Referenced commits") + assert.NotContains(t, result, "Commit authors") + }) +} \ No newline at end of file diff --git a/internal/gitstream/assign.go b/internal/gitstream/assign.go index c6a26b6..5ee3d69 100644 --- a/internal/gitstream/assign.go +++ b/internal/gitstream/assign.go @@ -1,6 +1,7 @@ package gitstream import ( + "bytes" "context" "fmt" "path" @@ -77,6 +78,12 @@ func (a *Assign) handleIssue(ctx context.Context, issue *github.Issue, owners *o return fmt.Errorf("error while looking for SHAs in %q: %v", *issue.Body, err) } + // Convert SHAs to strings for the comment + shaStrings := make([]string, len(shas)) + for i, s := range shas { + shaStrings[i] = s.String() + } + commitAuthors := make([]string, 0, len(shas)) for _, s := range shas { user, err := a.UserHelper.GetCommitAuthor(ctx, s.String()) @@ -87,7 +94,11 @@ func (a *Assign) handleIssue(ctx context.Context, issue *github.Issue, owners *o commitAuthors = append(commitAuthors, *user.Login) } - assignees := a.filterApproversFromCommitAuthors(commitAuthors, owners) + approverCommitAuthors := a.filterApproversFromCommitAuthors(commitAuthors, owners) + assignees := approverCommitAuthors + + var isRandomAssignment bool + var assignmentReason string if len(assignees) == 0 { logger.Info("None of the commit authors are approvers, picking a random approver") @@ -96,12 +107,39 @@ func (a *Assign) handleIssue(ctx context.Context, issue *github.Issue, owners *o return fmt.Errorf("could not get a random approver: %v", err) } assignees = append(assignees, randAssignee) + isRandomAssignment = true + assignmentReason = "none of the commit authors are approvers in the OWNERS file." + } else { + isRandomAssignment = false + if len(approverCommitAuthors) == 1 { + assignmentReason = "they are the author of a referenced commit and an approver." + } else { + assignmentReason = "they are authors of referenced commits and approvers." + } } if err := a.IssueHelper.Assign(ctx, issue, assignees...); err != nil { return fmt.Errorf("could not assign issue %d to %s: %v", *issue.Number, assignees, err) } + // Create assignment comment if not in dry run mode + if !a.DryRun { + commentData := gh.AssignmentCommentData{ + AppName: internal.AppName, + CommitSHAs: shaStrings, + CommitAuthors: commitAuthors, + ApproverCommitAuthors: approverCommitAuthors, + AssignedUsers: assignees, + AssignmentReason: assignmentReason, + IsRandomAssignment: isRandomAssignment, + } + + if err := a.createAssignmentComment(ctx, issue, &commentData); err != nil { + // Log the error but don't fail the assignment + logger.Error(err, "Failed to create assignment comment") + } + } + return nil } @@ -127,3 +165,17 @@ func (a *Assign) assignIssues(ctx context.Context) error { return multiErr } + +func (a *Assign) createAssignmentComment(ctx context.Context, issue *github.Issue, data *gh.AssignmentCommentData) error { + var buf bytes.Buffer + + if err := gh.ExecuteAssignmentCommentTemplate(&buf, data); err != nil { + return fmt.Errorf("could not execute assignment comment template: %v", err) + } + + if err := a.IssueHelper.Comment(ctx, issue, buf.String()); err != nil { + return fmt.Errorf("could not create assignment comment: %v", err) + } + + return nil +} diff --git a/internal/gitstream/assign_test.go b/internal/gitstream/assign_test.go index bde4c71..c85803d 100644 --- a/internal/gitstream/assign_test.go +++ b/internal/gitstream/assign_test.go @@ -333,6 +333,7 @@ func TestAssign_handleIssue(t *testing.T) { mockUserHelper.EXPECT().GetCommitAuthor(ctx, sha.String()).Return(user, nil), mockOwnersHelper.EXPECT().IsApprover(o, *user.Login).Return(true), mockIssueHelper.EXPECT().Assign(ctx, issue, *user.Login).Return(nil), + mockIssueHelper.EXPECT().Comment(ctx, issue, gomock.Any()).Return(nil), ) err := a.handleIssue(ctx, issue, o) @@ -382,6 +383,7 @@ func TestAssign_handleIssue(t *testing.T) { mockOwnersHelper.EXPECT().IsApprover(o, *user.Login).Return(false), mockOwnersHelper.EXPECT().GetRandomApprover(o).Return(*user.Login, nil), mockIssueHelper.EXPECT().Assign(ctx, issue, *user.Login).Return(nil), + mockIssueHelper.EXPECT().Comment(ctx, issue, gomock.Any()).Return(nil), ) err := a.handleIssue(ctx, issue, o) @@ -586,6 +588,7 @@ func TestAssign_assignIssues(t *testing.T) { mockUserHelper.EXPECT().GetCommitAuthor(ctx, sha.String()).Return(user, nil), mockOwnersHelper.EXPECT().IsApprover(o, *user.Login).Return(true), mockIssueHelper.EXPECT().Assign(ctx, issues[1], o.Approvers[0]).Return(nil), + mockIssueHelper.EXPECT().Comment(ctx, issues[1], gomock.Any()).Return(nil), ) err := a.assignIssues(ctx) @@ -755,12 +758,14 @@ func TestAssign_assignIssues(t *testing.T) { mockUserHelper.EXPECT().GetCommitAuthor(ctx, sha.String()).Return(user, nil), mockOwnersHelper.EXPECT().IsApprover(o, *user.Login).Return(true), mockIssueHelper.EXPECT().Assign(ctx, issues[0], o.Approvers[0]).Return(nil), + mockIssueHelper.EXPECT().Comment(ctx, issues[0], gomock.Any()).Return(nil), // issue #2 mockFinder.EXPECT().FindSHAs(body).Return([]plumbing.Hash{sha}, nil), mockUserHelper.EXPECT().GetCommitAuthor(ctx, sha.String()).Return(user, nil), mockOwnersHelper.EXPECT().IsApprover(o, *user.Login).Return(true), mockIssueHelper.EXPECT().Assign(ctx, issues[1], o.Approvers[0]).Return(nil), + mockIssueHelper.EXPECT().Comment(ctx, issues[1], gomock.Any()).Return(nil), ) err := a.assignIssues(ctx) From d6f778b47283a93ea534795696a4fb6782a6ae3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:30:58 +0000 Subject: [PATCH 3/4] Add comprehensive PR assignment test and improve template testing Co-authored-by: ybettan <29724509+ybettan@users.noreply.github.com> --- internal/github/templates_test.go | 21 +++++++++++++ internal/gitstream/assign_test.go | 51 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/internal/github/templates_test.go b/internal/github/templates_test.go index 4e58b79..ac69fbb 100644 --- a/internal/github/templates_test.go +++ b/internal/github/templates_test.go @@ -98,4 +98,25 @@ func TestExecuteAssignmentCommentTemplate(t *testing.T) { assert.NotContains(t, result, "Referenced commits") assert.NotContains(t, result, "Commit authors") }) + + t.Run("template properly handles PRs and issues", func(t *testing.T) { + // For PRs and issues with SHAs, it should say "issue" + // For other items without SHAs, it should say "item" + dataWithSHAs := &AssignmentCommentData{ + AppName: "gitstream", + CommitSHAs: []string{"abc123"}, + CommitAuthors: []string{"user1"}, + ApproverCommitAuthors: []string{"user1"}, + AssignedUsers: []string{"user1"}, + AssignmentReason: "they are the author of a referenced commit and an approver.", + IsRandomAssignment: false, + } + + var buf bytes.Buffer + err := ExecuteAssignmentCommentTemplate(&buf, dataWithSHAs) + + assert.NoError(t, err) + result := buf.String() + assert.Contains(t, result, "I assigned this issue to **@user1**") + }) } \ No newline at end of file diff --git a/internal/gitstream/assign_test.go b/internal/gitstream/assign_test.go index c85803d..c31bc98 100644 --- a/internal/gitstream/assign_test.go +++ b/internal/gitstream/assign_test.go @@ -389,6 +389,57 @@ func TestAssign_handleIssue(t *testing.T) { err := a.handleIssue(ctx, issue, o) assert.NoError(t, err) }) + + t.Run("working as expected for PR", func(t *testing.T) { + + var ( + ctx = context.Background() + issueNumber = 123 + issueURL = "some url" + body = "some body" + ) + + ctrl := gomock.NewController(t) + + mockFinder := markup.NewMockFinder(ctrl) + mockUserHelper := gh.NewMockUserHelper(ctrl) + mockOwnersHelper := owners.NewMockOwnersHelper(ctrl) + mockIssueHelper := gh.NewMockIssueHelper(ctrl) + + a := Assign{ + Finder: mockFinder, + Logger: logr.Discard(), + OwnersHelper: mockOwnersHelper, + UserHelper: mockUserHelper, + IssueHelper: mockIssueHelper, + } + + // This is a PR (has PullRequestLinks) + prLinks := &github.PullRequestLinks{} + issue := &github.Issue{ + Number: &issueNumber, + HTMLURL: &issueURL, + Body: &body, + PullRequestLinks: prLinks, // This makes it a PR + } + + repo := test.NewRepo(t) + sha, _ := test.AddEmptyCommit(t, repo, "empty") + user := &github.User{ + Login: &o.Approvers[0], + } + + gomock.InOrder( + mockFinder.EXPECT().FindSHAs(body).Return([]plumbing.Hash{sha}, nil), + mockUserHelper.EXPECT().GetCommitAuthor(ctx, sha.String()).Return(user, nil), + mockOwnersHelper.EXPECT().IsApprover(o, *user.Login).Return(true), + mockIssueHelper.EXPECT().Assign(ctx, issue, *user.Login).Return(nil), + mockIssueHelper.EXPECT().Comment(ctx, issue, gomock.Any()).Return(nil), + ) + + err := a.handleIssue(ctx, issue, o) + assert.NoError(t, err) + }) } func TestAssign_assignIssues(t *testing.T) { From d0458ff2fd1403cab963dcf9f1abbee4349ea265 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:10:18 +0000 Subject: [PATCH 4/4] Add Comment method to PRHelper interface for consistency Co-authored-by: ybettan <29724509+ybettan@users.noreply.github.com> --- internal/github/mock_pr.go | 16 +++++++++++- internal/github/pr.go | 13 ++++++++++ internal/github/pr_test.go | 51 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/internal/github/mock_pr.go b/internal/github/mock_pr.go index 02629db..15e0f07 100644 --- a/internal/github/mock_pr.go +++ b/internal/github/mock_pr.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: pr.go +// Source: internal/github/pr.go // Package github is a generated GoMock package. package github @@ -36,6 +36,20 @@ func (m *MockPRHelper) EXPECT() *MockPRHelperMockRecorder { return m.recorder } +// Comment mocks base method. +func (m *MockPRHelper) Comment(ctx context.Context, pr *github.PullRequest, comment string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Comment", ctx, pr, comment) + ret0, _ := ret[0].(error) + return ret0 +} + +// Comment indicates an expected call of Comment. +func (mr *MockPRHelperMockRecorder) Comment(ctx, pr, comment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Comment", reflect.TypeOf((*MockPRHelper)(nil).Comment), ctx, pr, comment) +} + // Create mocks base method. func (m *MockPRHelper) Create(ctx context.Context, branch, base, upstreamURL string, commit *object.Commit, draft bool) (*github.PullRequest, error) { m.ctrl.T.Helper() diff --git a/internal/github/pr.go b/internal/github/pr.go index e25c78b..08c344e 100644 --- a/internal/github/pr.go +++ b/internal/github/pr.go @@ -21,6 +21,7 @@ type PRHelper interface { Create(ctx context.Context, branch, base, upstreamURL string, commit *object.Commit, draft bool) (*github.PullRequest, error) ListAllOpen(ctx context.Context, filter PRFilterFunc) ([]*github.PullRequest, error) MakeReady(ctx context.Context, pr *github.PullRequest) error + Comment(ctx context.Context, pr *github.PullRequest, comment string) error } type PRHelperImpl struct { @@ -148,3 +149,15 @@ func PRHasLabel(pr *github.PullRequest, label string) bool { return false } + +func (ph *PRHelperImpl) Comment(ctx context.Context, pr *github.PullRequest, comment string) error { + commentRequest := &github.IssueComment{ + Body: github.String(comment), + } + + if _, _, err := ph.gc.Issues.CreateComment(ctx, ph.repoName.Owner, ph.repoName.Repo, *pr.Number, commentRequest); err != nil { + return fmt.Errorf("failed to create comment: %v", err) + } + + return nil +} diff --git a/internal/github/pr_test.go b/internal/github/pr_test.go index 2201f6d..f1dc3d8 100644 --- a/internal/github/pr_test.go +++ b/internal/github/pr_test.go @@ -95,3 +95,54 @@ func TestPRHelperImpl_Create(t *testing.T) { assert.NoError(t, err) assert.Equal(t, pr, res) } + +func TestPRHelperImpl_Comment(t *testing.T) { + const ( + owner = "owner" + repo = "repo" + prNumber = 123 + commentBody = "This is a test comment" + ) + + pr := &github.PullRequest{ + Number: github.Int(prNumber), + } + + c := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + m := make(map[string]interface{}) + + assert.NoError( + t, + json.NewDecoder(r.Body).Decode(&m), + ) + + assert.Equal(t, commentBody, m["body"]) + assert.Equal( + t, + fmt.Sprintf("/repos/%s/%s/issues/%d/comments", owner, repo, prNumber), + r.RequestURI, + ) + + comment := &github.IssueComment{ + ID: github.Int64(1), + Body: github.String(commentBody), + } + + assert.NoError( + t, + json.NewEncoder(w).Encode(comment), + ) + }), + ), + ) + + gc := github.NewClient(c) + prHelper := gh.NewPRHelper(gc, nil, "Markup", &gh.RepoName{Owner: owner, Repo: repo}) + + err := prHelper.Comment(context.Background(), pr, commentBody) + + assert.NoError(t, err) +}