From 705ec00d421ca9c05123a61e5c1158e0c4cdd89f Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Thu, 12 Dec 2024 15:17:48 +0200 Subject: [PATCH] Add PullRequestCommenter provider trait This allows for providers to comment on pull requests Signed-off-by: Juan Antonio Osorio --- internal/engine/actions/alert/alert.go | 2 +- .../pull_request_comment.go | 144 ++++----- .../pull_request_comment_test.go | 26 +- internal/providers/github/clients/app.go | 14 + internal/providers/github/clients/oauth.go | 9 + internal/providers/github/commenter.go | 289 ++++++++++++++++++ internal/providers/github/common.go | 29 ++ internal/providers/github/mock/github.go | 170 +++++++++++ pkg/providers/v1/mock/providers.go | 170 +++++++++++ pkg/providers/v1/providers.go | 50 +++ 10 files changed, 796 insertions(+), 107 deletions(-) create mode 100644 internal/providers/github/commenter.go diff --git a/internal/engine/actions/alert/alert.go b/internal/engine/actions/alert/alert.go index 5da2a06ab3..c8ac0ddbd5 100644 --- a/internal/engine/actions/alert/alert.go +++ b/internal/engine/actions/alert/alert.go @@ -53,7 +53,7 @@ func NewRuleAlert( if alertCfg.GetPullRequestComment() == nil { return nil, fmt.Errorf("alert engine missing pull_request_review configuration") } - client, err := provinfv1.As[provinfv1.GitHub](provider) + client, err := provinfv1.As[provinfv1.PullRequestCommenter](provider) if err != nil { zerolog.Ctx(ctx).Debug().Str("rule-type", ruletype.GetName()). Msg("provider is not a GitHub provider. Silently skipping alerts.") diff --git a/internal/engine/actions/alert/pull_request_comment/pull_request_comment.go b/internal/engine/actions/alert/pull_request_comment/pull_request_comment.go index 3bb9d23c66..4b61f8fdf5 100644 --- a/internal/engine/actions/alert/pull_request_comment/pull_request_comment.go +++ b/internal/engine/actions/alert/pull_request_comment/pull_request_comment.go @@ -8,11 +8,7 @@ package pull_request_comment import ( "context" "encoding/json" - "errors" "fmt" - "math" - "strconv" - "time" "github.com/google/go-github/v63/github" "github.com/rs/zerolog" @@ -21,6 +17,7 @@ import ( "github.com/mindersec/minder/internal/db" enginerr "github.com/mindersec/minder/internal/engine/errors" "github.com/mindersec/minder/internal/engine/interfaces" + "github.com/mindersec/minder/internal/entities/properties" pbinternal "github.com/mindersec/minder/internal/proto" "github.com/mindersec/minder/internal/util" pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" @@ -39,7 +36,7 @@ const ( // Alert is the structure backing the noop alert type Alert struct { actionType interfaces.ActionType - gh provifv1.GitHub + commenter provifv1.PullRequestCommenter reviewCfg *pb.RuleType_Definition_Alert_AlertTypePRComment setting models.ActionOpt } @@ -54,26 +51,17 @@ type PrCommentTemplateParams struct { } type paramsPR struct { - Owner string - Repo string - CommitSha string - Number int Comment string - Metadata *alertMetadata + props *properties.Properties + Metadata *provifv1.CommentResultMeta prevStatus *db.ListRuleEvaluationsByProfileIdRow } -type alertMetadata struct { - ReviewID string `json:"review_id,omitempty"` - SubmittedAt *time.Time `json:"submitted_at,omitempty"` - PullRequestUrl *string `json:"pull_request_url,omitempty"` -} - // NewPullRequestCommentAlert creates a new pull request comment alert action func NewPullRequestCommentAlert( actionType interfaces.ActionType, reviewCfg *pb.RuleType_Definition_Alert_AlertTypePRComment, - gh provifv1.GitHub, + gh provifv1.PullRequestCommenter, setting models.ActionOpt, ) (*Alert, error) { if actionType == "" { @@ -82,7 +70,7 @@ func NewPullRequestCommentAlert( return &Alert{ actionType: actionType, - gh: gh, + commenter: gh, reviewCfg: reviewCfg, setting: setting, }, nil @@ -134,70 +122,20 @@ func (alert *Alert) Do( } func (alert *Alert) run(ctx context.Context, params *paramsPR, cmd interfaces.ActionCmd) (json.RawMessage, error) { - logger := zerolog.Ctx(ctx) - // Process the command switch cmd { - // Create a review case interfaces.ActionCmdOn: - review := &github.PullRequestReviewRequest{ - CommitID: github.String(params.CommitSha), - Event: github.String("COMMENT"), - Body: github.String(params.Comment), - } - - r, err := alert.gh.CreateReview( - ctx, - params.Owner, - params.Repo, - params.Number, - review, - ) - if err != nil { - return nil, fmt.Errorf("error creating PR review: %w, %w", err, enginerr.ErrActionFailed) - } - - newMeta, err := json.Marshal(alertMetadata{ - ReviewID: strconv.FormatInt(r.GetID(), 10), - SubmittedAt: r.SubmittedAt.GetTime(), - PullRequestUrl: r.PullRequestURL, - }) - if err != nil { - return nil, fmt.Errorf("error marshalling alert metadata json: %w", err) - } - - logger.Info().Int64("review_id", *r.ID).Msg("PR review created") - return newMeta, nil - // Dismiss the review + // Create a review + return alert.runDoReview(ctx, params) case interfaces.ActionCmdOff: - if params.Metadata == nil { - // We cannot do anything without the PR review ID, so we assume that turning the alert off is a success - return nil, fmt.Errorf("no PR comment ID provided: %w", enginerr.ErrActionTurnedOff) - } - - reviewID, err := strconv.ParseInt(params.Metadata.ReviewID, 10, 64) - if err != nil { - zerolog.Ctx(ctx).Error().Err(err).Str("review_id", params.Metadata.ReviewID).Msg("failed to parse review_id") - return nil, fmt.Errorf("no PR comment ID provided: %w, %w", err, enginerr.ErrActionTurnedOff) - } - - _, err = alert.gh.DismissReview(ctx, params.Owner, params.Repo, params.Number, reviewID, - &github.PullRequestReviewDismissalRequest{ - Message: github.String("Dismissed due to alert being turned off"), - }) - if err != nil { - if errors.Is(err, enginerr.ErrNotFound) { - // There's no PR review with that ID anymore. - // We exit by stating that the action was turned off. - return nil, fmt.Errorf("PR comment already dismissed: %w, %w", err, enginerr.ErrActionTurnedOff) - } - return nil, fmt.Errorf("error dismissing PR comment: %w, %w", err, enginerr.ErrActionFailed) - } - logger.Info().Str("review_id", params.Metadata.ReviewID).Msg("PR comment dismissed") - // Success - return ErrActionTurnedOff to indicate the action was successful - return nil, fmt.Errorf("%s : %w", alert.Class(), enginerr.ErrActionTurnedOff) + return json.RawMessage(`{}`), nil case interfaces.ActionCmdDoNothing: - // Return the previous alert status. + // If the previous status didn't change (still a failure, for instance) we + // want to refresh the alert. + if alert.setting == models.ActionOptOn { + return alert.runDoReview(ctx, params) + } + // Else, we just do nothing. return alert.runDoNothing(ctx, params) } return nil, enginerr.ErrActionSkipped @@ -211,16 +149,16 @@ func (alert *Alert) runDry(ctx context.Context, params *paramsPR, cmd interfaces switch cmd { case interfaces.ActionCmdOn: body := github.String(params.Comment) - logger.Info().Msgf("dry run: create a PR comment on PR %d in repo %s/%s with the following body: %s", - params.Number, params.Owner, params.Repo, *body) + logger.Info().Dict("properties", params.props.ToLogDict()). + Msgf("dry run: create a PR comment on PR with body: %s", *body) return nil, nil case interfaces.ActionCmdOff: if params.Metadata == nil { // We cannot do anything without the PR review ID, so we assume that turning the alert off is a success return nil, fmt.Errorf("no PR comment ID provided: %w", enginerr.ErrActionTurnedOff) } - logger.Info().Msgf("dry run: dismiss PR comment %s on PR PR %d in repo %s/%s", params.Metadata.ReviewID, - params.Number, params.Owner, params.Repo) + logger.Info().Dict("properties", params.props.ToLogDict()). + Msgf("dry run: dismiss PR comment on PR") case interfaces.ActionCmdDoNothing: // Return the previous alert status. return alert.runDoNothing(ctx, params) @@ -231,7 +169,7 @@ func (alert *Alert) runDry(ctx context.Context, params *paramsPR, cmd interfaces // runDoNothing returns the previous alert status func (_ *Alert) runDoNothing(ctx context.Context, params *paramsPR) (json.RawMessage, error) { - logger := zerolog.Ctx(ctx).With().Str("repo", params.Repo).Logger() + logger := zerolog.Ctx(ctx).With().Dict("properties", params.props.ToLogDict()).Logger() logger.Debug().Msg("Running do nothing") @@ -245,6 +183,30 @@ func (_ *Alert) runDoNothing(ctx context.Context, params *paramsPR) (json.RawMes return nil, err } +func (alert *Alert) runDoReview(ctx context.Context, params *paramsPR) (json.RawMessage, error) { + logger := zerolog.Ctx(ctx) + + r, err := alert.commenter.CommentOnPullRequest(ctx, params.props, provifv1.PullRequestCommentInfo{ + // TODO: We should add the header to identify the alert. We could use the + // rule type name. + Commit: params.props.GetProperty(properties.PullRequestCommitSHA).GetString(), + Body: params.Comment, + // TODO: Determine the priority from the rule type severity + }) + if err != nil { + return nil, fmt.Errorf("error creating PR review: %w, %w", err, enginerr.ErrActionFailed) + } + logger.Info().Str("review_id", r.ID).Msg("PR review created") + + // serialize r to json + meta, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("error marshalling PR review metadata: %w", err) + } + + return meta, nil +} + // getParamsForSecurityAdvisory extracts the details from the entity func (alert *Alert) getParamsForPRComment( ctx context.Context, @@ -253,19 +215,15 @@ func (alert *Alert) getParamsForPRComment( metadata *json.RawMessage, ) (*paramsPR, error) { logger := zerolog.Ctx(ctx) - result := ¶msPR{ - prevStatus: params.GetEvalStatusFromDb(), - Owner: pr.GetRepoOwner(), - Repo: pr.GetRepoName(), - CommitSha: pr.GetCommitSha(), + props, err := properties.NewProperties(pr.GetProperties().AsMap()) + if err != nil { + return nil, fmt.Errorf("error creating properties: %w", err) } - // The GitHub Go API takes an int32, but our proto stores an int64; make sure we don't overflow - // The PR number is an int in GitHub and Gitlab; in practice overflow will never happen. - if pr.Number > math.MaxInt { - return nil, fmt.Errorf("pr number is too large") + result := ¶msPR{ + prevStatus: params.GetEvalStatusFromDb(), + props: props, } - result.Number = int(pr.Number) commentTmpl, err := util.NewSafeHTMLTemplate(&alert.reviewCfg.ReviewMessage, "message") if err != nil { @@ -289,7 +247,7 @@ func (alert *Alert) getParamsForPRComment( // Unmarshal the existing alert metadata, if any if metadata != nil { - meta := &alertMetadata{} + meta := &provifv1.CommentResultMeta{} err := json.Unmarshal(*metadata, meta) if err != nil { // There's nothing saved apparently, so no need to fail here, but do log the error diff --git a/internal/engine/actions/alert/pull_request_comment/pull_request_comment_test.go b/internal/engine/actions/alert/pull_request_comment/pull_request_comment_test.go index f02dae1cc9..bf3aed30d0 100644 --- a/internal/engine/actions/alert/pull_request_comment/pull_request_comment_test.go +++ b/internal/engine/actions/alert/pull_request_comment/pull_request_comment_test.go @@ -18,10 +18,10 @@ import ( enginerr "github.com/mindersec/minder/internal/engine/errors" engif "github.com/mindersec/minder/internal/engine/interfaces" pbinternal "github.com/mindersec/minder/internal/proto" - mockghclient "github.com/mindersec/minder/internal/providers/github/mock" pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" "github.com/mindersec/minder/pkg/engine/v1/interfaces" "github.com/mindersec/minder/pkg/profiles/models" + mockcommenter "github.com/mindersec/minder/pkg/providers/v1/mock" ) var TestActionTypeValid engif.ActionType = "alert-test" @@ -44,7 +44,7 @@ func TestPullRequestCommentAlert(t *testing.T) { cmd engif.ActionCmd reviewMsg string inputMetadata *json.RawMessage - mockSetup func(*mockghclient.MockGitHub) + mockSetup func(commenter *mockcommenter.MockPullRequestCommenter) expectedErr error expectedMetadata json.RawMessage }{ @@ -53,9 +53,9 @@ func TestPullRequestCommentAlert(t *testing.T) { actionType: TestActionTypeValid, reviewMsg: "This is a constant review message", cmd: engif.ActionCmdOn, - mockSetup: func(mockGitHub *mockghclient.MockGitHub) { + mockSetup: func(mockGitHub *mockcommenter.MockPullRequestCommenter) { mockGitHub.EXPECT(). - CreateReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + CommentOnPullRequest(gomock.Any(), gomock.Any(), gomock.Any()). Return(&github.PullRequestReview{ID: &reviewID}, nil) }, expectedMetadata: json.RawMessage(fmt.Sprintf(`{"review_id":"%s"}`, reviewIDStr)), @@ -65,9 +65,9 @@ func TestPullRequestCommentAlert(t *testing.T) { actionType: TestActionTypeValid, reviewMsg: "{{ .EvalErrorDetails }}", cmd: engif.ActionCmdOn, - mockSetup: func(mockGitHub *mockghclient.MockGitHub) { + mockSetup: func(mockGitHub *mockcommenter.MockPullRequestCommenter) { mockGitHub.EXPECT(). - CreateReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&github.PullRequestReviewRequest{})). + CommentOnPullRequest(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(validateReviewBodyAndReturn(evaluationFailureDetails, reviewID)) }, expectedMetadata: json.RawMessage(fmt.Sprintf(`{"review_id":"%s"}`, reviewIDStr)), @@ -77,9 +77,9 @@ func TestPullRequestCommentAlert(t *testing.T) { actionType: TestActionTypeValid, reviewMsg: "{{ .EvalResultOutput.ViolationMsg }}", cmd: engif.ActionCmdOn, - mockSetup: func(mockGitHub *mockghclient.MockGitHub) { + mockSetup: func(mockGitHub *mockcommenter.MockPullRequestCommenter) { mockGitHub.EXPECT(). - CreateReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&github.PullRequestReviewRequest{})). + CommentOnPullRequest(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(validateReviewBodyAndReturn(violationMsg, reviewID)) }, expectedMetadata: json.RawMessage(fmt.Sprintf(`{"review_id":"%s"}`, reviewIDStr)), @@ -89,9 +89,9 @@ func TestPullRequestCommentAlert(t *testing.T) { actionType: TestActionTypeValid, reviewMsg: "This is a constant review message", cmd: engif.ActionCmdOn, - mockSetup: func(mockGitHub *mockghclient.MockGitHub) { + mockSetup: func(mockGitHub *mockcommenter.MockPullRequestCommenter) { mockGitHub.EXPECT(). - CreateReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + CommentOnPullRequest(gomock.Any(), gomock.Any(), gomock.Any()). Return(nil, fmt.Errorf("failed to create PR comment")) }, expectedErr: enginerr.ErrActionFailed, @@ -102,9 +102,9 @@ func TestPullRequestCommentAlert(t *testing.T) { reviewMsg: "This is a constant review message", cmd: engif.ActionCmdOff, inputMetadata: &successfulRunMetadata, - mockSetup: func(mockGitHub *mockghclient.MockGitHub) { + mockSetup: func(mockGitHub *mockcommenter.MockPullRequestCommenter) { mockGitHub.EXPECT(). - DismissReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + CommentOnPullRequest(gomock.Any(), gomock.Any(), gomock.Any()). Return(&github.PullRequestReview{}, nil) }, expectedErr: enginerr.ErrActionTurnedOff, @@ -126,7 +126,7 @@ func TestPullRequestCommentAlert(t *testing.T) { ReviewMessage: tt.reviewMsg, } - mockClient := mockghclient.NewMockGitHub(ctrl) + mockClient := mockcommenter.NewMockPullRequestCommenter(ctrl) tt.mockSetup(mockClient) prCommentAlert, err := NewPullRequestCommentAlert( diff --git a/internal/providers/github/clients/app.go b/internal/providers/github/clients/app.go index b65d3a1e15..da555d701c 100644 --- a/internal/providers/github/clients/app.go +++ b/internal/providers/github/clients/app.go @@ -292,6 +292,20 @@ func (g *GitHubAppDelegate) GetUserId(ctx context.Context) (int64, error) { return user.GetID(), nil } +// GetMinderUserId returns the user id for the GitHub App user +func (g *GitHubAppDelegate) GetMinderUserId(ctx context.Context) (int64, error) { + // Try to get this user ID from the GitHub API + //nolint:errcheck // this will never error + appUserName, _ := g.GetName(ctx) + user, _, err := g.client.Users.Get(ctx, appUserName) + if err != nil { + // Fallback to the configured user ID + // note: this is different from the App ID + return g.defaultUserId, nil + } + return user.GetID(), nil +} + // GetName returns the username for the GitHub App user func (g *GitHubAppDelegate) GetName(_ context.Context) (string, error) { return fmt.Sprintf("%s[bot]", g.appName), nil diff --git a/internal/providers/github/clients/oauth.go b/internal/providers/github/clients/oauth.go index a1ff847825..4149a280ed 100644 --- a/internal/providers/github/clients/oauth.go +++ b/internal/providers/github/clients/oauth.go @@ -213,6 +213,15 @@ func (o *GitHubOAuthDelegate) GetUserId(ctx context.Context) (int64, error) { return user.GetID(), nil } +// GetMinderUserId returns the user id for the authenticated user +func (o *GitHubOAuthDelegate) GetMinderUserId(ctx context.Context) (int64, error) { + user, _, err := o.client.Users.Get(ctx, "") + if err != nil { + return 0, err + } + return user.GetID(), nil +} + // GetName returns the username for the authenticated user func (o *GitHubOAuthDelegate) GetName(ctx context.Context) (string, error) { user, _, err := o.client.Users.Get(ctx, "") diff --git a/internal/providers/github/commenter.go b/internal/providers/github/commenter.go new file mode 100644 index 0000000000..e9f9c847da --- /dev/null +++ b/internal/providers/github/commenter.go @@ -0,0 +1,289 @@ +// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package github + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/google/go-github/v63/github" + "github.com/rs/zerolog" + + "github.com/mindersec/minder/internal/util" + "github.com/mindersec/minder/internal/util/ptr" + provifv1 "github.com/mindersec/minder/pkg/providers/v1" +) + +const ( + // MagicCommentLimit is the maximum length of the magic comment + MagicCommentLimit = 1024 + // CommentLimit is the maximum length of the comment + CommentLimit = 65536 + minderTemplateMagicCommentName = "minderCommentBody" + //nolint:lll + statusBodyMagicComment = `` + statusBodyMagicCommentPrefix = "", statusBodyMagicCommentPrefix)) + + matches := re.FindStringSubmatch(input) + if len(matches) != 2 { + return magicCommentInfo{}, errors.New("no match found") + } + + jsonPart := matches[1] + + var strMagicCommentInfo struct { + ContentSha string `json:"ContentSha"` + ReviewID string `json:"ReviewID"` // Assuming you're handling ReviewID as a string + } + err := json.Unmarshal([]byte(jsonPart), &strMagicCommentInfo) + if err != nil { + return magicCommentInfo{}, fmt.Errorf("error unmarshalling JSON: %w", err) + } + + var contentInfo magicCommentInfo + contentInfo.ContentSha = strMagicCommentInfo.ContentSha + contentInfo.ReviewID, err = strconv.ParseInt(strMagicCommentInfo.ReviewID, 10, 64) + if err != nil { + return magicCommentInfo{}, fmt.Errorf("error parsing ReviewID: %w", err) + } + + return contentInfo, nil +} diff --git a/internal/providers/github/common.go b/internal/providers/github/common.go index c512810d28..46c67f3ac1 100644 --- a/internal/providers/github/common.go +++ b/internal/providers/github/common.go @@ -25,6 +25,7 @@ import ( "github.com/mindersec/minder/internal/db" engerrors "github.com/mindersec/minder/internal/engine/errors" + entprops "github.com/mindersec/minder/internal/entities/properties" gitclient "github.com/mindersec/minder/internal/providers/git" "github.com/mindersec/minder/internal/providers/github/ghcr" "github.com/mindersec/minder/internal/providers/github/properties" @@ -145,6 +146,7 @@ type Delegate interface { GetCredential() provifv1.GitHubCredential ListAllRepositories(context.Context) ([]*minderv1.Repository, error) GetUserId(ctx context.Context) (int64, error) + GetMinderUserId(ctx context.Context) (int64, error) GetName(ctx context.Context) (string, error) GetLogin(ctx context.Context) (string, error) GetPrimaryEmail(ctx context.Context) (string, error) @@ -419,6 +421,33 @@ func (c *GitHub) ListFiles( return resp.files, resp.resp, err } +// CommentOnPullRequest implements the CommentOnPullRequest method of the GitHub interface +func (c *GitHub) CommentOnPullRequest( + ctx context.Context, getByProps *entprops.Properties, comment provifv1.PullRequestCommentInfo, +) (*provifv1.CommentResultMeta, error) { + owner := getByProps.GetProperty(properties.PullPropertyRepoOwner).GetString() + name := getByProps.GetProperty(properties.PullPropertyRepoName).GetString() + prNum := getByProps.GetProperty(properties.PullPropertyNumber).GetInt64() + authorID := getByProps.GetProperty(properties.PullPropertyAuthorID).GetInt64() + + authorizedUser, err := c.delegate.GetMinderUserId(ctx) + if err != nil { + return nil, fmt.Errorf("could not get authenticated user: %w", err) + } + mci, err := c.findPreviousStatusComment(ctx, owner, name, prNum, authorizedUser) + if err != nil { + return nil, fmt.Errorf("could not find previous status comment: %w", err) + } + + mci, err = c.updateOrSubmitComment( + ctx, mci, comment, owner, name, prNum, comment.Commit, authorID, authorizedUser) + if err != nil { + // this should be fatal. In case we can't submit the review, we can't proceed + return nil, fmt.Errorf("could not submit review: %w", err) + } + return mci.ToCommentResultMeta(), nil +} + // CreateReview is a wrapper for the GitHub API to create a review func (c *GitHub) CreateReview( ctx context.Context, owner, repo string, number int, reviewRequest *github.PullRequestReviewRequest, diff --git a/internal/providers/github/mock/github.go b/internal/providers/github/mock/github.go index 6e4de76da3..53760b6e5b 100644 --- a/internal/providers/github/mock/github.go +++ b/internal/providers/github/mock/github.go @@ -2016,3 +2016,173 @@ func (mr *MockOCIMockRecorder) SupportsEntity(entType any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsEntity", reflect.TypeOf((*MockOCI)(nil).SupportsEntity), entType) } + +// MockPullRequestCommenter is a mock of PullRequestCommenter interface. +type MockPullRequestCommenter struct { + ctrl *gomock.Controller + recorder *MockPullRequestCommenterMockRecorder + isgomock struct{} +} + +// MockPullRequestCommenterMockRecorder is the mock recorder for MockPullRequestCommenter. +type MockPullRequestCommenterMockRecorder struct { + mock *MockPullRequestCommenter +} + +// NewMockPullRequestCommenter creates a new mock instance. +func NewMockPullRequestCommenter(ctrl *gomock.Controller) *MockPullRequestCommenter { + mock := &MockPullRequestCommenter{ctrl: ctrl} + mock.recorder = &MockPullRequestCommenterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPullRequestCommenter) EXPECT() *MockPullRequestCommenterMockRecorder { + return m.recorder +} + +// CanImplement mocks base method. +func (m *MockPullRequestCommenter) CanImplement(trait v10.ProviderType) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CanImplement", trait) + ret0, _ := ret[0].(bool) + return ret0 +} + +// CanImplement indicates an expected call of CanImplement. +func (mr *MockPullRequestCommenterMockRecorder) CanImplement(trait any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanImplement", reflect.TypeOf((*MockPullRequestCommenter)(nil).CanImplement), trait) +} + +// CommentOnPullRequest mocks base method. +func (m *MockPullRequestCommenter) CommentOnPullRequest(ctx context.Context, getByProps *properties.Properties, comment v11.PullRequestCommentInfo) (*v11.CommentResultMeta, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CommentOnPullRequest", ctx, getByProps, comment) + ret0, _ := ret[0].(*v11.CommentResultMeta) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CommentOnPullRequest indicates an expected call of CommentOnPullRequest. +func (mr *MockPullRequestCommenterMockRecorder) CommentOnPullRequest(ctx, getByProps, comment any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommentOnPullRequest", reflect.TypeOf((*MockPullRequestCommenter)(nil).CommentOnPullRequest), ctx, getByProps, comment) +} + +// DeregisterEntity mocks base method. +func (m *MockPullRequestCommenter) DeregisterEntity(ctx context.Context, entType v10.Entity, props *properties.Properties) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeregisterEntity", ctx, entType, props) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeregisterEntity indicates an expected call of DeregisterEntity. +func (mr *MockPullRequestCommenterMockRecorder) DeregisterEntity(ctx, entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeregisterEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).DeregisterEntity), ctx, entType, props) +} + +// FetchAllProperties mocks base method. +func (m *MockPullRequestCommenter) FetchAllProperties(ctx context.Context, getByProps *properties.Properties, entType v10.Entity, cachedProps *properties.Properties) (*properties.Properties, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchAllProperties", ctx, getByProps, entType, cachedProps) + ret0, _ := ret[0].(*properties.Properties) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchAllProperties indicates an expected call of FetchAllProperties. +func (mr *MockPullRequestCommenterMockRecorder) FetchAllProperties(ctx, getByProps, entType, cachedProps any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAllProperties", reflect.TypeOf((*MockPullRequestCommenter)(nil).FetchAllProperties), ctx, getByProps, entType, cachedProps) +} + +// FetchProperty mocks base method. +func (m *MockPullRequestCommenter) FetchProperty(ctx context.Context, getByProps *properties.Properties, entType v10.Entity, key string) (*properties.Property, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchProperty", ctx, getByProps, entType, key) + ret0, _ := ret[0].(*properties.Property) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchProperty indicates an expected call of FetchProperty. +func (mr *MockPullRequestCommenterMockRecorder) FetchProperty(ctx, getByProps, entType, key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchProperty", reflect.TypeOf((*MockPullRequestCommenter)(nil).FetchProperty), ctx, getByProps, entType, key) +} + +// GetEntityName mocks base method. +func (m *MockPullRequestCommenter) GetEntityName(entType v10.Entity, props *properties.Properties) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEntityName", entType, props) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEntityName indicates an expected call of GetEntityName. +func (mr *MockPullRequestCommenterMockRecorder) GetEntityName(entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntityName", reflect.TypeOf((*MockPullRequestCommenter)(nil).GetEntityName), entType, props) +} + +// PropertiesToProtoMessage mocks base method. +func (m *MockPullRequestCommenter) PropertiesToProtoMessage(entType v10.Entity, props *properties.Properties) (protoreflect.ProtoMessage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PropertiesToProtoMessage", entType, props) + ret0, _ := ret[0].(protoreflect.ProtoMessage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PropertiesToProtoMessage indicates an expected call of PropertiesToProtoMessage. +func (mr *MockPullRequestCommenterMockRecorder) PropertiesToProtoMessage(entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PropertiesToProtoMessage", reflect.TypeOf((*MockPullRequestCommenter)(nil).PropertiesToProtoMessage), entType, props) +} + +// RegisterEntity mocks base method. +func (m *MockPullRequestCommenter) RegisterEntity(ctx context.Context, entType v10.Entity, props *properties.Properties) (*properties.Properties, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RegisterEntity", ctx, entType, props) + ret0, _ := ret[0].(*properties.Properties) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RegisterEntity indicates an expected call of RegisterEntity. +func (mr *MockPullRequestCommenterMockRecorder) RegisterEntity(ctx, entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).RegisterEntity), ctx, entType, props) +} + +// ReregisterEntity mocks base method. +func (m *MockPullRequestCommenter) ReregisterEntity(ctx context.Context, entType v10.Entity, props *properties.Properties) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReregisterEntity", ctx, entType, props) + ret0, _ := ret[0].(error) + return ret0 +} + +// ReregisterEntity indicates an expected call of ReregisterEntity. +func (mr *MockPullRequestCommenterMockRecorder) ReregisterEntity(ctx, entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReregisterEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).ReregisterEntity), ctx, entType, props) +} + +// SupportsEntity mocks base method. +func (m *MockPullRequestCommenter) SupportsEntity(entType v10.Entity) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupportsEntity", entType) + ret0, _ := ret[0].(bool) + return ret0 +} + +// SupportsEntity indicates an expected call of SupportsEntity. +func (mr *MockPullRequestCommenterMockRecorder) SupportsEntity(entType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).SupportsEntity), entType) +} diff --git a/pkg/providers/v1/mock/providers.go b/pkg/providers/v1/mock/providers.go index 1501431d3d..c6a9925855 100644 --- a/pkg/providers/v1/mock/providers.go +++ b/pkg/providers/v1/mock/providers.go @@ -2016,3 +2016,173 @@ func (mr *MockOCIMockRecorder) SupportsEntity(entType any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsEntity", reflect.TypeOf((*MockOCI)(nil).SupportsEntity), entType) } + +// MockPullRequestCommenter is a mock of PullRequestCommenter interface. +type MockPullRequestCommenter struct { + ctrl *gomock.Controller + recorder *MockPullRequestCommenterMockRecorder + isgomock struct{} +} + +// MockPullRequestCommenterMockRecorder is the mock recorder for MockPullRequestCommenter. +type MockPullRequestCommenterMockRecorder struct { + mock *MockPullRequestCommenter +} + +// NewMockPullRequestCommenter creates a new mock instance. +func NewMockPullRequestCommenter(ctrl *gomock.Controller) *MockPullRequestCommenter { + mock := &MockPullRequestCommenter{ctrl: ctrl} + mock.recorder = &MockPullRequestCommenterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPullRequestCommenter) EXPECT() *MockPullRequestCommenterMockRecorder { + return m.recorder +} + +// CanImplement mocks base method. +func (m *MockPullRequestCommenter) CanImplement(trait v10.ProviderType) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CanImplement", trait) + ret0, _ := ret[0].(bool) + return ret0 +} + +// CanImplement indicates an expected call of CanImplement. +func (mr *MockPullRequestCommenterMockRecorder) CanImplement(trait any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanImplement", reflect.TypeOf((*MockPullRequestCommenter)(nil).CanImplement), trait) +} + +// CommentOnPullRequest mocks base method. +func (m *MockPullRequestCommenter) CommentOnPullRequest(ctx context.Context, getByProps *properties.Properties, comment v11.PullRequestCommentInfo) (*v11.CommentResultMeta, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CommentOnPullRequest", ctx, getByProps, comment) + ret0, _ := ret[0].(*v11.CommentResultMeta) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CommentOnPullRequest indicates an expected call of CommentOnPullRequest. +func (mr *MockPullRequestCommenterMockRecorder) CommentOnPullRequest(ctx, getByProps, comment any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommentOnPullRequest", reflect.TypeOf((*MockPullRequestCommenter)(nil).CommentOnPullRequest), ctx, getByProps, comment) +} + +// DeregisterEntity mocks base method. +func (m *MockPullRequestCommenter) DeregisterEntity(ctx context.Context, entType v10.Entity, props *properties.Properties) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeregisterEntity", ctx, entType, props) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeregisterEntity indicates an expected call of DeregisterEntity. +func (mr *MockPullRequestCommenterMockRecorder) DeregisterEntity(ctx, entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeregisterEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).DeregisterEntity), ctx, entType, props) +} + +// FetchAllProperties mocks base method. +func (m *MockPullRequestCommenter) FetchAllProperties(ctx context.Context, getByProps *properties.Properties, entType v10.Entity, cachedProps *properties.Properties) (*properties.Properties, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchAllProperties", ctx, getByProps, entType, cachedProps) + ret0, _ := ret[0].(*properties.Properties) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchAllProperties indicates an expected call of FetchAllProperties. +func (mr *MockPullRequestCommenterMockRecorder) FetchAllProperties(ctx, getByProps, entType, cachedProps any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAllProperties", reflect.TypeOf((*MockPullRequestCommenter)(nil).FetchAllProperties), ctx, getByProps, entType, cachedProps) +} + +// FetchProperty mocks base method. +func (m *MockPullRequestCommenter) FetchProperty(ctx context.Context, getByProps *properties.Properties, entType v10.Entity, key string) (*properties.Property, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchProperty", ctx, getByProps, entType, key) + ret0, _ := ret[0].(*properties.Property) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchProperty indicates an expected call of FetchProperty. +func (mr *MockPullRequestCommenterMockRecorder) FetchProperty(ctx, getByProps, entType, key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchProperty", reflect.TypeOf((*MockPullRequestCommenter)(nil).FetchProperty), ctx, getByProps, entType, key) +} + +// GetEntityName mocks base method. +func (m *MockPullRequestCommenter) GetEntityName(entType v10.Entity, props *properties.Properties) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEntityName", entType, props) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEntityName indicates an expected call of GetEntityName. +func (mr *MockPullRequestCommenterMockRecorder) GetEntityName(entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntityName", reflect.TypeOf((*MockPullRequestCommenter)(nil).GetEntityName), entType, props) +} + +// PropertiesToProtoMessage mocks base method. +func (m *MockPullRequestCommenter) PropertiesToProtoMessage(entType v10.Entity, props *properties.Properties) (protoreflect.ProtoMessage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PropertiesToProtoMessage", entType, props) + ret0, _ := ret[0].(protoreflect.ProtoMessage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PropertiesToProtoMessage indicates an expected call of PropertiesToProtoMessage. +func (mr *MockPullRequestCommenterMockRecorder) PropertiesToProtoMessage(entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PropertiesToProtoMessage", reflect.TypeOf((*MockPullRequestCommenter)(nil).PropertiesToProtoMessage), entType, props) +} + +// RegisterEntity mocks base method. +func (m *MockPullRequestCommenter) RegisterEntity(ctx context.Context, entType v10.Entity, props *properties.Properties) (*properties.Properties, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RegisterEntity", ctx, entType, props) + ret0, _ := ret[0].(*properties.Properties) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RegisterEntity indicates an expected call of RegisterEntity. +func (mr *MockPullRequestCommenterMockRecorder) RegisterEntity(ctx, entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).RegisterEntity), ctx, entType, props) +} + +// ReregisterEntity mocks base method. +func (m *MockPullRequestCommenter) ReregisterEntity(ctx context.Context, entType v10.Entity, props *properties.Properties) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReregisterEntity", ctx, entType, props) + ret0, _ := ret[0].(error) + return ret0 +} + +// ReregisterEntity indicates an expected call of ReregisterEntity. +func (mr *MockPullRequestCommenterMockRecorder) ReregisterEntity(ctx, entType, props any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReregisterEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).ReregisterEntity), ctx, entType, props) +} + +// SupportsEntity mocks base method. +func (m *MockPullRequestCommenter) SupportsEntity(entType v10.Entity) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupportsEntity", entType) + ret0, _ := ret[0].(bool) + return ret0 +} + +// SupportsEntity indicates an expected call of SupportsEntity. +func (mr *MockPullRequestCommenterMockRecorder) SupportsEntity(entType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportsEntity", reflect.TypeOf((*MockPullRequestCommenter)(nil).SupportsEntity), entType) +} diff --git a/pkg/providers/v1/providers.go b/pkg/providers/v1/providers.go index 8d084f5b6e..f9a5af0503 100644 --- a/pkg/providers/v1/providers.go +++ b/pkg/providers/v1/providers.go @@ -225,6 +225,56 @@ type OCI interface { GetAuthenticator() (authn.Authenticator, error) } +// PullRequestCommentType is the type of the pull request comment +type PullRequestCommentType string + +const ( + // PullRequestCommentTypeApprove is the type for an approval + PullRequestCommentTypeApprove PullRequestCommentType = "approve" + // PullRequestCommentTypeRequestChanges is the type for a request for changes + PullRequestCommentTypeRequestChanges PullRequestCommentType = "request_changes" + // PullRequestCommentTypeComment is the type for a regular comment + PullRequestCommentTypeComment PullRequestCommentType = "comment" +) + +// PullRequestCommentInfo is the information for a pull request comment to +// be issued by the provider +type PullRequestCommentInfo struct { + // The commit sha for the pull request + Commit string `json:"commit,omitempty"` + // An optional header for the comment. If aggregating multiple comments, this + // could be used as a header. + Header string `json:"header,omitempty"` + // The comment body + Body string `json:"body,omitempty"` + // The priority of the comment. This is used to determine the order of the comments + // when aggregating multiple comments. Lower values are higher priority. + Priority int `json:"priority,omitempty"` + // The type of the comment + Type PullRequestCommentType `json:"type,omitempty"` + // +} + +// CommentResultMeta is the metadata for the comment result +type CommentResultMeta struct { + ID string `json:"review_id,omitempty"` + SubmittedAt time.Time `json:"submitted_at,omitempty"` + URL string `json:"pull_request_url,omitempty"` +} + +// PullRequestCommenter is the interface for commenting on pull requests +// The provider must implement this interface if it supports commenting on pull requests. +// Providers are assumed to support discovering the pull request by the properties +// as well as discovering the *one* comment they're supposed to work on. +// That is, the provider may issue one comment and aggregate multiple comments into one. +type PullRequestCommenter interface { + Provider + + // CommentOnPullRequest issues comments on a pull request + CommentOnPullRequest( + ctx context.Context, getByProps *properties.Properties, comment PullRequestCommentInfo) (*CommentResultMeta, error) +} + // ParseAndValidate parses the given provider configuration and validates it. func ParseAndValidate(rawConfig json.RawMessage, to any) error { if err := json.Unmarshal(rawConfig, to); err != nil {