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

mindersec / minder / 24073953802

07 Apr 2026 09:18AM UTC coverage: 58.45% (+0.2%) from 58.298%
24073953802

Pull #6218

github

web-flow
Merge 5995f2e3f into 59497f2fc
Pull Request #6218: Support reporting status checks and comments from PR rules

104 of 212 new or added lines in 3 files covered. (49.06%)

16 existing lines in 3 files now uncovered.

19409 of 33206 relevant lines covered (58.45%)

36.26 hits per line

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

54.74
/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/google/go-github/v63/github"
17
        "github.com/rs/zerolog"
18
        uritemplate "github.com/std-uritemplate/std-uritemplate/go/v2"
19
        "google.golang.org/protobuf/reflect/protoreflect"
20

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

32
const (
33
        // AlertType is the type of the commit status alert engine
34
        AlertType = "commit_status"
35
        // DescriptionMaxLength is the maximum length of the commit status description
36
        DescriptionMaxLength = 1024
37
)
38

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

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

52
        // EvalResult is the output of the evaluation, which may be empty
53
        EvalResultOutput any
54

55
        // RuleName is the name of the rule
56
        RuleName string
57
}
58

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

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

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

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

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

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

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

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

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

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

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

3✔
145
        switch cmd {
3✔
146
        // ActionCmdOn: The evaluation failed. Set status to failure.
147
        case interfaces.ActionCmdOn:
2✔
148
                commitStatus := &github.RepoStatus{
2✔
149
                        Context: github.String(params.Context),
2✔
150
                        State:   github.String("failure"),
2✔
151
                }
2✔
152
                if params.Description != "" {
2✔
NEW
153
                        commitStatus.Description = github.String(params.Description)
×
154
                } else {
2✔
155
                        commitStatus.Description = github.String("Minder evaluation failed")
2✔
156
                }
2✔
157
                if params.TargetURL != "" {
2✔
NEW
158
                        commitStatus.TargetURL = github.String(params.TargetURL)
×
NEW
159
                }
×
160

161
                _, err := alert.gh.SetCommitStatus(
2✔
162
                        ctx,
2✔
163
                        params.Owner,
2✔
164
                        params.Repo,
2✔
165
                        params.CommitSha,
2✔
166
                        commitStatus,
2✔
167
                )
2✔
168
                if err != nil {
3✔
169
                        return nil, fmt.Errorf("error setting commit status: %w, %w", err, enginerr.ErrActionFailed)
1✔
170
                }
1✔
171

172
                now := time.Now()
1✔
173
                newMeta, err := json.Marshal(alertMetadata{
1✔
174
                        SubmittedAt: &now,
1✔
175
                })
1✔
176
                if err != nil {
1✔
NEW
177
                        return nil, fmt.Errorf("error marshalling alert metadata json: %w", err)
×
NEW
178
                }
×
179

180
                logger.Info().Str("commit_sha", params.CommitSha).Msg("PR commit status updated to failure")
1✔
181
                return newMeta, nil
1✔
182

183
        // ActionCmdOff: The evaluation succeeded (or alert turned off). Set status to success.
184
        case interfaces.ActionCmdOff:
1✔
185
                commitStatus := &github.RepoStatus{
1✔
186
                        Context: github.String(params.Context),
1✔
187
                        State:   github.String("success"),
1✔
188
                }
1✔
189
                if params.Description != "" {
1✔
NEW
190
                        commitStatus.Description = github.String(params.Description)
×
191
                } else {
1✔
192
                        commitStatus.Description = github.String("Minder evaluation succeeded")
1✔
193
                }
1✔
194
                if params.TargetURL != "" {
1✔
NEW
195
                        commitStatus.TargetURL = github.String(params.TargetURL)
×
NEW
196
                }
×
197

198
                _, err := alert.gh.SetCommitStatus(
1✔
199
                        ctx,
1✔
200
                        params.Owner,
1✔
201
                        params.Repo,
1✔
202
                        params.CommitSha,
1✔
203
                        commitStatus,
1✔
204
                )
1✔
205
                if err != nil {
1✔
NEW
206
                        return nil, fmt.Errorf("error dismissing commit status: %w, %w", err, enginerr.ErrActionFailed)
×
NEW
207
                }
×
208

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

NEW
213
        case interfaces.ActionCmdDoNothing:
×
NEW
214
                // Return the previous alert status.
×
NEW
215
                return alert.runDoNothing(ctx, params)
×
216
        }
NEW
217
        return nil, enginerr.ErrActionSkipped
×
218
}
219

