• 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

61.19
/internal/providers/github/common.go
1
// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
// Package github provides a client for interacting with the GitHub API
5
package github
6

7
import (
8
        "bytes"
9
        "context"
10
        "errors"
11
        "fmt"
12
        "io"
13
        "net/http"
14
        "net/url"
15
        "sort"
16
        "strings"
17
        "time"
18

19
        backoffv4 "github.com/cenkalti/backoff/v4"
20
        "github.com/go-git/go-git/v5"
21
        "github.com/google/go-github/v63/github"
22
        "github.com/rs/zerolog"
23
        "golang.org/x/oauth2"
24
        "google.golang.org/protobuf/types/known/timestamppb"
25

26
        "github.com/mindersec/minder/internal/db"
27
        engerrors "github.com/mindersec/minder/internal/engine/errors"
28
        gitclient "github.com/mindersec/minder/internal/providers/git"
29
        "github.com/mindersec/minder/internal/providers/github/ghcr"
30
        "github.com/mindersec/minder/internal/providers/github/properties"
31
        "github.com/mindersec/minder/internal/providers/ratecache"
32
        minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
33
        config "github.com/mindersec/minder/pkg/config/server"
34
        provifv1 "github.com/mindersec/minder/pkg/providers/v1"
35
)
36

37
//go:generate go run go.uber.org/mock/mockgen -package mock_$GOPACKAGE -destination=./mock/$GOFILE -source=./$GOFILE
38

39
const (
40
        // ExpensiveRestCallTimeout is the timeout for expensive REST calls
41
        ExpensiveRestCallTimeout = 15 * time.Second
42
        // MaxRateLimitWait is the maximum time to wait for a rate limit to reset
43
        MaxRateLimitWait = 5 * time.Minute
44
        // MaxRateLimitRetries is the maximum number of retries for rate limit errors after waiting
45
        MaxRateLimitRetries = 1
46
        // DefaultRateLimitWaitTime is the default time to wait for a rate limit to reset
47
        DefaultRateLimitWaitTime = 1 * time.Minute
48

49
        githubBranchNotFoundMsg = "Branch not found"
50
)
51

52
var (
53
        // ErrNotFound denotes if the call returned a 404
54
        ErrNotFound = errors.New("not found")
55
        // ErrBranchNotFound denotes if the branch was not found
56
        ErrBranchNotFound = errors.New("branch not found")
57
        // ErrNoPackageListingClient denotes if there is no package listing client available
58
        ErrNoPackageListingClient = errors.New("no package listing client available")
59
        // ErroNoCheckPermissions is a fixed error returned when the credentialed
60
        // identity has not been authorized to use the checks API
61
        ErroNoCheckPermissions = errors.New("missing permissions: check")
62
        // ErrBranchNameEmpty is a fixed error returned when the branch name is empty
63
        ErrBranchNameEmpty = errors.New("branch name cannot be empty")
64
)
65

66
// GitHub is the struct that contains the shared GitHub client operations
67
type GitHub struct {
68
        client               *github.Client
69
        packageListingClient *github.Client
70
        cache                ratecache.RestClientCache
71
        delegate             Delegate
72
        ghcrwrap             *ghcr.ImageLister
73
        gitConfig            config.GitConfig
74
        webhookConfig        *config.WebhookConfig
75
        propertyFetchers     properties.GhPropertyFetcherFactory
76
}
77

78
// Ensure that the GitHub client implements the GitHub interface
79
var _ provifv1.GitHub = (*GitHub)(nil)
80

81
// Ensure that the GitHub client implements the CommitStatusPublisher interface
82
var _ provifv1.CommitStatusPublisher = (*GitHub)(nil)
83

84
// Ensure that the GitHub client implements the ReviewPublisher interface
85
var _ provifv1.ReviewPublisher = (*GitHub)(nil)
86

87
// ClientService is an interface for GitHub operations
88
// It is used to mock GitHub operations in tests, but in order to generate
89
// mocks, the interface must be exported
90
type ClientService interface {
91
        GetInstallation(ctx context.Context, id int64, jwt string) (*github.Installation, *github.Response, error)
92
        GetUserIdFromToken(ctx context.Context, token *oauth2.Token) (*int64, error)
93
        ListUserInstallations(ctx context.Context, token *oauth2.Token) ([]*github.Installation, error)
94
        DeleteInstallation(ctx context.Context, id int64, jwt string) (*github.Response, error)
95
        GetOrgMembership(ctx context.Context, token *oauth2.Token, org string) (*github.Membership, *github.Response, error)
96
}
97

98
var _ ClientService = (*ClientServiceImplementation)(nil)
99

100
// ClientServiceImplementation is the implementation of the ClientService interface
101
type ClientServiceImplementation struct{}
102

103
// GetInstallation is a wrapper for the GitHub API to get an installation
104
func (ClientServiceImplementation) GetInstallation(
105
        ctx context.Context,
106
        installationID int64,
107
        jwt string,
108
) (*github.Installation, *github.Response, error) {
×
109
        ghClient := github.NewClient(nil).WithAuthToken(jwt)
×
110
        return ghClient.Apps.GetInstallation(ctx, installationID)
×
111
}
×
112

113
// GetUserIdFromToken is a wrapper for the GitHub API to get the user id from a token
114
func (ClientServiceImplementation) GetUserIdFromToken(ctx context.Context, token *oauth2.Token) (*int64, error) {
×
115
        ghClient := github.NewClient(nil).WithAuthToken(token.AccessToken)
×
116

×
117
        user, _, err := ghClient.Users.Get(ctx, "")
×
118
        if err != nil {
×
119
                return nil, err
×
120
        }
×
121

122
        return user.ID, nil
×
123
}
124

125
// ListUserInstallations is a wrapper for the GitHub API to list user installations
126
func (ClientServiceImplementation) ListUserInstallations(
127
        ctx context.Context, token *oauth2.Token,
128
) ([]*github.Installation, error) {
×
129
        ghClient := github.NewClient(nil).WithAuthToken(token.AccessToken)
×
130

×
131
        installations, _, err := ghClient.Apps.ListUserInstallations(ctx, nil)
×
132
        return installations, err
×
133
}
×
134

135
// DeleteInstallation is a wrapper for the GitHub API to delete an installation
136
func (ClientServiceImplementation) DeleteInstallation(ctx context.Context, id int64, jwt string) (*github.Response, error) {
×
137
        ghClient := github.NewClient(nil).WithAuthToken(jwt)
×
138
        return ghClient.Apps.DeleteInstallation(ctx, id)
×
139
}
×
140

141
// GetOrgMembership is a wrapper for the GitHub API to get users' organization membership
142
func (ClientServiceImplementation) GetOrgMembership(
143
        ctx context.Context, token *oauth2.Token, org string,
144
) (*github.Membership, *github.Response, error) {
×
145
        ghClient := github.NewClient(nil).WithAuthToken(token.AccessToken)
×
146
        return ghClient.Organizations.GetOrgMembership(ctx, "", org)
×
147
}
×
148

149
// Delegate is the interface that contains operations that differ between different GitHub actors (user vs app)
150
type Delegate interface {
151
        GetCredential() provifv1.GitHubCredential
152
        ListAllRepositories(context.Context) ([]*minderv1.Repository, error)
153
        GetUserId(ctx context.Context) (int64, error)
154
        GetName(ctx context.Context) (string, error)
155
        GetLogin(ctx context.Context) (string, error)
156
        GetPrimaryEmail(ctx context.Context) (string, error)
157
        GetOwner() string
158
        IsOrg() bool
159
}
160

161
// NewGitHub creates a new GitHub client
162
func NewGitHub(
163
        client *github.Client,
164
        packageListingClient *github.Client,
165
        cache ratecache.RestClientCache,
166
        delegate Delegate,
167
        cfg *config.ProviderConfig,
168
        whcfg *config.WebhookConfig,
169
        propertyFetchers properties.GhPropertyFetcherFactory,
170
) *GitHub {
50✔
171
        var gitConfig config.GitConfig
50✔
172
        if cfg != nil {
59✔
173
                gitConfig = cfg.Git
9✔
174
        }
9✔
175
        return &GitHub{
50✔
176
                client:               client,
50✔
177
                packageListingClient: packageListingClient,
50✔
178
                cache:                cache,
50✔
179
                delegate:             delegate,
50✔
180
                ghcrwrap:             ghcr.FromGitHubClient(client, delegate.GetOwner()),
50✔
181
                gitConfig:            gitConfig,
50✔
182
                webhookConfig:        whcfg,
50✔
183
                propertyFetchers:     propertyFetchers,
50✔
184
        }
50✔
185
}
186

187
// CanImplement returns true/false depending on whether the Provider
188
// can implement the specified trait
189
func (*GitHub) CanImplement(trait minderv1.ProviderType) bool {
×
190
        return trait == minderv1.ProviderType_PROVIDER_TYPE_GITHUB ||
×
191
                trait == minderv1.ProviderType_PROVIDER_TYPE_GIT ||
×
192
                trait == minderv1.ProviderType_PROVIDER_TYPE_REST ||
×
193
                trait == minderv1.ProviderType_PROVIDER_TYPE_REPO_LISTER
×
194
}
×
195

196
// ListPackagesByRepository returns a list of all packages for a specific repository
197
func (c *GitHub) ListPackagesByRepository(
198
        ctx context.Context,
199
        owner string,
200
        artifactType string,
201
        repositoryId int64,
202
        pageNumber int,
203
        itemsPerPage int,
204
) ([]*github.Package, error) {
4✔
205
        opt := &github.PackageListOptions{
4✔
206
                PackageType: &artifactType,
4✔
207
                ListOptions: github.ListOptions{
4✔
208
                        Page:    pageNumber,
4✔
209
                        PerPage: itemsPerPage,
4✔
210
                },
4✔
211
        }
4✔
212
        // create a slice to hold the containers
4✔
213
        var allContainers []*github.Package
4✔
214

4✔
215
        if c.packageListingClient == nil {
5✔
216
                zerolog.Ctx(ctx).Error().Msg("No client available for listing packages")
1✔
217
                return allContainers, ErrNoPackageListingClient
1✔
218
        }
1✔
219

220
        type listPackagesRespWrapper struct {
3✔
221
                artifacts []*github.Package
3✔
222
                resp      *github.Response
3✔
223
        }
3✔
224
        op := func() (listPackagesRespWrapper, error) {
6✔
225
                var artifacts []*github.Package
3✔
226
                var resp *github.Response
3✔
227
                var err error
3✔
228

3✔
229
                if c.IsOrg() {
6✔
230
                        artifacts, resp, err = c.packageListingClient.Organizations.ListPackages(ctx, owner, opt)
3✔
231
                } else {
3✔
232
                        artifacts, resp, err = c.packageListingClient.Users.ListPackages(ctx, owner, opt)
×
233
                }
×
234

235
                listPackagesResp := listPackagesRespWrapper{
3✔
236
                        artifacts: artifacts,
3✔
237
                        resp:      resp,
3✔
238
                }
3✔
239

3✔
240
                if isRateLimitError(err) {
3✔
241
                        waitErr := c.waitForRateLimitReset(ctx, err)
×
242
                        if waitErr == nil {
×
243
                                return listPackagesResp, err
×
244
                        }
×
245
                        return listPackagesResp, backoffv4.Permanent(err)
×
246
                }
247

248
                return listPackagesResp, backoffv4.Permanent(err)
3✔
249
        }
250

251
        for {
6✔
252
                result, err := performWithRetry(ctx, op)
3✔
253
                if err != nil {
3✔
254
                        if result.resp != nil && result.resp.StatusCode == http.StatusNotFound {
×
255
                                return allContainers, fmt.Errorf("packages not found for repository %d: %w", repositoryId, ErrNotFound)
×
256
                        }
×
257

258
                        return allContainers, err
×
259
                }
260

261
                // now just append the ones belonging to the repository
262
                for _, artifact := range result.artifacts {
7✔
263
                        if artifact.Repository.GetID() == repositoryId {
7✔
264
                                allContainers = append(allContainers, artifact)
3✔
265
                        }
3✔
266
                }
267

268
                if result.resp.NextPage == 0 {
6✔
269
                        break
3✔
270
                }
271
                opt.Page = result.resp.NextPage
×
272
        }
273

274
        return allContainers, nil
3✔
275
}
276

277
// getPackageVersions returns a list of all package versions for the authenticated user or org
278
func (c *GitHub) getPackageVersions(ctx context.Context, owner string, package_type string, package_name string,
279
) ([]*github.PackageVersion, error) {
4✔
280
        state := "active"
4✔
281

4✔
282
        // since the GH API sometimes returns container and sometimes CONTAINER as the type, let's just lowercase it
4✔
283
        package_type = strings.ToLower(package_type)
4✔
284

4✔
285
        opt := &github.PackageListOptions{
4✔
286
                PackageType: &package_type,
4✔
287
                State:       &state,
4✔
288
                ListOptions: github.ListOptions{
4✔
289
                        Page:    1,
4✔
290
                        PerPage: 100,
4✔
291
                },
4✔
292
        }
4✔
293

4✔
294
        // create a slice to hold the versions
4✔
295
        var allVersions []*github.PackageVersion
4✔
296

4✔
297
        // loop until we get all package versions
4✔
298
        for {
8✔
299
                var v []*github.PackageVersion
4✔
300
                var resp *github.Response
4✔
301
                var err error
4✔
302
                if c.IsOrg() {
8✔
303
                        v, resp, err = c.client.Organizations.PackageGetAllVersions(ctx, owner, package_type, package_name, opt)
4✔
304
                } else {
4✔
305
                        package_name = url.PathEscape(package_name)
×
306
                        v, resp, err = c.client.Users.PackageGetAllVersions(ctx, owner, package_type, package_name, opt)
×
307
                }
×
308
                if err != nil {
5✔
309
                        return nil, err
1✔
310
                }
1✔
311

312
                // append to the slice
313
                allVersions = append(allVersions, v...)
3✔
314

3✔
315
                // if there is no next page, break
3✔
316
                if resp.NextPage == 0 {
6✔
317
                        break
3✔
318
                }
319

320
                // update the page
321
                opt.Page = resp.NextPage
×
322
        }
323

324
        // return the slice
325
        return allVersions, nil
3✔
326
}
327

328
// GetPackageByName returns a single package for the authenticated user or for the org
329
func (c *GitHub) GetPackageByName(ctx context.Context, owner string, package_type string, package_name string,
330
) (*github.Package, error) {
2✔
331
        var pkg *github.Package
2✔
332
        var err error
2✔
333

2✔
334
        // since the GH API sometimes returns container and sometimes CONTAINER as the type, let's just lowercase it
2✔
335
        package_type = strings.ToLower(package_type)
2✔
336

2✔
337
        if c.IsOrg() {
4✔
338
                pkg, _, err = c.client.Organizations.GetPackage(ctx, owner, package_type, package_name)
2✔
339
                if err != nil {
2✔
340
                        return nil, err
×
341
                }
×
342
        } else {
×
343
                pkg, _, err = c.client.Users.GetPackage(ctx, "", package_type, package_name)
×
344
                if err != nil {
×
345
                        return nil, err
×
346
                }
×
347
        }
348
        return pkg, nil
2✔
349
}
350

351
// GetPackageVersionById returns a single package version for the specific id
352
func (c *GitHub) GetPackageVersionById(ctx context.Context, owner string, packageType string, packageName string,
353
        version int64) (*github.PackageVersion, error) {
2✔
354
        var pkgVersion *github.PackageVersion
2✔
355
        var err error
2✔
356

2✔
357
        if c.IsOrg() {
4✔
358
                pkgVersion, _, err = c.client.Organizations.PackageGetVersion(ctx, owner, packageType, packageName, version)
2✔
359
                if err != nil {
2✔
360
                        return nil, err
×
361
                }
×
362
        } else {
×
363
                packageName = url.PathEscape(packageName)
×
364
                pkgVersion, _, err = c.client.Users.PackageGetVersion(ctx, owner, packageType, packageName, version)
×
365
                if err != nil {
×
366
                        return nil, err
×
367
                }
×
368
        }
369

370
        return pkgVersion, nil
2✔
371
}
372

373
// GetPullRequest is a wrapper for the GitHub API to get a pull request
374
func (c *GitHub) GetPullRequest(
375
        ctx context.Context,
376
        owner string,
377
        repo string,
378
        number int,
379
) (*github.PullRequest, error) {
×
380
        pr, _, err := c.client.PullRequests.Get(ctx, owner, repo, number)
×
381
        if err != nil {
×
382
                return nil, err
×
383
        }
×
384
        return pr, nil
×
385
}
386

387
// ListFiles is a wrapper for the GitHub API to list files in a pull request
388
func (c *GitHub) ListFiles(
389
        ctx context.Context,
390
        owner string,
391
        repo string,
392
        prNumber int,
393
        perPage int,
394
        pageNumber int,
395
) ([]*github.CommitFile, *github.Response, error) {
3✔
396
        type listFilesRespWrapper struct {
3✔
397
                files []*github.CommitFile
3✔
398
                resp  *github.Response
3✔
399
        }
3✔
400

3✔
401
        op := func() (listFilesRespWrapper, error) {
7✔
402
                opt := &github.ListOptions{
4✔
403
                        Page:    pageNumber,
4✔
404
                        PerPage: perPage,
4✔
405
                }
4✔
406
                files, resp, err := c.client.PullRequests.ListFiles(ctx, owner, repo, prNumber, opt)
4✔
407

4✔
408
                listFileResp := listFilesRespWrapper{
4✔
409
                        files: files,
4✔
410
                        resp:  resp,
4✔
411
                }
4✔
412

4✔
413
                if isRateLimitError(err) {
5✔
414
                        waitErr := c.waitForRateLimitReset(ctx, err)
1✔
415
                        if waitErr == nil {
2✔
416
                                return listFileResp, err
1✔
417
                        }
1✔
418
                        return listFileResp, backoffv4.Permanent(err)
×
419
                }
420

421
                return listFileResp, backoffv4.Permanent(err)
3✔
422
        }
423

424
        resp, err := performWithRetry(ctx, op)
3✔
425
        return resp.files, resp.resp, err
3✔
426
}
427

428
// CreateReview is a wrapper for the GitHub API to create a review
429
func (c *GitHub) CreateReview(
430
        ctx context.Context, owner, repo string, number int, reviewRequest *github.PullRequestReviewRequest,
431
) (*github.PullRequestReview, error) {
×
432
        review, _, err := c.client.PullRequests.CreateReview(ctx, owner, repo, number, reviewRequest)
×
433
        if err != nil {
×
434
                return nil, fmt.Errorf("error creating review: %w", err)
×
435
        }
×
436
        return review, nil
×
437
}
438

439
// UpdateReview is a wrapper for the GitHub API to update a review
440
func (c *GitHub) UpdateReview(
441
        ctx context.Context, owner, repo string, number int, reviewId int64, body string,
442
) (*github.PullRequestReview, error) {
×
443
        review, _, err := c.client.PullRequests.UpdateReview(ctx, owner, repo, number, reviewId, body)
×
444
        if err != nil {
×
445
                return nil, fmt.Errorf("error updating review: %w", err)
×
446
        }
×
447
        return review, nil
×
448
}
449

450
// ListIssueComments is a wrapper for the GitHub API to get all comments in a review
451
func (c *GitHub) ListIssueComments(
452
        ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions,
453
) ([]*github.IssueComment, error) {
×
454
        comments, _, err := c.client.Issues.ListComments(ctx, owner, repo, number, opts)
×
455
        if err != nil {
×
456
                return nil, fmt.Errorf("error getting list of comments: %w", err)
×
457
        }
×
458
        return comments, nil
×
459
}
460

461
// ListReviews is a wrapper for the GitHub API to list reviews
462
func (c *GitHub) ListReviews(
463
        ctx context.Context,
464
        owner, repo string,
465
        number int,
466
        opt *github.ListOptions,
467
) ([]*github.PullRequestReview, error) {
×
468
        reviews, _, err := c.client.PullRequests.ListReviews(ctx, owner, repo, number, opt)
×
469
        if err != nil {
×
470
                return nil, fmt.Errorf("error listing reviews for PR %s/%s/%d: %w", owner, repo, number, err)
×
471
        }
×
472
        return reviews, nil
×
473
}
474

475
// DismissReview is a wrapper for the GitHub API to dismiss a review
476
func (c *GitHub) DismissReview(
477
        ctx context.Context,
478
        owner, repo string,
479
        prId int,
480
        reviewId int64,
481
        dismissalRequest *github.PullRequestReviewDismissalRequest,
482
) (*github.PullRequestReview, error) {
×
483
        review, _, err := c.client.PullRequests.DismissReview(ctx, owner, repo, prId, reviewId, dismissalRequest)
×
484
        if err != nil {
×
485
                return nil, fmt.Errorf("error dismissing review %d for PR %s/%s/%d: %w", reviewId, owner, repo, prId, err)
×
486
        }
×
487
        return review, nil
×
488
}
489

490
// SetCommitStatus is a wrapper for the GitHub API to set a commit status
491
func (c *GitHub) SetCommitStatus(
492
        ctx context.Context, owner, repo, ref string, status *github.RepoStatus,
493
) (*github.RepoStatus, error) {
×
494
        status, _, err := c.client.Repositories.CreateStatus(ctx, owner, repo, ref, status)
×
495
        if err != nil {
×
496
                return nil, fmt.Errorf("error creating commit status: %w", err)
×
497
        }
×
498
        return status, nil
×
499
}
500

501
// PublishCommitStatus implements CommitStatusPublisher by translating the generic
502
// CommitStatus DTO to a GitHub-native RepoStatus and calling the GitHub API.
503
func (c *GitHub) PublishCommitStatus(
504
        ctx context.Context, owner, repo, ref string, status *provifv1.CommitStatus,
NEW
505
) error {
×
NEW
506
        ghStatus := &github.RepoStatus{
×
NEW
507
                State:       github.String(string(status.State)),
×
NEW
508
                Description: github.String(status.Description),
×
NEW
509
                Context:     github.String(status.Context),
×
NEW
510
        }
×
NEW
511
        if status.TargetURL != "" {
×
NEW
512
                ghStatus.TargetURL = github.String(status.TargetURL)
×
NEW
513
        }
×
NEW
514
        _, err := c.SetCommitStatus(ctx, owner, repo, ref, ghStatus)
×
NEW
515
        return err
×
516
}
517

518
// PublishReview implements ReviewPublisher by translating the generic Review DTO
519
// to a GitHub-native PullRequestReviewRequest and calling the GitHub API.
520
func (c *GitHub) PublishReview(
521
        ctx context.Context, owner, repo string, prNumber int, review *provifv1.Review,
NEW
522
) error {
×
NEW
523
        req := &github.PullRequestReviewRequest{
×
NEW
524
                Body:  github.String(review.Body),
×
NEW
525
                Event: github.String("COMMENT"),
×
NEW
526
        }
×
NEW
527
        _, err := c.CreateReview(ctx, owner, repo, prNumber, req)
×
NEW
528
        return err
×
NEW
529
}
×
530

531
// DismissPublishedReview implements ReviewPublisher by translating the generic dismiss call
532
// to a GitHub-native PullRequestReviewDismissalRequest.
533
func (c *GitHub) DismissPublishedReview(
534
        ctx context.Context, owner, repo string, prNumber int, reviewID int64, message string,
NEW
535
) error {
×
NEW
536
        _, _, err := c.client.PullRequests.DismissReview(ctx, owner, repo, prNumber, reviewID,
×
NEW
537
                &github.PullRequestReviewDismissalRequest{
×
NEW
538
                        Message: github.String(message),
×
NEW
539
                })
×
NEW
540
        if err != nil {
×
NEW
541
                return fmt.Errorf("error dismissing review %d for PR %s/%s/%d: %w", reviewID, owner, repo, prNumber, err)
×
NEW
542
        }
×
NEW
543
        return nil
×
544
}
545

546
// GetRepository returns a single repository for the authenticated user
547
func (c *GitHub) GetRepository(ctx context.Context, owner string, name string) (*github.Repository, error) {
×
548
        // create a slice to hold the repositories
×
549
        repo, _, err := c.client.Repositories.Get(ctx, owner, name)
×
550
        if err != nil {
×
551
                return nil, fmt.Errorf("error getting repository: %w", err)
×
552
        }
×
553

554
        return repo, nil
×
555
}
556

557
// GetBranchProtection returns the branch protection for a given branch
558
func (c *GitHub) GetBranchProtection(ctx context.Context, owner string,
559
        repo_name string, branch_name string) (*github.Protection, error) {
×
560
        var respErr *github.ErrorResponse
×
561
        if branch_name == "" {
×
562
                return nil, ErrBranchNameEmpty
×
563
        }
×
564

565
        protection, _, err := c.client.Repositories.GetBranchProtection(ctx, owner, repo_name, branch_name)
×
566
        if errors.As(err, &respErr) {
×
567
                if respErr.Message == githubBranchNotFoundMsg {
×
568
                        return nil, ErrBranchNotFound
×
569
                }
×
570

571
                return nil, fmt.Errorf("error getting branch protection: %w", err)
×
572
        } else if err != nil {
×
573
                return nil, err
×
574
        }
×
575
        return protection, nil
×
576
}
577

578
// UpdateBranchProtection updates the branch protection for a given branch
579
func (c *GitHub) UpdateBranchProtection(
580
        ctx context.Context, owner, repo, branch string, preq *github.ProtectionRequest,
581
) error {
×
582
        if branch == "" {
×
583
                return ErrBranchNameEmpty
×
584
        }
×
585
        _, _, err := c.client.Repositories.UpdateBranchProtection(ctx, owner, repo, branch, preq)
×
586
        return err
×
587
}
588

589
// GetBaseURL returns the base URL for the REST API.
590
func (c *GitHub) GetBaseURL() string {
1✔
591
        return c.client.BaseURL.String()
1✔
592
}
1✔
593

594
// NewRequest creates an API request. A relative URL can be provided in urlStr,
595
// which will be resolved to the BaseURL of the Client. Relative URLS should
596
// always be specified without a preceding slash. If specified, the value
597
// pointed to by body is JSON encoded and included as the request body.
598
func (c *GitHub) NewRequest(method, requestUrl string, body any) (*http.Request, error) {
9✔
599
        return c.client.NewRequest(method, requestUrl, body)
9✔
600
}
9✔
601

602
// Do sends an API request and returns the API response.
603
func (c *GitHub) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
9✔
604
        var buf bytes.Buffer
9✔
605

9✔
606
        // The GitHub client closes the response body, so we need to capture it
9✔
607
        // in a buffer so that we can return it to the caller
9✔
608
        resp, err := c.client.Do(ctx, req, &buf)
9✔
609
        if err != nil && resp == nil {
9✔
610
                return nil, err
×
611
        }
×
612

613
        if resp.Response != nil {
18✔
614
                resp.Body = io.NopCloser(&buf)
9✔
615
        }
9✔
616
        return resp.Response, err
9✔
617
}
618

619
// GetCredential returns the credential used to authenticate with the GitHub API
620
func (c *GitHub) GetCredential() provifv1.GitHubCredential {
×
621
        return c.delegate.GetCredential()
×
622
}
×
623

624
// IsOrg returns true if the owner is an organization
625
func (c *GitHub) IsOrg() bool {
11✔
626
        return c.delegate.IsOrg()
11✔
627
}
11✔
628

629
// ListHooks lists all Hooks for the specified repository.
630
func (c *GitHub) ListHooks(ctx context.Context, owner, repo string) ([]*github.Hook, error) {
×
631
        list, resp, err := c.client.Repositories.ListHooks(ctx, owner, repo, nil)
×
632
        if err != nil && resp.StatusCode == http.StatusNotFound {
×
633
                // return empty list so that the caller can ignore the error and iterate over the empty list
×
634
                return []*github.Hook{}, fmt.Errorf("hooks not found for repository %s/%s: %w", owner, repo, ErrNotFound)
×
635
        }
×
636
        return list, err
×
637
}
638

639
// DeleteHook deletes a specified Hook.
640
func (c *GitHub) DeleteHook(ctx context.Context, owner, repo string, id int64) error {
×
641
        resp, err := c.client.Repositories.DeleteHook(ctx, owner, repo, id)
×
642
        if resp != nil && resp.StatusCode == http.StatusNotFound {
×
643
                // If the hook is not found, we can ignore the
×
644
                // error, user might have deleted it manually.
×
645
                return nil
×
646
        }
×
647
        if resp != nil && resp.StatusCode == http.StatusForbidden {
×
648
                // We ignore deleting webhooks that we're not
×
649
                // allowed to touch. This is usually the case
×
650
                // with repository transfer.
×
651
                return nil
×
652
        }
×
653
        return err
×
654
}
655

656
// CreateHook creates a new Hook.
657
func (c *GitHub) CreateHook(ctx context.Context, owner, repo string, hook *github.Hook) (*github.Hook, error) {
4✔
658
        h, _, err := c.client.Repositories.CreateHook(ctx, owner, repo, hook)
4✔
659
        return h, err
4✔
660
}
4✔
661

662
// EditHook edits an existing Hook.
663
func (c *GitHub) EditHook(ctx context.Context, owner, repo string, id int64, hook *github.Hook) (*github.Hook, error) {
×
664
        h, _, err := c.client.Repositories.EditHook(ctx, owner, repo, id, hook)
×
665
        return h, err
×
666
}
×
667

668
// CreateSecurityAdvisory creates a new security advisory
669
func (c *GitHub) CreateSecurityAdvisory(ctx context.Context, owner, repo, severity, summary, description string,
670
        v []*github.AdvisoryVulnerability) (string, error) {
3✔
671
        u := fmt.Sprintf("repos/%v/%v/security-advisories", owner, repo)
3✔
672

3✔
673
        payload := &struct {
3✔
674
                Summary         string                          `json:"summary"`
3✔
675
                Description     string                          `json:"description"`
3✔
676
                Severity        string                          `json:"severity"`
3✔
677
                Vulnerabilities []*github.AdvisoryVulnerability `json:"vulnerabilities"`
3✔
678
        }{
3✔
679
                Summary:         summary,
3✔
680
                Description:     description,
3✔
681
                Severity:        severity,
3✔
682
                Vulnerabilities: v,
3✔
683
        }
3✔
684
        req, err := c.client.NewRequest("POST", u, payload)
3✔
685
        if err != nil {
3✔
686
                return "", err
×
687
        }
×
688

689
        res := &struct {
3✔
690
                ID string `json:"ghsa_id"`
3✔
691
        }{}
3✔
692

3✔
693
        resp, err := c.client.Do(ctx, req, res)
3✔
694
        if err != nil {
5✔
695
                return "", err
2✔
696
        }
2✔
697

698
        if resp.StatusCode != http.StatusCreated {
1✔
699
                return "", fmt.Errorf("error creating security advisory: %v", resp.Status)
×
700
        }
×
701
        return res.ID, nil
1✔
702
}
703

704
// CloseSecurityAdvisory closes a security advisory
705
func (c *GitHub) CloseSecurityAdvisory(ctx context.Context, owner, repo, id string) error {
4✔
706
        u := fmt.Sprintf("repos/%v/%v/security-advisories/%v", owner, repo, id)
4✔
707

4✔
708
        payload := &struct {
4✔
709
                State string `json:"state"`
4✔
710
        }{
4✔
711
                State: "closed",
4✔
712
        }
4✔
713

4✔
714
        req, err := c.client.NewRequest("PATCH", u, payload)
4✔
715
        if err != nil {
4✔
716
                return err
×
717
        }
×
718

719
        resp, err := c.client.Do(ctx, req, nil)
4✔
720
        if err != nil {
7✔
721
                return err
3✔
722
        }
3✔
723
        // Translate the HTTP status code to an error, nil if between 200 and 299
724
        return engerrors.HTTPErrorCodeToErr(resp.StatusCode)
1✔
725
}
726

