Skip to content

Commit 71abfd7

Browse files
committed
Add PullRequestCommenter provider trait
This allows for providers to comment on pull requests Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent d843a45 commit 71abfd7

File tree

6 files changed

+372
-174
lines changed

6 files changed

+372
-174
lines changed

internal/engine/actions/alert/alert.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func NewRuleAlert(
5353
if alertCfg.GetPullRequestComment() == nil {
5454
return nil, fmt.Errorf("alert engine missing pull_request_review configuration")
5555
}
56-
client, err := provinfv1.As[provinfv1.GitHub](provider)
56+
client, err := provinfv1.As[provinfv1.PullRequestCommenter](provider)
5757
if err != nil {
5858
zerolog.Ctx(ctx).Debug().Str("rule-type", ruletype.GetName()).
5959
Msg("provider is not a GitHub provider. Silently skipping alerts.")

internal/engine/actions/alert/pull_request_comment/pull_request_comment.go

+65-92
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ import (
1010
"encoding/json"
1111
"errors"
1212
"fmt"
13-
"math"
14-
"strconv"
15-
"time"
16-
1713
"github.com/google/go-github/v63/github"
14+
"github.com/mindersec/minder/internal/entities/properties"
1815
"github.com/rs/zerolog"
1916
"google.golang.org/protobuf/reflect/protoreflect"
2017

@@ -39,7 +36,7 @@ const (
3936
// Alert is the structure backing the noop alert
4037
type Alert struct {
4138
actionType interfaces.ActionType
42-
gh provifv1.GitHub
39+
commenter provifv1.PullRequestCommenter
4340
reviewCfg *pb.RuleType_Definition_Alert_AlertTypePRComment
4441
setting models.ActionOpt
4542
}
@@ -54,26 +51,17 @@ type PrCommentTemplateParams struct {
5451
}
5552

5653
type paramsPR struct {
57-
Owner string
58-
Repo string
59-
CommitSha string
60-
Number int
6154
Comment string
62-
Metadata *alertMetadata
55+
props *properties.Properties
56+
Metadata *provifv1.CommentResultMeta
6357
prevStatus *db.ListRuleEvaluationsByProfileIdRow
6458
}
6559

66-
type alertMetadata struct {
67-
ReviewID string `json:"review_id,omitempty"`
68-
SubmittedAt *time.Time `json:"submitted_at,omitempty"`
69-
PullRequestUrl *string `json:"pull_request_url,omitempty"`
70-
}
71-
7260
// NewPullRequestCommentAlert creates a new pull request comment alert action
7361
func NewPullRequestCommentAlert(
7462
actionType interfaces.ActionType,
7563
reviewCfg *pb.RuleType_Definition_Alert_AlertTypePRComment,
76-
gh provifv1.GitHub,
64+
gh provifv1.PullRequestCommenter,
7765
setting models.ActionOpt,
7866
) (*Alert, error) {
7967
if actionType == "" {
@@ -82,7 +70,7 @@ func NewPullRequestCommentAlert(
8270

8371
return &Alert{
8472
actionType: actionType,
85-
gh: gh,
73+
commenter: gh,
8674
reviewCfg: reviewCfg,
8775
setting: setting,
8876
}, nil
@@ -134,68 +122,14 @@ func (alert *Alert) Do(
134122
}
135123

136124
func (alert *Alert) run(ctx context.Context, params *paramsPR, cmd interfaces.ActionCmd) (json.RawMessage, error) {
137-
logger := zerolog.Ctx(ctx)
138-
139125
// Process the command
140126
switch cmd {
141-
// Create a review
142127
case interfaces.ActionCmdOn:
143-
review := &github.PullRequestReviewRequest{
144-
CommitID: github.String(params.CommitSha),
145-
Event: github.String("COMMENT"),
146-
Body: github.String(params.Comment),
147-
}
148-
149-
r, err := alert.gh.CreateReview(
150-
ctx,
151-
params.Owner,
152-
params.Repo,
153-
params.Number,
154-
review,
155-
)
156-
if err != nil {
157-
return nil, fmt.Errorf("error creating PR review: %w, %w", err, enginerr.ErrActionFailed)
158-
}
159-
160-
newMeta, err := json.Marshal(alertMetadata{
161-
ReviewID: strconv.FormatInt(r.GetID(), 10),
162-
SubmittedAt: r.SubmittedAt.GetTime(),
163-
PullRequestUrl: r.PullRequestURL,
164-
})
165-
if err != nil {
166-
return nil, fmt.Errorf("error marshalling alert metadata json: %w", err)
167-
}
168-
169-
logger.Info().Int64("review_id", *r.ID).Msg("PR review created")
170-
return newMeta, nil
171-
// Dismiss the review
128+
// Create a review
129+
return alert.runDoReview(ctx, params)
172130
case interfaces.ActionCmdOff:
173-
if params.Metadata == nil {
174-
// We cannot do anything without the PR review ID, so we assume that turning the alert off is a success
175-
return nil, fmt.Errorf("no PR comment ID provided: %w", enginerr.ErrActionTurnedOff)
176-
}
177-
178-
reviewID, err := strconv.ParseInt(params.Metadata.ReviewID, 10, 64)
179-
if err != nil {
180-
zerolog.Ctx(ctx).Error().Err(err).Str("review_id", params.Metadata.ReviewID).Msg("failed to parse review_id")
181-
return nil, fmt.Errorf("no PR comment ID provided: %w, %w", err, enginerr.ErrActionTurnedOff)
182-
}
183-
184-
_, err = alert.gh.DismissReview(ctx, params.Owner, params.Repo, params.Number, reviewID,
185-
&github.PullRequestReviewDismissalRequest{
186-
Message: github.String("Dismissed due to alert being turned off"),
187-
})
188-
if err != nil {
189-
if errors.Is(err, enginerr.ErrNotFound) {
190-
// There's no PR review with that ID anymore.
191-
// We exit by stating that the action was turned off.
192-
return nil, fmt.Errorf("PR comment already dismissed: %w, %w", err, enginerr.ErrActionTurnedOff)
193-
}
194-
return nil, fmt.Errorf("error dismissing PR comment: %w, %w", err, enginerr.ErrActionFailed)
195-
}
196-
logger.Info().Str("review_id", params.Metadata.ReviewID).Msg("PR comment dismissed")
197-
// Success - return ErrActionTurnedOff to indicate the action was successful
198-
return nil, fmt.Errorf("%s : %w", alert.Class(), enginerr.ErrActionTurnedOff)
131+
// Dismiss the review
132+
return runDismissReview(ctx, alert, params)
199133
case interfaces.ActionCmdDoNothing:
200134
// Return the previous alert status.
201135
return alert.runDoNothing(ctx, params)
@@ -211,16 +145,16 @@ func (alert *Alert) runDry(ctx context.Context, params *paramsPR, cmd interfaces
211145
switch cmd {
212146
case interfaces.ActionCmdOn:
213147
body := github.String(params.Comment)
214-
logger.Info().Msgf("dry run: create a PR comment on PR %d in repo %s/%s with the following body: %s",
215-
params.Number, params.Owner, params.Repo, *body)
148+
logger.Info().Dict("properties", params.props.ToLogDict()).
149+
Msgf("dry run: create a PR comment on PR with body: %s", *body)
216150
return nil, nil
217151
case interfaces.ActionCmdOff:
218152
if params.Metadata == nil {
219153
// We cannot do anything without the PR review ID, so we assume that turning the alert off is a success
220154
return nil, fmt.Errorf("no PR comment ID provided: %w", enginerr.ErrActionTurnedOff)
221155
}
222-
logger.Info().Msgf("dry run: dismiss PR comment %s on PR PR %d in repo %s/%s", params.Metadata.ReviewID,
223-
params.Number, params.Owner, params.Repo)
156+
logger.Info().Dict("properties", params.props.ToLogDict()).
157+
Msgf("dry run: dismiss PR comment on PR")
224158
case interfaces.ActionCmdDoNothing:
225159
// Return the previous alert status.
226160
return alert.runDoNothing(ctx, params)
@@ -231,7 +165,7 @@ func (alert *Alert) runDry(ctx context.Context, params *paramsPR, cmd interfaces
231165

232166
// runDoNothing returns the previous alert status
233167
func (_ *Alert) runDoNothing(ctx context.Context, params *paramsPR) (json.RawMessage, error) {
234-
logger := zerolog.Ctx(ctx).With().Str("repo", params.Repo).Logger()
168+
logger := zerolog.Ctx(ctx).With().Dict("properties", params.props.ToLogDict()).Logger()
235169

236170
logger.Debug().Msg("Running do nothing")
237171

@@ -245,6 +179,49 @@ func (_ *Alert) runDoNothing(ctx context.Context, params *paramsPR) (json.RawMes
245179
return nil, err
246180
}
247181

182+
func (alert *Alert) runDoReview(ctx context.Context, params *paramsPR) (json.RawMessage, error) {
183+
logger := zerolog.Ctx(ctx)
184+
185+
r, err := alert.commenter.CommentOnPullRequest(ctx, params.props, provifv1.PullRequestCommentInfo{
186+
// TODO: We should add the header to identify the alert. We could use the
187+
// rule type name.
188+
Commit: params.props.GetProperty(properties.PullRequestCommitSHA).GetString(),
189+
Body: params.Comment,
190+
// TODO: Determine the priority from the rule type severity
191+
})
192+
if err != nil {
193+
return nil, fmt.Errorf("error creating PR review: %w, %w", err, enginerr.ErrActionFailed)
194+
}
195+
logger.Info().Str("review_id", r.ID).Msg("PR review created")
196+
197+
// serialize r to json
198+
meta, err := json.Marshal(r)
199+
if err != nil {
200+
return nil, fmt.Errorf("error marshalling PR review metadata: %w", err)
201+
}
202+
203+
return meta, nil
204+
}
205+
206+
func runDismissReview(ctx context.Context, alert *Alert, params *paramsPR) (json.RawMessage, error) {
207+
if params.Metadata == nil {
208+
// We cannot do anything without the PR review ID, so we assume that turning the alert off is a success
209+
return nil, fmt.Errorf("no PR comment ID provided: %w", enginerr.ErrActionTurnedOff)
210+
}
211+
212+
err := alert.commenter.DismissPullRequestReview(ctx, params.props)
213+
if err != nil {
214+
if errors.Is(err, enginerr.ErrNotFound) {
215+
// There's no PR review with that ID anymore.
216+
// We exit by stating that the action was turned off.
217+
return nil, fmt.Errorf("PR comment already dismissed: %w, %w", err, enginerr.ErrActionTurnedOff)
218+
}
219+
return nil, fmt.Errorf("error dismissing PR comment: %w, %w", err, enginerr.ErrActionFailed)
220+
}
221+
// Success - return ErrActionTurnedOff to indicate the action was successful
222+
return nil, fmt.Errorf("%s : %w", alert.Class(), enginerr.ErrActionTurnedOff)
223+
}
224+
248225
// getParamsForSecurityAdvisory extracts the details from the entity
249226
func (alert *Alert) getParamsForPRComment(
250227
ctx context.Context,
@@ -253,19 +230,15 @@ func (alert *Alert) getParamsForPRComment(
253230
metadata *json.RawMessage,
254231
) (*paramsPR, error) {
255232
logger := zerolog.Ctx(ctx)
256-
result := &paramsPR{
257-
prevStatus: params.GetEvalStatusFromDb(),
258-
Owner: pr.GetRepoOwner(),
259-
Repo: pr.GetRepoName(),
260-
CommitSha: pr.GetCommitSha(),
233+
props, err := properties.NewProperties(pr.GetProperties().AsMap())
234+
if err != nil {
235+
return nil, fmt.Errorf("error creating properties: %w", err)
261236
}
262237

263-
// The GitHub Go API takes an int32, but our proto stores an int64; make sure we don't overflow
264-
// The PR number is an int in GitHub and Gitlab; in practice overflow will never happen.
265-
if pr.Number > math.MaxInt {
266-
return nil, fmt.Errorf("pr number is too large")
238+
result := &paramsPR{
239+
prevStatus: params.GetEvalStatusFromDb(),
240+
props: props,
267241
}
268-
result.Number = int(pr.Number)
269242

270243
commentTmpl, err := util.NewSafeHTMLTemplate(&alert.reviewCfg.ReviewMessage, "message")
271244
if err != nil {
@@ -289,7 +262,7 @@ func (alert *Alert) getParamsForPRComment(
289262

290263
// Unmarshal the existing alert metadata, if any
291264
if metadata != nil {
292-
meta := &alertMetadata{}
265+
meta := &provifv1.CommentResultMeta{}
293266
err := json.Unmarshal(*metadata, meta)
294267
if err != nil {
295268
// There's nothing saved apparently, so no need to fail here, but do log the error

0 commit comments

Comments
 (0)