• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 12195161121

06 Dec 2024 08:03AM UTC coverage: 55.184% (-0.03%) from 55.217%
12195161121

push

github

web-flow
Add basic pull request comment alert type (#5133)

Add basic pull request review alert type

Ref https://github.com/mindersec/minder/issues/5117

86 of 172 new or added lines in 3 files covered. (50.0%)

1 existing line in 1 file now uncovered.

16569 of 30025 relevant lines covered (55.18%)

38.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.85
/internal/engine/actions/alert/pull_request_comment/pull_request_comment.go
1
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
// Package pull_request_comment provides necessary interfaces and implementations for
5
// processing pull request comment alerts.
6
package pull_request_comment
7

8
import (
9
        "context"
10
        "encoding/json"
11
        "errors"
12
        "fmt"
13
        "math"
14
        "strconv"
15
        "time"
16

17
        "github.com/google/go-github/v63/github"
18
        "github.com/rs/zerolog"
19
        "google.golang.org/protobuf/reflect/protoreflect"
20

21
        "github.com/mindersec/minder/internal/db"
22
        enginerr "github.com/mindersec/minder/internal/engine/errors"
23
        "github.com/mindersec/minder/internal/engine/interfaces"
24
        pbinternal "github.com/mindersec/minder/internal/proto"
25
        pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
26
        "github.com/mindersec/minder/pkg/profiles/models"
27
        provifv1 "github.com/mindersec/minder/pkg/providers/v1"
28
)
29

30
const (
31
        // AlertType is the type of the pull request comment alert engine
32
        AlertType = "pull_request_comment"
33
)
34

35
// Alert is the structure backing the noop alert
36
type Alert struct {
37
        actionType interfaces.ActionType
38
        gh         provifv1.GitHub
39
        reviewCfg  *pb.RuleType_Definition_Alert_AlertTypePRComment
40
        setting    models.ActionOpt
41
}
42

43
type paramsPR struct {
44
        Owner      string
45
        Repo       string
46
        CommitSha  string
47
        Number     int
48
        Metadata   *alertMetadata
49
        prevStatus *db.ListRuleEvaluationsByProfileIdRow
50
}
51

52
type alertMetadata struct {
53
        ReviewID       string     `json:"review_id,omitempty"`
54
        SubmittedAt    *time.Time `json:"submitted_at,omitempty"`
55
        PullRequestUrl *string    `json:"pull_request_url,omitempty"`
56
}
57

58
// NewPullRequestCommentAlert creates a new pull request comment alert action
59
func NewPullRequestCommentAlert(
60
        actionType interfaces.ActionType,
61
        reviewCfg *pb.RuleType_Definition_Alert_AlertTypePRComment,
62
        gh provifv1.GitHub,
63
        setting models.ActionOpt,
64
) (*Alert, error) {
3✔
65
        if actionType == "" {
3✔
NEW
66
                return nil, fmt.Errorf("action type cannot be empty")
×
NEW
67
        }
×
68

69
        return &Alert{
3✔
70
                actionType: actionType,
3✔
71
                gh:         gh,
3✔
72
                reviewCfg:  reviewCfg,
3✔
73
                setting:    setting,
3✔
74
        }, nil
3✔
75
}
76

77
// Class returns the action type of the PR comment alert engine
78
func (alert *Alert) Class() interfaces.ActionType {
1✔
79
        return alert.actionType
1✔
80
}
1✔
81

82
// Type returns the action subtype of the PR comment alert engine
NEW
83
func (_ *Alert) Type() string {
×
NEW
84
        return AlertType
×
NEW
85
}
×
86

87
// GetOnOffState returns the alert action state read from the profile
NEW
88
func (alert *Alert) GetOnOffState() models.ActionOpt {
×
NEW
89
        return models.ActionOptOrDefault(alert.setting, models.ActionOptOff)
×
NEW
90
}
×
91

92
// Do comments on a pull request
93
func (alert *Alert) Do(
94
        ctx context.Context,
95
        cmd interfaces.ActionCmd,
96
        entity protoreflect.ProtoMessage,
97
        params interfaces.ActionsParams,
98
        metadata *json.RawMessage,
99
) (json.RawMessage, error) {
3✔
100
        pr, ok := entity.(*pbinternal.PullRequest)
3✔
101
        if !ok {
3✔
NEW
102
                return nil, fmt.Errorf("expected repository, got %T", entity)
×
NEW
103
        }
×
104

105
        commentParams, err := getParamsForPRComment(ctx, pr, params, metadata)
3✔
106
        if err != nil {
3✔
NEW
107
                return nil, fmt.Errorf("error extracting parameters for PR comment: %w", err)
×
NEW
108
        }
×
109

110
        // Process the command based on the action setting
111
        switch alert.setting {
3✔
112
        case models.ActionOptOn:
3✔
113
                return alert.run(ctx, commentParams, cmd)
3✔
NEW
114
        case models.ActionOptDryRun:
×
NEW
115
                return alert.runDry(ctx, commentParams, cmd)
×
NEW
116
        case models.ActionOptOff, models.ActionOptUnknown:
×
NEW
117
                return nil, fmt.Errorf("unexpected action setting: %w", enginerr.ErrActionFailed)
×
118
        }
NEW
119
        return nil, enginerr.ErrActionSkipped
×
120
}
121

122
func (alert *Alert) run(ctx context.Context, params *paramsPR, cmd interfaces.ActionCmd) (json.RawMessage, error) {
3✔
123
        logger := zerolog.Ctx(ctx)
3✔
124

3✔
125
        // Process the command
3✔
126
        switch cmd {
3✔
127
        // Create a review
128
        case interfaces.ActionCmdOn:
2✔
129
                review := &github.PullRequestReviewRequest{
2✔
130
                        CommitID: github.String(params.CommitSha),
2✔
131
                        Event:    github.String("COMMENT"),
2✔
132
                        Body:     github.String(alert.reviewCfg.ReviewMessage),
2✔
133
                }
2✔
134

2✔
135
                r, err := alert.gh.CreateReview(
2✔
136
                        ctx,
2✔
137
                        params.Owner,
2✔
138
                        params.Repo,
2✔
139
                        params.Number,
2✔
140
                        review,
2✔
141
                )
2✔
142
                if err != nil {
3✔
143
                        return nil, fmt.Errorf("error creating PR review: %w, %w", err, enginerr.ErrActionFailed)
1✔
144
                }
1✔
145

146
                newMeta, err := json.Marshal(alertMetadata{
1✔
147
                        ReviewID:       strconv.FormatInt(r.GetID(), 10),
1✔
148
                        SubmittedAt:    r.SubmittedAt.GetTime(),
1✔
149
                        PullRequestUrl: r.PullRequestURL,
1✔
150
                })
1✔
151
                if err != nil {
1✔
NEW
152
                        return nil, fmt.Errorf("error marshalling alert metadata json: %w", err)
×
NEW
153
                }
×
154

155
                logger.Info().Int64("review_id", *r.ID).Msg("PR review created")
1✔
156
                return newMeta, nil
1✔
157
        // Dismiss the review
158
        case interfaces.ActionCmdOff:
1✔
159
                if params.Metadata == nil {
1✔
NEW
160
                        // We cannot do anything without the PR review ID, so we assume that turning the alert off is a success
×
NEW
161
                        return nil, fmt.Errorf("no PR comment ID provided: %w", enginerr.ErrActionTurnedOff)
×
NEW
162
                }
×
163

164
                reviewID, err := strconv.ParseInt(params.Metadata.ReviewID, 10, 64)
1✔
165
                if err != nil {
1✔
NEW
166
                        zerolog.Ctx(ctx).Error().Err(err).Str("review_id", params.Metadata.ReviewID).Msg("failed to parse review_id")
×
NEW
167
                        return nil, fmt.Errorf("no PR comment ID provided: %w, %w", err, enginerr.ErrActionTurnedOff)
×
NEW
168
                }
×
169

170
                _, err = alert.gh.DismissReview(ctx, params.Owner, params.Repo, params.Number, reviewID,
1✔
171
                        &github.PullRequestReviewDismissalRequest{
1✔
172
                                Message: github.String("Dismissed due to alert being turned off"),
1✔
173
                        })
1✔
174
                if err != nil {
1✔
NEW
175
                        if errors.Is(err, enginerr.ErrNotFound) {
×
NEW
176
                                // There's no PR review with that ID anymore.
×
NEW
177
                                // We exit by stating that the action was turned off.
×
NEW
178
                                return nil, fmt.Errorf("PR comment already dismissed: %w, %w", err, enginerr.ErrActionTurnedOff)
×
NEW
179
                        }
×
NEW
180
                        return nil, fmt.Errorf("error dismissing PR comment: %w, %w", err, enginerr.ErrActionFailed)
×
181
                }
182
                logger.Info().Str("review_id", params.Metadata.ReviewID).Msg("PR comment dismissed")
1✔
183
                // Success - return ErrActionTurnedOff to indicate the action was successful
1✔
184
                return nil, fmt.Errorf("%s : %w", alert.Class(), enginerr.ErrActionTurnedOff)
1✔
NEW
185
        case interfaces.ActionCmdDoNothing:
×
NEW
186
                // Return the previous alert status.
×
NEW
187
                return alert.runDoNothing(ctx, params)
×
188
        }
NEW
189
        return nil, enginerr.ErrActionSkipped
×
190
}
191

192
// runDry runs the pull request comment action in dry run mode, which logs the comment that would be made
NEW
193
func (alert *Alert) runDry(ctx context.Context, params *paramsPR, cmd interfaces.ActionCmd) (json.RawMessage, error) {
×
NEW
194
        logger := zerolog.Ctx(ctx)
×
NEW
195

×
NEW
196
        // Process the command
×
NEW
197
        switch cmd {
×
NEW
198
        case interfaces.ActionCmdOn:
×
NEW
199
                body := github.String(alert.reviewCfg.ReviewMessage)
×
NEW
200
                logger.Info().Msgf("dry run: create a PR comment on PR %d in repo %s/%s with the following body: %s",
×
NEW
201
                        params.Number, params.Owner, params.Repo, *body)
×
NEW
202
                return nil, nil
×
NEW
203
        case interfaces.ActionCmdOff:
×
NEW
204
                if params.Metadata == nil {
×
NEW
205
                        // We cannot do anything without the PR review ID, so we assume that turning the alert off is a success
×
NEW
206
                        return nil, fmt.Errorf("no PR comment ID provided: %w", enginerr.ErrActionTurnedOff)
×
NEW
207
                }
×
NEW
208
                logger.Info().Msgf("dry run: dismiss PR comment %s on PR PR %d in repo %s/%s", params.Metadata.ReviewID,
×
NEW
209
                        params.Number, params.Owner, params.Repo)
×
NEW
210
        case interfaces.ActionCmdDoNothing:
×
NEW
211
                // Return the previous alert status.
×
NEW
212
                return alert.runDoNothing(ctx, params)
×
213

214
        }
NEW
215
        return nil, enginerr.ErrActionSkipped
×
216
}
217

218
// runDoNothing returns the previous alert status
NEW
219
func (_ *Alert) runDoNothing(ctx context.Context, params *paramsPR) (json.RawMessage, error) {
×
NEW
220
        logger := zerolog.Ctx(ctx).With().Str("repo", params.Repo).Logger()
×
NEW
221

×
NEW
222
        logger.Debug().Msg("Running do nothing")
×
NEW
223

×
NEW
224
        // Return the previous alert status.
×
NEW
225
        err := enginerr.AlertStatusAsError(params.prevStatus)
×
NEW
226
        // If there is a valid alert metadata, return it too
×
NEW
227
        if params.prevStatus != nil {
×
NEW
228
                return params.prevStatus.AlertMetadata, err
×
NEW
229
        }
×
230
        // If there is no alert metadata, return nil as the metadata and the error
NEW
231
        return nil, err
×
232
}
233

234
// getParamsForSecurityAdvisory extracts the details from the entity
235
func getParamsForPRComment(
236
        ctx context.Context,
237
        pr *pbinternal.PullRequest,
238
        params interfaces.ActionsParams,
239
        metadata *json.RawMessage,
240
) (*paramsPR, error) {
3✔
241
        logger := zerolog.Ctx(ctx)
3✔
242
        result := &paramsPR{
3✔
243
                prevStatus: params.GetEvalStatusFromDb(),
3✔
244
                Owner:      pr.GetRepoOwner(),
3✔
245
                Repo:       pr.GetRepoName(),
3✔
246
                CommitSha:  pr.GetCommitSha(),
3✔
247
        }
3✔
248

3✔
249
        // The GitHub Go API takes an int32, but our proto stores an int64; make sure we don't overflow
3✔
250
        // The PR number is an int in GitHub and Gitlab; in practice overflow will never happen.
3✔
251
        if pr.Number > math.MaxInt {
3✔
NEW
252
                return nil, fmt.Errorf("pr number is too large")
×
NEW
253
        }
×
254
        result.Number = int(pr.Number)
3✔
255

3✔
256
        // Unmarshal the existing alert metadata, if any
3✔
257
        if metadata != nil {
4✔
258
                meta := &alertMetadata{}
1✔
259
                err := json.Unmarshal(*metadata, meta)
1✔
260
                if err != nil {
1✔
NEW
261
                        // There's nothing saved apparently, so no need to fail here, but do log the error
×
NEW
262
                        logger.Debug().Msgf("error unmarshalling alert metadata: %v", err)
×
263
                } else {
1✔
264
                        result.Metadata = meta
1✔
265
                }
1✔
266
        }
267

268
        return result, nil
3✔
269
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc