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

mindersec / minder / 23937706608

03 Apr 2026 07:10AM UTC coverage: 58.176% (-0.1%) from 58.315%
23937706608

Pull #6262

github

web-flow
Merge 1f1c47001 into d626305a2
Pull Request #6262: Decouple PR feedback actions from GitHub-specific provider

99 of 231 new or added lines in 5 files covered. (42.86%)

2 existing lines in 1 file now uncovered.

19307 of 33187 relevant lines covered (58.18%)

35.8 hits per line

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

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

4
// Package commit_status provides necessary interfaces and implementations for
5
// processing pull request commit status check alerts.
6
package commit_status
7

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

16
        "github.com/rs/zerolog"
17
        uritemplate "github.com/std-uritemplate/std-uritemplate/go/v2"
18
        "google.golang.org/protobuf/reflect/protoreflect"
19

20
        "github.com/mindersec/minder/internal/db"
21
        enginerr "github.com/mindersec/minder/internal/engine/errors"
22
        "github.com/mindersec/minder/internal/engine/interfaces"
23
        pbinternal "github.com/mindersec/minder/internal/proto"
24
        "github.com/mindersec/minder/internal/util"
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 commit status alert engine
32
        AlertType = "commit_status"
33
        // DescriptionMaxLength is the maximum length of the commit status description
34
        DescriptionMaxLength = 1024
35
)
36

37
// Alert is the structure backing the commit status alert
38
type Alert struct {
39
        actionType interfaces.ActionType
40
        gh         provifv1.CommitStatusPublisher
41
        alertCfg   *pb.RuleType_Definition_Alert_AlertTypeCommitStatus
42
        setting    models.ActionOpt
43
}
44

45
// CommitStatusTemplateParams is the parameters for the commit status templates
46
type CommitStatusTemplateParams struct {
47
        // EvalErrorDetails is the details of the error that occurred during evaluation, which may be empty
48
        EvalErrorDetails string
49

50
        // EvalResult is the output of the evaluation, which may be empty
51
        EvalResultOutput any
52

53
        // RuleName is the name of the rule
54
        RuleName string
55
}
56

57
type paramsPR struct {
58
        Owner       string
59
        Repo        string
60
        CommitSha   string
61
        Number      int
62
        Context     string
63
        Description string
64
        TargetURL   string
65
        Metadata    *alertMetadata
66
        prevStatus  *db.ListRuleEvaluationsByProfileIdRow
67
}
68

69
type alertMetadata struct {
70
        SubmittedAt *time.Time `json:"submitted_at,omitempty"`
71
}
72

73
// NewCommitStatusAlert creates a new commit status alert action
74
func NewCommitStatusAlert(
75
        actionType interfaces.ActionType,
76
        alertCfg *pb.RuleType_Definition_Alert_AlertTypeCommitStatus,
77
        gh provifv1.CommitStatusPublisher,
78
        setting models.ActionOpt,
79
) (*Alert, error) {
3✔
80
        if actionType == "" {
3✔
NEW
81
                return nil, fmt.Errorf("action type cannot be empty")
×
NEW
82
        }
×
83

84
        return &Alert{
3✔
85
                actionType: actionType,
3✔
86
                gh:         gh,
3✔
87
                alertCfg:   alertCfg,
3✔
88
                setting:    setting,
3✔
89
        }, nil
3✔
90
}
91

92
// Class returns the action type of the commit status alert engine
93
func (alert *Alert) Class() interfaces.ActionType {
1✔
94
        return alert.actionType
1✔
95
}
1✔
96

97
// Type returns the action subtype of the commit status alert engine
NEW
98
func (*Alert) Type() string {
×
NEW
99
        return AlertType
×
NEW
100
}
×
101

102
// GetOnOffState returns the alert action state read from the profile
NEW
103
func (alert *Alert) GetOnOffState() models.ActionOpt {
×
NEW
104
        return models.ActionOptOrDefault(alert.setting, models.ActionOptOff)
×
NEW
105
}
×
106

107
// Do sets the commit status on a pull request
108
func (alert *Alert) Do(
109
        ctx context.Context,
110
        cmd interfaces.ActionCmd,
111
        entity protoreflect.ProtoMessage,
112
        params interfaces.ActionsParams,
113
        metadata *json.RawMessage,
114
) (json.RawMessage, error) {
3✔
115
        pr, ok := entity.(*pbinternal.PullRequest)
3✔
116
        if !ok {
3✔
NEW
117
                return nil, fmt.Errorf("expected pull request, got %T", entity)
×
NEW
118
        }
×
119

120
        commitStatusParams, err := alert.getParamsForCommitStatus(ctx, pr, params, metadata)
3✔
121
        if err != nil {
3✔
NEW
122
                return nil, fmt.Errorf("error extracting parameters for commit status: %w", err)
×
NEW
123
        }
×
124

125
        // Process the command based on the action setting
126
        switch alert.setting {
3✔
127
        case models.ActionOptOn:
3✔
128
                return alert.run(ctx, commitStatusParams, cmd, params)
3✔
NEW
129
        case models.ActionOptDryRun:
×
NEW
130
                return alert.runDry(ctx, commitStatusParams, cmd, params)
×
NEW
131
        case models.ActionOptOff, models.ActionOptUnknown:
×
NEW
132
                return nil, fmt.Errorf("unexpected action setting: %w", enginerr.ErrActionFailed)
×
133
        }
NEW
134
        return nil, enginerr.ErrActionSkipped
×
135
}
136

137
func (alert *Alert) run(
138
        ctx context.Context, params *paramsPR,
139
        cmd interfaces.ActionCmd, _ interfaces.ActionsParams,
140
) (json.RawMessage, error) {
3✔
141
        logger := zerolog.Ctx(ctx)
3✔
142

3✔
143
        switch cmd {
3✔
144
        // ActionCmdOn: The evaluation failed. Set status to failure.
145
        case interfaces.ActionCmdOn:
2✔
146
                desc := params.Description
2✔
147
                if desc == "" {
4✔
148
                        desc = "Minder evaluation failed"
2✔
149
                }
2✔
150
                commitStatus := &provifv1.CommitStatus{
2✔
151
                        Context:     params.Context,
2✔
152
                        State:       provifv1.CommitStatusFailure,
2✔
153
                        Description: desc,
2✔
154
                        TargetURL:   params.TargetURL,
2✔
155
                }
2✔
156
                if err := alert.gh.PublishCommitStatus(ctx, params.Owner, params.Repo, params.CommitSha, commitStatus); err != nil {
3✔
157
                        return nil, fmt.Errorf("error setting commit status: %w, %w", err, enginerr.ErrActionFailed)
1✔
158
                }
1✔
159

160
                now := time.Now()
1✔
161
                newMeta, err := json.Marshal(alertMetadata{
1✔
162
                        SubmittedAt: &now,
1✔
163
                })
1✔
164
                if err != nil {
1✔
NEW
165
                        return nil, fmt.Errorf("error marshalling alert metadata json: %w", err)
×
NEW
166
                }
×
167

168
                logger.Info().Str("commit_sha", params.CommitSha).Msg("PR commit status updated to failure")
1✔
169
                return newMeta, nil
1✔
170

171
        // ActionCmdOff: The evaluation succeeded (or alert turned off). Set status to success.
172
        case interfaces.ActionCmdOff:
1✔
173
                desc := params.Description
1✔
174
                if desc == "" {
2✔
175
                        desc = "Minder evaluation succeeded"
1✔
176
                }
1✔
177
                commitStatus := &provifv1.CommitStatus{
1✔
178
                        Context:     params.Context,
1✔
179
                        State:       provifv1.CommitStatusSuccess,
1✔
180
                        Description: desc,
1✔
181
                        TargetURL:   params.TargetURL,
1✔
182
                }
1✔
183
                if err := alert.gh.PublishCommitStatus(ctx, params.Owner, params.Repo, params.CommitSha, commitStatus); err != nil {
1✔
NEW
184
                        return nil, fmt.Errorf("error dismissing commit status: %w, %w", err, enginerr.ErrActionFailed)
×
NEW
185
                }
×
186

187
                logger.Info().Str("commit_sha", params.CommitSha).Msg("PR commit status updated to success")
1✔
188
                // Return ErrActionTurnedOff to indicate the action resolved appropriately
1✔
189
                return nil, fmt.Errorf("%s : %w", alert.Class(), enginerr.ErrActionTurnedOff)
1✔
190

NEW
191
        case interfaces.ActionCmdDoNothing:
×
NEW
192
                // Return the previous alert status.
×
NEW
193
                return alert.runDoNothing(ctx, params)
×
194
        }
NEW
195
        return nil, enginerr.ErrActionSkipped
×
196
}
197

198
// runDry runs the commit status action in dry run mode, logging what it would do
199
func (alert *Alert) runDry(
200
        ctx context.Context, params *paramsPR,
201
        cmd interfaces.ActionCmd, _ interfaces.ActionsParams,
NEW
202
) (json.RawMessage, error) {
×
NEW
203
        logger := zerolog.Ctx(ctx)
×
NEW
204

×
NEW
205
        switch cmd {
×
NEW
206
        case interfaces.ActionCmdOn:
×
NEW
207
                logger.Info().Msgf("dry run: set commit status to failure for context %s on PR %d in repo %s/%s",
×
NEW
208
                        params.Context, params.Number, params.Owner, params.Repo)
×
NEW
209
                return nil, nil
×
NEW
210
        case interfaces.ActionCmdOff:
×
NEW
211
                logger.Info().Msgf("dry run: set commit status to success for context %s on PR %d in repo %s/%s",
×
NEW
212
                        params.Context, params.Number, params.Owner, params.Repo)
×
NEW
213
        case interfaces.ActionCmdDoNothing:
×
NEW
214
                return alert.runDoNothing(ctx, params)
×
215
        }
NEW
216
        return nil, enginerr.ErrActionSkipped
×
217
}
218

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

×
NEW
224
        err := enginerr.AlertStatusAsError(params.prevStatus)
×
NEW
225
        if params.prevStatus != nil {
×
NEW
226
                return params.prevStatus.AlertMetadata, err
×
NEW
227
        }
×
NEW
228
        return nil, err
×
229
}
230

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

3✔
246
        if pr.Number > math.MaxInt {
3✔
NEW
247
                return nil, fmt.Errorf("pr number is too large")
×
NEW
248
        }
×
249
        result.Number = int(pr.Number)
3✔
250

3✔
251
        // Create template params
3✔
252
        tmplParams := &CommitStatusTemplateParams{
3✔
253
                EvalErrorDetails: enginerr.ErrorAsEvalDetails(params.GetEvalErr()),
3✔
254
        }
3✔
255
        if params.GetEvalResult() != nil {
3✔
NEW
256
                tmplParams.EvalResultOutput = params.GetEvalResult().Output
×
NEW
257
        }
×
258
        if rule := params.GetRule(); rule != nil {
6✔
259
                tmplParams.RuleName = rule.Name
3✔
260
        }
3✔
261

262
        // 1. Evaluate context
263
        contextStr := "minder"
3✔
264
        if tmplParams.RuleName != "" {
6✔
265
                contextStr = fmt.Sprintf("%s/%s", contextStr, tmplParams.RuleName)
3✔
266
        }
3✔
267
        result.Context = cmp.Or(alert.alertCfg.GetStatusName(), contextStr)
3✔
268

3✔
269
        // 2. Evaluate description, using text/template
3✔
270
        descTmplStr := alert.alertCfg.GetDescription()
3✔
271
        if descTmplStr != "" {
3✔
NEW
272
                descTmpl, err := util.NewSafeTextTemplate(&descTmplStr, "description")
×
NEW
273
                if err != nil {
×
NEW
274
                        return nil, fmt.Errorf("cannot parse description template: %w", err)
×
NEW
275
                }
×
NEW
276
                descStr, err := descTmpl.Render(ctx, tmplParams, DescriptionMaxLength)
×
NEW
277
                if err != nil {
×
NEW
278
                        return nil, fmt.Errorf("cannot execute description template: %w", err)
×
NEW
279
                }
×
NEW
280
                result.Description = descStr
×
281
        }
282

283
        // 3. Evaluate target_url, using RFC6570
284
        targetUrlTmplStr := alert.alertCfg.GetTargetUrl()
3✔
285
        if targetUrlTmplStr != "" {
3✔
NEW
286
                argsMap := map[string]any{
×
NEW
287
                        "owner":      result.Owner,
×
NEW
288
                        "repo":       result.Repo,
×
NEW
289
                        "commit_sha": result.CommitSha,
×
NEW
290
                        "rule_name":  tmplParams.RuleName,
×
NEW
291
                        "output":     tmplParams.EvalResultOutput,
×
NEW
292
                }
×
NEW
293
                expandedUrl, err := uritemplate.Expand(targetUrlTmplStr, argsMap)
×
NEW
294
                if err != nil {
×
NEW
295
                        return nil, fmt.Errorf("cannot expand target_url template: %w", err)
×
NEW
296
                }
×
NEW
297
                result.TargetURL = expandedUrl
×
298
        }
299

300
        if metadata != nil {
3✔
NEW
301
                meta := &alertMetadata{}
×
NEW
302
                err := json.Unmarshal(*metadata, meta)
×
NEW
303
                if err != nil {
×
NEW
304
                        logger.Debug().Msgf("error unmarshalling alert metadata: %v", err)
×
NEW
305
                } else {
×
NEW
306
                        result.Metadata = meta
×
NEW
307
                }
×
308
        }
309

310
        return result, nil
3✔
311
}
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