727
// CreatePullRequest creates a pull request in a repository.
728
func (c *GitHub) CreatePullRequest(
729
        ctx context.Context,
730
        owner, repo, title, body, head, base string,
731
) (*github.PullRequest, error) {
×
732
        pr, _, err := c.client.PullRequests.Create(ctx, owner, repo, &github.NewPullRequest{
×
733
                Title:               github.String(title),
×
734
                Body:                github.String(body),
×
735
                Head:                github.String(head),
×
736
                Base:                github.String(base),
×
737
                MaintainerCanModify: github.Bool(true),
×
738
        })
×
739
        if err != nil {
×
740
                return nil, err
×
741
        }
×
742

743
        return pr, nil
×
744
}
745

746
// ClosePullRequest closes a pull request in a repository.
747
func (c *GitHub) ClosePullRequest(ctx context.Context, owner, repo string, number int) (*github.PullRequest, error) {
×
748
        pr, _, err := c.client.PullRequests.Edit(ctx, owner, repo, number, &github.PullRequest{
×
749
                State: github.String("closed"),
×
750
        })
×
751
        if err != nil {
×
752
                return nil, err
×
753
        }
×
754
        return pr, nil
×
755
}
756

757
// ListPullRequests lists all pull requests in a repository.
758
func (c *GitHub) ListPullRequests(
759
        ctx context.Context,
760
        owner, repo string,
761
        opt *github.PullRequestListOptions,
762
) ([]*github.PullRequest, error) {
×
763
        prs, _, err := c.client.PullRequests.List(ctx, owner, repo, opt)
×
764
        if err != nil {
×
765
                return nil, err
×
766
        }
×
767

768
        return prs, nil
×
769
}
770

771
// CreateIssueComment creates a comment on a pull request or an issue
772
func (c *GitHub) CreateIssueComment(
773
        ctx context.Context, owner, repo string, number int, comment string,
774
) (*github.IssueComment, error) {
14✔
775
        var issueComment *github.IssueComment
14✔
776

14✔
777
        op := func() (any, error) {
30✔
778
                var err error
16✔
779

16✔
780
                issueComment, _, err = c.client.Issues.CreateComment(ctx, owner, repo, number, &github.IssueComment{
16✔
781
                        Body: &comment,
16✔
782
                })
16✔
783

16✔
784
                if isRateLimitError(err) {
30✔
785
                        waitWrr := c.waitForRateLimitReset(ctx, err)
14✔
786
                        if waitWrr == nil {
16✔
787
                                return nil, err
2✔
788
                        }
2✔
789
                        return nil, backoffv4.Permanent(err)
12✔
790
                }
791

792
                return nil, backoffv4.Permanent(err)
2✔
793
        }
794
        _, retryErr := performWithRetry(ctx, op)
14✔
795
        return issueComment, retryErr
14✔
796
}
797

798
// UpdateIssueComment updates a comment on a pull request or an issue
799
func (c *GitHub) UpdateIssueComment(ctx context.Context, owner, repo string, number int64, comment string) error {
×
800
        _, _, err := c.client.Issues.EditComment(ctx, owner, repo, number, &github.IssueComment{
×
801
                Body: &comment,
×
802
        })
×
803
        return err
×
804
}
×
805

806
// Clone clones a GitHub repository
807
func (c *GitHub) Clone(ctx context.Context, cloneUrl string, branch string) (*git.Repository, error) {
×
808
        delegator := gitclient.NewGit(c.delegate.GetCredential(), gitclient.WithConfig(c.gitConfig))
×
809
        return delegator.Clone(ctx, cloneUrl, branch)
×
810
}
×
811

812
// AddAuthToPushOptions adds authorization to the push options
813
func (c *GitHub) AddAuthToPushOptions(ctx context.Context, pushOptions *git.PushOptions) error {
×
814
        login, err := c.delegate.GetLogin(ctx)
×
815
        if err != nil {
×
816
                return fmt.Errorf("cannot get login: %w", err)
×
817
        }
×
818
        c.delegate.GetCredential().AddToPushOptions(pushOptions, login)
×
819
        return nil
×
820
}
821

822
// ListAllRepositories lists all repositories the credential has access to
823
func (c *GitHub) ListAllRepositories(ctx context.Context) ([]*minderv1.Repository, error) {
3✔
824
        return c.delegate.ListAllRepositories(ctx)
3✔
825
}
3✔
826

827
// GetUserId returns the user id for the acting user
828
func (c *GitHub) GetUserId(ctx context.Context) (int64, error) {
1✔
829
        return c.delegate.GetUserId(ctx)
1✔
830
}
1✔
831

832
// GetName returns the username for the acting user
833
func (c *GitHub) GetName(ctx context.Context) (string, error) {
1✔
834
        return c.delegate.GetName(ctx)
1✔
835
}
1✔
836

837
// GetLogin returns the login for the acting user
838
func (c *GitHub) GetLogin(ctx context.Context) (string, error) {
1✔
839
        return c.delegate.GetLogin(ctx)
1✔
840
}
1✔
841

842
// GetPrimaryEmail returns the primary email for the acting user
843
func (c *GitHub) GetPrimaryEmail(ctx context.Context) (string, error) {
1✔
844
        return c.delegate.GetPrimaryEmail(ctx)
1✔
845
}
1✔
846

847
// ListImages lists all containers in the GitHub Container Registry
848
func (c *GitHub) ListImages(ctx context.Context) ([]string, error) {
×
849
        return c.ghcrwrap.ListImages(ctx)
×
850
}
×
851

852
// GetNamespaceURL returns the URL for the repository
853
func (c *GitHub) GetNamespaceURL() string {
×
854
        return c.ghcrwrap.GetNamespaceURL()
×
855
}
×
856