220
// runDry runs the commit status action in dry run mode, logging what it would do
221
func (alert *Alert) runDry(
222
        ctx context.Context, params *paramsPR,
223
        cmd interfaces.ActionCmd, _ interfaces.ActionsParams,
NEW
224
) (json.RawMessage, error) {
×
NEW
225
        logger := zerolog.Ctx(ctx)
×
NEW
226

×
NEW
227
        switch cmd {
×
NEW
228
        case interfaces.ActionCmdOn:
×
NEW
229
                logger.Info().Msgf("dry run: set commit status to failure for context %s on PR %d in repo %s/%s",
×
NEW
230
                        params.Context, params.Number, params.Owner, params.Repo)
×
NEW
231
                return nil, nil
×
NEW
232
        case interfaces.ActionCmdOff:
×
NEW
233
                logger.Info().Msgf("dry run: set commit status to success for context %s on PR %d in repo %s/%s",
×
NEW
234
                        params.Context, params.Number, params.Owner, params.Repo)
×
NEW
235
        case interfaces.ActionCmdDoNothing:
×
NEW
236
                return alert.runDoNothing(ctx, params)
×
237
        }
NEW
238
        return nil, enginerr.ErrActionSkipped
×
239
}
240

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

×
NEW
246
        err := dbadapter.AlertStatusAsError(params.prevStatus)
×
NEW
247
        if params.prevStatus != nil {
×
NEW
248
                return params.prevStatus.AlertMetadata, err
×
NEW
249
        }
×
NEW
250
        return nil, err
×
251
}
252

253
// getParamsForCommitStatus extracts the details from the entity
254
func (alert *Alert) getParamsForCommitStatus(
255
        ctx context.Context,
256
        pr *pbinternal.PullRequest,
257
        params interfaces.ActionsParams,
258
        metadata *json.RawMessage,
259
) (*paramsPR, error) {
3✔
260
        logger := zerolog.Ctx(ctx)
3✔
261
        result := &paramsPR{
3✔
262
                prevStatus: params.GetEvalStatusFromDb(),
3✔
263
                Owner:      pr.GetRepoOwner(),
3✔
264
                Repo:       pr.GetRepoName(),
3✔
265
                CommitSha:  pr.GetCommitSha(),
3✔
266
        }
3✔
267

3✔
268
        if pr.Number > math.MaxInt {
3✔
NEW
269
                return nil, fmt.Errorf("pr number is too large")
×
NEW
270
        }
×
271
        result.Number = int(pr.Number)
3✔
272

3✔
273
        // Create template params
3✔
274
        tmplParams := &CommitStatusTemplateParams{
3✔
275
                EvalErrorDetails: dbadapter.ErrorAsEvalDetails(params.GetEvalErr()),
3✔
276
        }
3✔
277
        if params.GetEvalResult() != nil {
3✔
NEW
278
                tmplParams.EvalResultOutput = params.GetEvalResult().Output
×
NEW
279
        }
×
280
        if rule := params.GetRule(); rule != nil {
6✔
281
                tmplParams.RuleName = rule.Name
3✔
282
        }
3✔
283

284
        // 1. Evaluate context
285
        contextStr := "minder"
3✔
286
        if tmplParams.RuleName != "" {
6✔
287
                contextStr = fmt.Sprintf("%s/%s", contextStr, tmplParams.RuleName)
3✔
288
        }
3✔
289
        result.Context = cmp.Or(alert.alertCfg.GetStatusName(), contextStr)
3✔
290

3✔
291
        // 2. Evaluate description, using text/template
3✔
292
        descTmplStr := alert.alertCfg.GetDescription()
3✔
293
        if descTmplStr != "" {
3✔
NEW
294
                descTmpl, err := util.NewSafeTextTemplate(&descTmplStr, "description")
×
NEW
295
                if err != nil {
×
NEW
296
                        return nil, fmt.Errorf("cannot parse description template: %w", err)
×
NEW
297
                }
×
NEW
298
                descStr, err := descTmpl.Render(ctx, tmplParams, DescriptionMaxLength)
×
NEW
299
                if err != nil {
×
NEW
300
                        return nil, fmt.Errorf("cannot execute description template: %w", err)
×
NEW
301
                }
×
NEW
302
                result.Description = descStr
×
303
        }
304

305
        // 3. Evaluate target_url, using RFC6570
306
        targetUrlTmplStr := alert.alertCfg.GetTargetUrl()
3✔
307
        if targetUrlTmplStr != "" {
3✔
NEW
308
                argsMap := map[string]any{
×
NEW
309
                        "owner":      result.Owner,
×
NEW
310
                        "repo":       result.Repo,
×
NEW
311
                        "commit_sha": result.CommitSha,
×
NEW
312
                        "rule_name":  tmplParams.RuleName,
×
NEW
313
                        "output":     tmplParams.EvalResultOutput,
×
NEW
314
                }
×
NEW
315
                expandedUrl, err := uritemplate.Expand(targetUrlTmplStr, argsMap)
×
NEW
316
                if err != nil {
×
NEW
317
                        return nil, fmt.Errorf("cannot expand target_url template: %w", err)
×
NEW
318
                }
×
NEW
319
                result.TargetURL = expandedUrl
×
320
        }
321

322
        if metadata != nil {
3✔
NEW
323
                meta := &alertMetadata{}
×
NEW
324
                err := json.Unmarshal(*metadata, meta)
×
NEW
325
                if err != nil {
×
NEW
326
                        logger.Debug().Msgf("error unmarshalling alert metadata: %v", err)
×
NEW
327
                } else {
×
NEW
328
                        result.Metadata = meta
×
NEW
329
                }
×
330
        }
331

332
        return result, nil
3✔
333
}
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