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

mindersec / minder / 23861234921

01 Apr 2026 05:14PM UTC coverage: 58.247% (-0.04%) from 58.29%
23861234921

Pull #6218

github

web-flow
Merge bed25ae31 into cac62d57e
Pull Request #6218: Support reporting status checks and comments from PR rules

106 of 215 new or added lines in 3 files covered. (49.3%)

1 existing line in 1 file now uncovered.

19327 of 33181 relevant lines covered (58.25%)

35.91 hits per line

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

54.92
/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
        "context"
10
        "encoding/json"
11
        "fmt"
12
        "math"
13
        "time"
14

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

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

28
        uritemplate "github.com/std-uritemplate/std-uritemplate/go/v2"
29
)
30

31
const (
32
        // AlertType is the type of the commit status alert engine
33
        AlertType = "commit_status"
34
)
35

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

155
                _, err := alert.gh.SetCommitStatus(
2✔
156
                        ctx,
2✔
157
                        params.Owner,
2✔
158
                        params.Repo,
2✔
159
                        params.CommitSha,
2✔
160
                        commitStatus,
2✔
161
                )
2✔
162
                if err != nil {
3✔
163
                        return nil, fmt.Errorf("error setting commit status: %w, %w", err, enginerr.ErrActionFailed)
1✔
164
                }
1✔
165

166
                now := time.Now()
1✔
167
                newMeta, err := json.Marshal(alertMetadata{
1✔
168
                        SubmittedAt: &now,
1✔
169
                })
1✔
170
                if err != nil {
1✔
NEW
171
                        return nil, fmt.Errorf("error marshalling alert metadata json: %w", err)
×
NEW
172
                }
×
173

174
                logger.Info().Str("commit_sha", params.CommitSha).Msg("PR commit status updated to failure")
1✔
175
                return newMeta, nil
1✔
176

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

192
                _, err := alert.gh.SetCommitStatus(
1✔
193
                        ctx,
1✔
194
                        params.Owner,
1✔
195
                        params.Repo,
1✔
196
                        params.CommitSha,
1✔
197
                        commitStatus,
1✔
198
                )
1✔
199
                if err != nil {
1✔
NEW
200
                        return nil, fmt.Errorf("error dismissing commit status: %w, %w", err, enginerr.ErrActionFailed)
×
NEW
201
                }
×
202

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

NEW
207
        case interfaces.ActionCmdDoNothing:
×
NEW
208
                // Return the previous alert status.
×
NEW
209
                return alert.runDoNothing(ctx, params)
×
210
        }
NEW
211
        return nil, enginerr.ErrActionSkipped
×
212
}
213

214
// runDry runs the commit status action in dry run mode, logging what it would do
NEW
215
func (alert *Alert) runDry(ctx context.Context, params *paramsPR, cmd interfaces.ActionCmd, actionParams interfaces.ActionsParams) (json.RawMessage, error) {
×
NEW
216
        logger := zerolog.Ctx(ctx)
×
NEW
217

×
NEW
218
        switch cmd {
×
NEW
219
        case interfaces.ActionCmdOn:
×
NEW
220
                logger.Info().Msgf("dry run: set commit status to failure for context %s on PR %d in repo %s/%s",
×
NEW
221
                        params.Context, params.Number, params.Owner, params.Repo)
×
NEW
222
                return nil, nil
×
NEW
223
        case interfaces.ActionCmdOff:
×
NEW
224
                logger.Info().Msgf("dry run: set commit status to success for context %s on PR %d in repo %s/%s",
×
NEW
225
                        params.Context, params.Number, params.Owner, params.Repo)
×
NEW
226
        case interfaces.ActionCmdDoNothing:
×
NEW
227
                return alert.runDoNothing(ctx, params)
×
228
        }
NEW
229
        return nil, enginerr.ErrActionSkipped
×
230
}
231

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

×
NEW
237
        err := enginerr.AlertStatusAsError(params.prevStatus)
×
NEW
238
        if params.prevStatus != nil {
×
NEW
239
                return params.prevStatus.AlertMetadata, err
×
NEW
240
        }
×
NEW
241
        return nil, err
×
242
}
243

244
// getParamsForCommitStatus extracts the details from the entity
245
func (alert *Alert) getParamsForCommitStatus(
246
        ctx context.Context,
247
        pr *pbinternal.PullRequest,
248
        params interfaces.ActionsParams,
249
        metadata *json.RawMessage,
250
) (*paramsPR, error) {
3✔
251
        logger := zerolog.Ctx(ctx)
3✔
252
        result := &paramsPR{
3✔
253
                prevStatus: params.GetEvalStatusFromDb(),
3✔
254
                Owner:      pr.GetRepoOwner(),
3✔
255
                Repo:       pr.GetRepoName(),
3✔
256
                CommitSha:  pr.GetCommitSha(),
3✔
257
        }
3✔
258

3✔
259
        if pr.Number > math.MaxInt {
3✔
NEW
260
                return nil, fmt.Errorf("pr number is too large")
×
NEW
261
        }
×
262
        result.Number = int(pr.Number)
3✔
263

3✔
264
        // Create template params
3✔
265
        tmplParams := &CommitStatusTemplateParams{
3✔
266
                EvalErrorDetails: enginerr.ErrorAsEvalDetails(params.GetEvalErr()),
3✔
267
        }
3✔
268
        if params.GetEvalResult() != nil {
3✔
NEW
269
                tmplParams.EvalResultOutput = params.GetEvalResult().Output
×
NEW
270
        }
×
271
        if rule := params.GetRule(); rule != nil {
6✔
272
                tmplParams.RuleName = rule.Name
3✔
273
        }
3✔
274

275
        // 1. Evaluate context
276
        contextStr := "minder"
3✔
277
        contextNameTmpl := alert.alertCfg.GetStatusName()
3✔
278
        if contextNameTmpl != "" {
3✔
NEW
279
                // No templating specified for status_name, use as-is
×
NEW
280
                contextStr = contextNameTmpl
×
281
        } else if tmplParams.RuleName != "" {
6✔
282
                contextStr = fmt.Sprintf("minder/%s", tmplParams.RuleName)
3✔
283
        }
3✔
284
        result.Context = contextStr
3✔
285

3✔
286
        // 2. Evaluate description, using text/template
3✔
287
        descTmplStr := alert.alertCfg.GetDescription()
3✔
288
        if descTmplStr != "" {
3✔
NEW
289
                descTmpl, err := util.NewSafeHTMLTemplate(&descTmplStr, "description")
×
NEW
290
                if err != nil {
×
NEW
291
                        return nil, fmt.Errorf("cannot parse description template: %w", err)
×
NEW
292
                }
×
NEW
293
                descStr, err := descTmpl.Render(ctx, tmplParams, 1024)
×
NEW
294
                if err != nil {
×
NEW
295
                        return nil, fmt.Errorf("cannot execute description template: %w", err)
×
NEW
296
                }
×
NEW
297
                result.Description = descStr
×
298
        }
299

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

316
        if metadata != nil {
3✔
NEW
317
                meta := &alertMetadata{}
×
NEW
318
                err := json.Unmarshal(*metadata, meta)
×
NEW
319
                if err != nil {
×
NEW
320
                        logger.Debug().Msgf("error unmarshalling alert metadata: %v", err)
×
NEW
321
                } else {
×
NEW
322
                        result.Metadata = meta
×
NEW
323
                }
×
324
        }
325

326
        return result, nil
3✔
327
}
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