857
// GetArtifactVersions returns a list of all versions for a specific artifact
858
func (gv *GitHub) GetArtifactVersions(
859
        ctx context.Context, artifact *minderv1.Artifact,
860
        filter provifv1.GetArtifactVersionsFilter,
861
) ([]*minderv1.ArtifactVersion, error) {
2✔
862
        // We don't need to URL-encode the artifact name
2✔
863
        // since this already happens in go-github
2✔
864
        upstreamVersions, err := gv.getPackageVersions(
2✔
865
                ctx, artifact.GetOwner(), artifact.GetTypeLower(), artifact.GetName(),
2✔
866
        )
2✔
867
        if err != nil {
3✔
868
                return nil, fmt.Errorf("error retrieving artifact versions: %w", err)
1✔
869
        }
1✔
870

871
        out := make([]*minderv1.ArtifactVersion, 0, len(upstreamVersions))
1✔
872
        for _, uv := range upstreamVersions {
2✔
873
                tags := uv.Metadata.Container.Tags
1✔
874

1✔
875
                if err := filter.IsSkippable(uv.CreatedAt.Time, tags); err != nil {
1✔
876
                        zerolog.Ctx(ctx).Debug().Str("name", artifact.GetName()).Strs("tags", tags).
×
877
                                Str("reason", err.Error()).Msg("skipping artifact version")
×
878
                        continue
×
879
                }
880

881
                sort.Strings(tags)
1✔
882

1✔
883
                // only the tags and creation time is relevant to us.
1✔
884
                out = append(out, &minderv1.ArtifactVersion{
1✔
885
                        Tags: tags,
1✔
886
                        // NOTE: GitHub's name is actually a SHA. This is misleading...
1✔
887
                        // but it is what it is. We'll use it as the SHA for now.
1✔
888
                        Sha:       *uv.Name,
1✔
889
                        CreatedAt: timestamppb.New(uv.CreatedAt.Time),
1✔
890
                })
1✔
891
        }
892

893
        return out, nil
1✔
894
}
895

896
// setAsRateLimited adds the GitHub to the cache as rate limited.
897
// An optimistic concurrency control mechanism is used to ensure that every request doesn't need
898
// synchronization. GitHub only adds itself to the cache if it's not already there. It doesn't
899
// remove itself from the cache when the rate limit is reset. This approach leverages the high
900
// likelihood of the client or token being rate-limited again. By keeping the client in the cache,
901
// we can reuse client's rateLimits map, which holds rate limits for different endpoints.
902
// This reuse of cached rate limits helps avoid unnecessary GitHub API requests when the client
903
// is rate-limited. Every cache entry has an expiration time, so the cache will eventually evict
904
// the rate-limited client.
905
func (c *GitHub) setAsRateLimited() {
18✔
906
        if c.cache != nil {
36✔
907
                c.cache.Set(c.delegate.GetOwner(), c.delegate.GetCredential().GetCacheKey(), db.ProviderTypeGithub, c)
18✔
908
        }
18✔
909
}
910

911
// waitForRateLimitReset waits for token wait limit to reset. Returns error if wait time is more
912
// than MaxRateLimitWait or requests' context is cancelled.
913
func (c *GitHub) waitForRateLimitReset(ctx context.Context, err error) error {
19✔
914
        var rateLimitError *github.RateLimitError
19✔
915
        isRateLimitErr := errors.As(err, &rateLimitError)
19✔
916

19✔
917
        if isRateLimitErr {
36✔
918
                return c.processPrimaryRateLimitErr(ctx, rateLimitError)
17✔
919
        }
17✔
920

921
        var abuseRateLimitError *github.AbuseRateLimitError
2✔
922
        isAbuseRateLimitErr := errors.As(err, &abuseRateLimitError)
2✔
923

2✔
924
        if isAbuseRateLimitErr {
3✔
925
                return c.processAbuseRateLimitErr(ctx, abuseRateLimitError)
1✔
926
        }
1✔
927

928
        return nil
1✔
929
}
930

931
func (c *GitHub) processPrimaryRateLimitErr(ctx context.Context, err *github.RateLimitError) error {
17✔
932
        logger := zerolog.Ctx(ctx)
17✔
933
        rate := err.Rate
17✔
934
        if rate.Remaining == 0 {
34✔
935
                c.setAsRateLimited()
17✔
936

17✔
937
                waitTime := DefaultRateLimitWaitTime
17✔
938
                resetTime := rate.Reset.Time
17✔
939
                if !resetTime.IsZero() {
34✔
940
                        waitTime = time.Until(resetTime)
17✔
941
                }
17✔
942

943
                logRateLimitError(logger, "RateLimitError", waitTime, c.delegate.GetOwner(), err.Response)
17✔
944

17✔
945
                if waitTime > MaxRateLimitWait {
30✔
946
                        logger.Debug().Msgf("rate limit reset time: %v exceeds maximum wait time: %v", waitTime, MaxRateLimitWait)
13✔
947
                        return err
13✔
948
                }
13✔
949

950
                // Wait for the rate limit to reset
951
                select {
4✔
952
                case <-time.After(waitTime):
4✔
953
                        return nil
4✔
954
                case <-ctx.Done():
×
955
                        logger.Debug().Err(ctx.Err()).Msg("context done while waiting for rate limit to reset")
×
956
                        return err
×
957
                }
958
        }
959

960
        return nil
×
961
}
962

963
func (c *GitHub) processAbuseRateLimitErr(ctx context.Context, err *github.AbuseRateLimitError) error {
1✔
964
        logger := zerolog.Ctx(ctx)
1✔
965
        c.setAsRateLimited()
1✔
966

1✔
967
        retryAfter := err.RetryAfter
1✔
968
        waitTime := DefaultRateLimitWaitTime
1✔
969
        if retryAfter != nil && *retryAfter > 0 {
2✔
970
                waitTime = *retryAfter
1✔
971
        }
1✔
972

973
        logRateLimitError(logger, "AbuseRateLimitError", waitTime, c.delegate.GetOwner(), err.Response)
1✔
974

1✔
975
        if waitTime > MaxRateLimitWait {
1✔
976
                logger.Debug().Msgf("abuse rate limit wait time: %v exceeds maximum wait time: %v", waitTime, MaxRateLimitWait)
×
977
                return err
×
978
        }
×
979

980
        // Wait for the rate limit to reset
981
        select {
1✔
982
        case <-time.After(waitTime):
1✔
983
                return nil
1✔
984
        case <-ctx.Done():
×
985
                logger.Debug().Err(ctx.Err()).Msg("context done while waiting for rate limit to reset")
×
986
                return err
×
987
        }
988
}
989

990
func logRateLimitError(logger *zerolog.Logger, errType string, waitTime time.Duration, owner string, resp *http.Response) {
18✔
991
        var method, path string
18✔
992
        if resp != nil && resp.Request != nil {
36✔
993
                method = resp.Request.Method
18✔
994
                path = resp.Request.URL.Path
18✔
995
        }
18✔
996

997
        event := logger.Debug().
18✔
998
                Str("owner", owner).
18✔
999
                Str("wait_time", waitTime.String()).
18✔
1000
                Str("error_type", errType)
18✔
1001

18✔
1002
        if method != "" {
36✔
1003
                event = event.Str("method", method)
18✔
1004
        }
18✔
1005

1006
        if path != "" {
36✔
1007
                event = event.Str("path", path)
18✔
1008
        }
18✔
1009

1010
        event.Msg("rate limit exceeded")
18✔
1011
}
1012

1013
func performWithRetry[T any](ctx context.Context, op backoffv4.OperationWithData[T]) (T, error) {
23✔
1014
        exponentialBackOff := backoffv4.NewExponentialBackOff()
23✔
1015
        maxRetriesBackoff := backoffv4.WithMaxRetries(exponentialBackOff, MaxRateLimitRetries)
23✔
1016
        return backoffv4.RetryWithData(op, backoffv4.WithContext(maxRetriesBackoff, ctx))
23✔
1017
}
23✔
1018

1019
func isRateLimitError(err error) bool {
23✔
1020
        var rateLimitError *github.RateLimitError
23✔
1021
        isRateLimitErr := errors.As(err, &rateLimitError)
23✔
1022

23✔
1023
        var abuseRateLimitError *github.AbuseRateLimitError
23✔
1024
        isAbuseRateLimitErr := errors.As(err, &abuseRateLimitError)
23✔
1025

23✔
1026
        return isRateLimitErr || isAbuseRateLimitErr
23✔
1027
}
23✔
1028

1029
// IsMinderHook checks if a GitHub hook is a Minder hook
1030
func IsMinderHook(hook *github.Hook, hostURL string) (bool, error) {
4✔
1031
        configURL := hook.GetConfig().GetURL()
4✔
1032
        if configURL == "" {
5✔
1033
                return false, fmt.Errorf("unexpected hook config structure: %v", hook.Config)
1✔
1034
        }
1✔
1035
        parsedURL, err := url.Parse(configURL)
3✔
1036
        if err != nil {
4✔
1037
                return false, err
1✔
1038
        }
1✔
1039
        if parsedURL.Host == hostURL {
3✔
1040
                return true, nil
1✔
1041
        }
1✔
1042

1043
        return false, nil
1✔
1044
}
1045

1046
// CanHandleOwner checks if the GitHub provider has the right credentials to handle the owner
1047
func CanHandleOwner(_ context.Context, prov db.Provider, owner string) bool {
4✔
1048
        // TODO: this is fragile and does not handle organization renames, in the future we can make sure the credential
4✔
1049
        // has admin permissions on the owner
4✔
1050
        if prov.Name == fmt.Sprintf("%s-%s", db.ProviderClassGithubApp, owner) {
5✔
1051
                return true
1✔
1052
        }
1✔
1053
        if prov.Class == db.ProviderClassGithub {
4✔
1054
                return true
1✔
1055
        }
1✔
1056
        return false
2✔
1057
}
1058

1059
// NewFallbackTokenClient creates a new GitHub client that uses the GitHub App's fallback token
1060
func NewFallbackTokenClient(appConfig config.ProviderConfig) *github.Client {
3✔
1061
        if appConfig.GitHubApp == nil {
4✔
1062
                return nil
1✔
1063
        }
1✔
1064
        fallbackToken, err := appConfig.GitHubApp.GetFallbackToken()
2✔
1065
        if err != nil || fallbackToken == "" {
3✔
1066
                return nil
1✔
1067
        }
1✔
1068
        var packageListingClient *github.Client
1✔
1069

1✔
1070
        fallbackTokenSource := oauth2.StaticTokenSource(
1✔
1071
                &oauth2.Token{AccessToken: fallbackToken},
1✔
1072
        )
1✔
1073
        fallbackTokenTC := &http.Client{
1✔
1074
                Transport: &oauth2.Transport{
1✔
1075
                        Base:   http.DefaultClient.Transport,
1✔
1076
                        Source: fallbackTokenSource,
1✔
1077
                },
1✔
1078
        }
1✔
1079

1✔
1080
        packageListingClient = github.NewClient(fallbackTokenTC)
1✔
1081
        return packageListingClient
1✔
1082
}
1083

1084
// StartCheckRun calls the GitHub API to initialize a new check using the
1085
// supplied options.
1086
func (c *GitHub) StartCheckRun(
1087
        ctx context.Context, owner, repo string, opts *github.CreateCheckRunOptions,
1088
) (*github.CheckRun, error) {
2✔
1089
        if opts.StartedAt == nil {
3✔
1090
                opts.StartedAt = &github.Timestamp{Time: time.Now()}
1✔
1091
        }
1✔
1092

1093
        run, resp, err := c.client.Checks.CreateCheckRun(ctx, owner, repo, *opts)
2✔
1094
        if err != nil {
3✔
1095
                // If error is 403 then it means we are missing permissions
1✔
1096
                if resp.StatusCode == 403 {
2✔
1097
                        return nil, fmt.Errorf("missing permissions: check")
1✔
1098
                }
1✔
1099
                return nil, ErroNoCheckPermissions
×
1100
        }
1101
        return run, nil
1✔
1102
}
1103

1104
// UpdateCheckRun updates an existing check run in GitHub. The check run is referenced
1105
// using its run ID. This function returns the updated CheckRun srtuct.
1106
func (c *GitHub) UpdateCheckRun(
1107
        ctx context.Context, owner, repo string, checkRunID int64, opts *github.UpdateCheckRunOptions,
1108
) (*github.CheckRun, error) {
2✔
1109
        run, resp, err := c.client.Checks.UpdateCheckRun(ctx, owner, repo, checkRunID, *opts)
2✔
1110
        if err != nil {
3✔
1111
                // If error is 403 then it means we are missing permissions
1✔
1112
                if resp.StatusCode == 403 {
2✔
1113
                        return nil, ErroNoCheckPermissions
1✔
1114
                }
1✔
1115
                return nil, fmt.Errorf("updating check: %w", err)
×
1116
        }
1117
        return run, nil
1✔
1118
}
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