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

mindersec / minder / 25076733413

28 Apr 2026 08:44PM UTC coverage: 60.47% (+0.5%) from 60.0%
25076733413

Pull #6278

github

web-flow
Merge 7277590bc into 45b9c0d1a
Pull Request #6278: feat: make executor event handling timeout configurable

27 of 29 new or added lines in 2 files covered. (93.1%)

427 existing lines in 13 files now uncovered.

20410 of 33752 relevant lines covered (60.47%)

37.4 hits per line

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

63.34
/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
        gitclient "github.com/mindersec/minder/internal/providers/git"
28
        "github.com/mindersec/minder/internal/providers/github/ghcr"
29
        "github.com/mindersec/minder/internal/providers/github/properties"
30
        "github.com/mindersec/minder/internal/providers/ratecache"
31
        minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
32
        config "github.com/mindersec/minder/pkg/config/server"
33
        engerrors "github.com/mindersec/minder/pkg/engine/errors"
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
        providerClass        db.ProviderClass
73
        ghcrwrap             *ghcr.ImageLister
74
        gitConfig            config.GitConfig
75
        webhookConfig        *config.WebhookConfig
76
        propertyFetchers     properties.GhPropertyFetcherFactory
77
}
78

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

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

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

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

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

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

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

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

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

123
        return user.ID, nil
×
124
}
125

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

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

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

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

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

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

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

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

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

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

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

238
                listPackagesResp := listPackagesRespWrapper{
3✔
239
                        artifacts: artifacts,
3✔
240
                        resp:      resp,
3✔
241
                }
3✔
242

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

251
                return listPackagesResp, backoffv4.Permanent(err)
3✔
252
        }
253

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

UNCOV
261
                        return allContainers, err
×
262
                }
263

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

271
                if result.resp.NextPage == 0 {
6✔
272
                        break
3✔
273
                }
UNCOV
274
                opt.Page = result.resp.NextPage
×
275
        }
276

277
        return allContainers, nil
3✔
278
}
279

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

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

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

4✔
297
        // create a slice to hold the versions
4✔
298
        var allVersions []*github.PackageVersion
4✔
299

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

315
                // append to the slice
316
                allVersions = append(allVersions, v...)
3✔
317

3✔
318
                // if there is no next page, break
3✔
319
                if resp.NextPage == 0 {
6✔
320
                        break
3✔
321
                }
322

323
                // update the page
UNCOV
324
                opt.Page = resp.NextPage
×
325
        }
326

327
        // return the slice
328
        return allVersions, nil
3✔
329
}
330

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

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

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

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

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

373
        return pkgVersion, nil
2✔
374
}
375

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

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

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

4✔
411
                listFileResp := listFilesRespWrapper{
4✔
412
                        files: files,
4✔
413
                        resp:  resp,
4✔
414
                }
4✔
415

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

424
                return listFileResp, backoffv4.Permanent(err)
3✔
425
        }
426

427
        resp, err := performWithRetry(ctx, op)
3✔
428
        return resp.files, resp.resp, err
3✔
429
}
430

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

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

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

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

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

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

504
// GetRepository returns a single repository for the authenticated user
UNCOV
505
func (c *GitHub) GetRepository(ctx context.Context, owner string, name string) (*github.Repository, error) {
×
506
        // create a slice to hold the repositories
×
UNCOV
507
        repo, _, err := c.client.Repositories.Get(ctx, owner, name)
×
UNCOV
508
        if err != nil {
×
UNCOV
509
                return nil, fmt.Errorf("error getting repository: %w", err)
×
UNCOV
510
        }
×
511

512
        return repo, nil
×
513
}
514

515
// GetBranchProtection returns the branch protection for a given branch
516
func (c *GitHub) GetBranchProtection(ctx context.Context, owner string,
517
        repo_name string, branch_name string) (*github.Protection, error) {
×
518
        var respErr *github.ErrorResponse
×
519
        if branch_name == "" {
×
520
                return nil, ErrBranchNameEmpty
×
521
        }
×
522

523
        protection, _, err := c.client.Repositories.GetBranchProtection(ctx, owner, repo_name, branch_name)
×
524
        if errors.As(err, &respErr) {
×
525
                if respErr.Message == githubBranchNotFoundMsg {
×
526
                        return nil, ErrBranchNotFound
×
527
                }
×
528

UNCOV
529
                return nil, fmt.Errorf("error getting branch protection: %w", err)
×
UNCOV
530
        } else if err != nil {
×
UNCOV
531
                return nil, err
×
UNCOV
532
        }
×
533
        return protection, nil
×
534
}
535

536
// UpdateBranchProtection updates the branch protection for a given branch
537
func (c *GitHub) UpdateBranchProtection(
538
        ctx context.Context, owner, repo, branch string, preq *github.ProtectionRequest,
UNCOV
539
) error {
×
UNCOV
540
        if branch == "" {
×
UNCOV
541
                return ErrBranchNameEmpty
×
UNCOV
542
        }
×
UNCOV
543
        _, _, err := c.client.Repositories.UpdateBranchProtection(ctx, owner, repo, branch, preq)
×
UNCOV
544
        return err
×
545
}
546

547
// GetBaseURL returns the base URL for the REST API.
548
func (c *GitHub) GetBaseURL() string {
1✔
549
        return c.client.BaseURL.String()
1✔
550
}
1✔
551

552
// NewRequest creates an API request. A relative URL can be provided in urlStr,
553
// which will be resolved to the BaseURL of the Client. Relative URLS should
554
// always be specified without a preceding slash. If specified, the value
555
// pointed to by body is JSON encoded and included as the request body.
556
func (c *GitHub) NewRequest(method, requestUrl string, body any) (*http.Request, error) {
9✔
557
        return c.client.NewRequest(method, requestUrl, body)
9✔
558
}
9✔
559

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

9✔
564
        // The GitHub client closes the response body, so we need to capture it
9✔
565
        // in a buffer so that we can return it to the caller
9✔
566
        resp, err := c.client.Do(ctx, req, &buf)
9✔
567
        if err != nil && resp == nil {
9✔
UNCOV
568
                return nil, err
×
UNCOV
569
        }
×
570

571
        if resp.Response != nil {
18✔
572
                resp.Body = io.NopCloser(&buf)
9✔
573
        }
9✔
574
        return resp.Response, err
9✔
575
}
576

577
// GetCredential returns the credential used to authenticate with the GitHub API
UNCOV
578
func (c *GitHub) GetCredential() provifv1.GitHubCredential {
×
UNCOV
579
        return c.delegate.GetCredential()
×
UNCOV
580
}
×
581

582
// IsOrg returns true if the owner is an organization
583
func (c *GitHub) IsOrg() bool {
11✔
584
        return c.delegate.IsOrg()
11✔
585
}
11✔
586

587
// ListHooks lists all Hooks for the specified repository.
588
func (c *GitHub) ListHooks(ctx context.Context, owner, repo string) ([]*github.Hook, error) {
×
UNCOV
589
        list, resp, err := c.client.Repositories.ListHooks(ctx, owner, repo, nil)
×
UNCOV
590
        if err != nil && resp != nil && resp.StatusCode == http.StatusNotFound {
×
UNCOV
591
                // return empty list so that the caller can ignore the error and iterate over the empty list
×
592
                return []*github.Hook{}, fmt.Errorf("hooks not found for repository %s/%s: %w", owner, repo, ErrNotFound)
×
593
        }
×
594
        return list, err
×
595
}
596

597
// DeleteHook deletes a specified Hook.
598
func (c *GitHub) DeleteHook(ctx context.Context, owner, repo string, id int64) error {
×
599
        resp, err := c.client.Repositories.DeleteHook(ctx, owner, repo, id)
×
600
        if isRateLimitError(err) {
×
601
                return c.waitForRateLimitReset(ctx, err)
×
602
        }
×
603
        if resp != nil && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden) {
×
604
                // If the hook is not found, we can ignore the
×
605
                // error, user might have deleted it manually.
×
UNCOV
606
                // We also ignore deleting webhooks that we're not
×
UNCOV
607
                // allowed to touch. This is usually the case
×
UNCOV
608
                // with repository transfer.
×
UNCOV
609
                return nil
×
UNCOV
610
        }
×
UNCOV
611
        return err
×
612
}
613

614
// CreateHook creates a new Hook.
615
func (c *GitHub) CreateHook(ctx context.Context, owner, repo string, hook *github.Hook) (*github.Hook, error) {
4✔
616
        h, _, err := c.client.Repositories.CreateHook(ctx, owner, repo, hook)
4✔
617
        if isRateLimitError(err) {
5✔
618
                if waitErr := c.waitForRateLimitReset(ctx, err); waitErr != nil {
2✔
619
                        return nil, waitErr
1✔
620
                }
1✔
621
        }
622
        return h, err
3✔
623
}
624

625
// EditHook edits an existing Hook.
UNCOV
626
func (c *GitHub) EditHook(ctx context.Context, owner, repo string, id int64, hook *github.Hook) (*github.Hook, error) {
×
627
        h, _, err := c.client.Repositories.EditHook(ctx, owner, repo, id, hook)
×
UNCOV
628
        if isRateLimitError(err) {
×
UNCOV
629
                if waitErr := c.waitForRateLimitReset(ctx, err); waitErr != nil {
×
UNCOV
630
                        return nil, waitErr
×
UNCOV
631
                }
×
632
        }
UNCOV
633
        return h, err
×
634
}
635

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

3✔
641
        payload := &struct {
3✔
642
                Summary         string                          `json:"summary"`
3✔
643
                Description     string                          `json:"description"`
3✔
644
                Severity        string                          `json:"severity"`
3✔
645
                Vulnerabilities []*github.AdvisoryVulnerability `json:"vulnerabilities"`
3✔
646
        }{
3✔
647
                Summary:         summary,
3✔
648
                Description:     description,
3✔
649
                Severity:        severity,
3✔
650
                Vulnerabilities: v,
3✔
651
        }
3✔
652
        req, err := c.client.NewRequest("POST", u, payload)
3✔
653
        if err != nil {
3✔
UNCOV
654
                return "", err
×
UNCOV
655
        }
×
656

657
        res := &struct {
3✔
658
                ID string `json:"ghsa_id"`
3✔
659
        }{}
3✔
660

3✔
661
        resp, err := c.client.Do(ctx, req, res)
3✔
662
        if err != nil {
5✔
663
                return "", err
2✔
664
        }
2✔
665

666
        if resp.StatusCode != http.StatusCreated {
1✔
UNCOV
667
                return "", fmt.Errorf("error creating security advisory: %v", resp.Status)
×
UNCOV
668
        }
×
669
        return res.ID, nil
1✔
670
}
671

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

4✔
676
        payload := &struct {
4✔
677
                State string `json:"state"`
4✔
678
        }{
4✔
679
                State: "closed",
4✔
680
        }
4✔
681

4✔
682
        req, err := c.client.NewRequest("PATCH", u, payload)
4✔
683
        if err != nil {
4✔
UNCOV
684
                return err
×
UNCOV
685
        }
×
686

687
        resp, err := c.client.Do(ctx, req, nil)
4✔
688
        if err != nil {
7✔
689
                return err
3✔
690
        }
3✔
691
        // Translate the HTTP status code to an error, nil if between 200 and 299
692
        return engerrors.HTTPErrorCodeToErr(resp.StatusCode)
1✔
693
}
694

695
// CreatePullRequest creates a pull request in a repository.
696
func (c *GitHub) CreatePullRequest(
697
        ctx context.Context,
698
        owner, repo, title, body, head, base string,
699
) (*github.PullRequest, error) {
×
700
        pr, _, err := c.client.PullRequests.Create(ctx, owner, repo, &github.NewPullRequest{
×
701
                Title:               github.String(title),
×
702
                Body:                github.String(body),
×
703
                Head:                github.String(head),
×
UNCOV
704
                Base:                github.String(base),
×
705
                MaintainerCanModify: github.Bool(true),
×
UNCOV
706
        })
×
UNCOV
707
        if err != nil {
×
UNCOV
708
                return nil, err
×
709
        }
×
710

711
        return pr, nil
×
712
}
713

714
// ClosePullRequest closes a pull request in a repository.
715
func (c *GitHub) ClosePullRequest(ctx context.Context, owner, repo string, number int) (*github.PullRequest, error) {
×
716
        pr, _, err := c.client.PullRequests.Edit(ctx, owner, repo, number, &github.PullRequest{
×
UNCOV
717
                State: github.String("closed"),
×
UNCOV
718
        })
×
UNCOV
719
        if err != nil {
×
UNCOV
720
                return nil, err
×
UNCOV
721
        }
×
UNCOV
722
        return pr, nil
×
723
}
724

725
// ListPullRequests lists all pull requests in a repository.
726
func (c *GitHub) ListPullRequests(
727
        ctx context.Context,
728
        owner, repo string,
729
        opt *github.PullRequestListOptions,
730
) ([]*github.PullRequest, error) {
×
UNCOV
731
        prs, _, err := c.client.PullRequests.List(ctx, owner, repo, opt)
×
UNCOV
732
        if err != nil {
×
UNCOV
733
                return nil, err
×
UNCOV
734
        }
×
735

UNCOV
736
        return prs, nil
×
737
}
738

739
// CreateIssueComment creates a comment on a pull request or an issue
740
func (c *GitHub) CreateIssueComment(
741
        ctx context.Context, owner, repo string, number int, comment string,
742
) (*github.IssueComment, error) {
14✔
743
        var issueComment *github.IssueComment
14✔
744

14✔
745
        op := func() (any, error) {
30✔
746
                var err error
16✔
747

16✔
748
                issueComment, _, err = c.client.Issues.CreateComment(ctx, owner, repo, number, &github.IssueComment{
16✔
749
                        Body: &comment,
16✔
750
                })
16✔
751

16✔
752
                if isRateLimitError(err) {
30✔
753
                        waitWrr := c.waitForRateLimitReset(ctx, err)
14✔
754
                        if waitWrr == nil {
16✔
755
                                return nil, err
2✔
756
                        }
2✔
757
                        return nil, backoffv4.Permanent(err)
12✔
758
                }
759

760
                return nil, backoffv4.Permanent(err)
2✔
761
        }
762
        _, retryErr := performWithRetry(ctx, op)
14✔
763
        return issueComment, retryErr
14✔
764
}
765

766
// UpdateIssueComment updates a comment on a pull request or an issue
UNCOV
767
func (c *GitHub) UpdateIssueComment(ctx context.Context, owner, repo string, number int64, comment string) error {
×
UNCOV
768
        _, _, err := c.client.Issues.EditComment(ctx, owner, repo, number, &github.IssueComment{
×
769
                Body: &comment,
×
770
        })
×
771
        return err
×
772
}
×
773

774
// Clone clones a GitHub repository
775
func (c *GitHub) Clone(ctx context.Context, cloneUrl string, branch string) (*git.Repository, error) {
×
776
        delegator := gitclient.NewGit(c.delegate.GetCredential(), gitclient.WithConfig(c.gitConfig))
×
777
        return delegator.Clone(ctx, cloneUrl, branch)
×
778
}
×
779

780
// AddAuthToPushOptions adds authorization to the push options
781
func (c *GitHub) AddAuthToPushOptions(ctx context.Context, pushOptions *git.PushOptions) error {
×
UNCOV
782
        login, err := c.delegate.GetLogin(ctx)
×
UNCOV
783
        if err != nil {
×
UNCOV
784
                return fmt.Errorf("cannot get login: %w", err)
×
UNCOV
785
        }
×
UNCOV
786
        c.delegate.GetCredential().AddToPushOptions(pushOptions, login)
×
UNCOV
787
        return nil
×
788
}
789

790
// ListAllRepositories lists all repositories the credential has access to
791
func (c *GitHub) ListAllRepositories(ctx context.Context) ([]*minderv1.Repository, error) {
3✔
792
        return c.delegate.ListAllRepositories(ctx)
3✔
793
}
3✔
794

795
// GetUserId returns the user id for the acting user
796
func (c *GitHub) GetUserId(ctx context.Context) (int64, error) {
1✔
797
        return c.delegate.GetUserId(ctx)
1✔
798
}
1✔
799

800
// GetName returns the username for the acting user
801
func (c *GitHub) GetName(ctx context.Context) (string, error) {
1✔
802
        return c.delegate.GetName(ctx)
1✔
803
}
1✔
804

805
// GetLogin returns the login for the acting user
806
func (c *GitHub) GetLogin(ctx context.Context) (string, error) {
1✔
807
        return c.delegate.GetLogin(ctx)
1✔
808
}
1✔
809

810
// GetPrimaryEmail returns the primary email for the acting user
811
func (c *GitHub) GetPrimaryEmail(ctx context.Context) (string, error) {
1✔
812
        return c.delegate.GetPrimaryEmail(ctx)
1✔
813
}
1✔
814

815
// ListImages lists all containers in the GitHub Container Registry
816
func (c *GitHub) ListImages(ctx context.Context) ([]string, error) {
×
817
        return c.ghcrwrap.ListImages(ctx)
×
UNCOV
818
}
×
819

820
// GetNamespaceURL returns the URL for the repository
UNCOV
821
func (c *GitHub) GetNamespaceURL() string {
×
UNCOV
822
        return c.ghcrwrap.GetNamespaceURL()
×
UNCOV
823
}
×
824

825
// GetArtifactVersions returns a list of all versions for a specific artifact
826
func (gv *GitHub) GetArtifactVersions(
827
        ctx context.Context, artifact *minderv1.Artifact,
828
        filter provifv1.GetArtifactVersionsFilter,
829
) ([]*minderv1.ArtifactVersion, error) {
2✔
830
        // We don't need to URL-encode the artifact name
2✔
831
        // since this already happens in go-github
2✔
832
        upstreamVersions, err := gv.getPackageVersions(
2✔
833
                ctx, artifact.GetOwner(), artifact.GetTypeLower(), artifact.GetName(),
2✔
834
        )
2✔
835
        if err != nil {
3✔
836
                return nil, fmt.Errorf("error retrieving artifact versions: %w", err)
1✔
837
        }
1✔
838

839
        out := make([]*minderv1.ArtifactVersion, 0, len(upstreamVersions))
1✔
840
        for _, uv := range upstreamVersions {
2✔
841
                tags := uv.Metadata.Container.Tags
1✔
842

1✔
843
                if err := filter.IsSkippable(uv.CreatedAt.Time, tags); err != nil {
1✔
UNCOV
844
                        zerolog.Ctx(ctx).Debug().Str("name", artifact.GetName()).Strs("tags", tags).
×
UNCOV
845
                                Str("reason", err.Error()).Msg("skipping artifact version")
×
UNCOV
846
                        continue
×
847
                }
848

849
                sort.Strings(tags)
1✔
850

1✔
851
                // only the tags and creation time is relevant to us.
1✔
852
                out = append(out, &minderv1.ArtifactVersion{
1✔
853
                        Tags: tags,
1✔
854
                        // NOTE: GitHub's name is actually a SHA. This is misleading...
1✔
855
                        // but it is what it is. We'll use it as the SHA for now.
1✔
856
                        Sha:       *uv.Name,
1✔
857
                        CreatedAt: timestamppb.New(uv.CreatedAt.Time),
1✔
858
                })
1✔
859
        }
860

861
        return out, nil
1✔
862
}
863

864
// setAsRateLimited adds the GitHub to the cache as rate limited.
865
// An optimistic concurrency control mechanism is used to ensure that every request doesn't need
866
// synchronization. GitHub only adds itself to the cache if it's not already there. It doesn't
867
// remove itself from the cache when the rate limit is reset. This approach leverages the high
868
// likelihood of the client or token being rate-limited again. By keeping the client in the cache,
869
// we can reuse client's rateLimits map, which holds rate limits for different endpoints.
870
// This reuse of cached rate limits helps avoid unnecessary GitHub API requests when the client
871
// is rate-limited. Every cache entry has an expiration time, so the cache will eventually evict
872
// the rate-limited client.
873
func (c *GitHub) setAsRateLimited() {
19✔
874
        if c.cache != nil {
38✔
875
                c.cache.Set(c.delegate.GetOwner(), c.delegate.GetCredential().GetCacheKey(), db.ProviderTypeGithub, c)
19✔
876
        }
19✔
877
}
878

879
// waitForRateLimitReset waits for token wait limit to reset. Returns error if wait time is more
880
// than MaxRateLimitWait or requests' context is cancelled.
881
func (c *GitHub) waitForRateLimitReset(ctx context.Context, err error) error {
20✔
882
        var rateLimitError *github.RateLimitError
20✔
883
        if errors.As(err, &rateLimitError) {
38✔
884
                return c.processPrimaryRateLimitErr(ctx, rateLimitError)
18✔
885
        }
18✔
886

887
        var abuseRateLimitError *github.AbuseRateLimitError
2✔
888
        if errors.As(err, &abuseRateLimitError) {
3✔
889
                return c.processAbuseRateLimitErr(ctx, abuseRateLimitError)
1✔
890
        }
1✔
891

892
        return nil
1✔
893
}
894

895
func (c *GitHub) processPrimaryRateLimitErr(ctx context.Context, err *github.RateLimitError) error {
18✔
896
        logger := zerolog.Ctx(ctx)
18✔
897
        rate := err.Rate
18✔
898
        if rate.Remaining == 0 {
36✔
899
                c.setAsRateLimited()
18✔
900

18✔
901
                waitTime := DefaultRateLimitWaitTime
18✔
902
                resetTime := rate.Reset.Time
18✔
903
                if !resetTime.IsZero() {
36✔
904
                        waitTime = time.Until(resetTime)
18✔
905
                }
18✔
906

907
                logRateLimitError(logger, "RateLimitError", waitTime, c.delegate.GetOwner(), err.Response)
18✔
908

18✔
909
                if waitTime > MaxRateLimitWait {
32✔
910
                        logger.Debug().Msgf("rate limit reset time: %v exceeds maximum wait time: %v", waitTime, MaxRateLimitWait)
14✔
911
                        return engerrors.NewRateLimitError(err, int64(rate.Limit), int64(rate.Remaining), resetTime)
14✔
912
                }
14✔
913

914
                // Wait for the rate limit to reset
915
                select {
4✔
916
                case <-time.After(waitTime):
4✔
917
                        return nil
4✔
918
                case <-ctx.Done():
×
UNCOV
919
                        logger.Debug().Err(ctx.Err()).Msg("context done while waiting for rate limit to reset")
×
UNCOV
920
                        return engerrors.NewRateLimitError(err, int64(rate.Limit), int64(rate.Remaining), resetTime)
×
921
                }
922
        }
923

UNCOV
924
        return nil
×
925
}
926

927
func (c *GitHub) processAbuseRateLimitErr(ctx context.Context, err *github.AbuseRateLimitError) error {
1✔
928
        logger := zerolog.Ctx(ctx)
1✔
929
        c.setAsRateLimited()
1✔
930

1✔
931
        retryAfter := err.RetryAfter
1✔
932
        waitTime := DefaultRateLimitWaitTime
1✔
933
        if retryAfter != nil && *retryAfter > 0 {
2✔
934
                waitTime = *retryAfter
1✔
935
        }
1✔
936

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

1✔
939
        if waitTime > MaxRateLimitWait {
1✔
UNCOV
940
                logger.Debug().Msgf("abuse rate limit wait time: %v exceeds maximum wait time: %v", waitTime, MaxRateLimitWait)
×
UNCOV
941
                return engerrors.NewRateLimitError(err, 0, 0, time.Now().Add(waitTime))
×
942
        }
×
943

944
        // Wait for the rate limit to reset
945
        select {
1✔
946
        case <-time.After(waitTime):
1✔
947
                return nil
1✔
UNCOV
948
        case <-ctx.Done():
×
UNCOV
949
                logger.Debug().Err(ctx.Err()).Msg("context done while waiting for rate limit to reset")
×
UNCOV
950
                return engerrors.NewRateLimitError(err, 0, 0, time.Now().Add(waitTime))
×
951
        }
952
}
953

954
func logRateLimitError(logger *zerolog.Logger, errType string, waitTime time.Duration, owner string, resp *http.Response) {
19✔
955
        var method, path string
19✔
956
        if resp != nil && resp.Request != nil {
38✔
957
                method = resp.Request.Method
19✔
958
                path = resp.Request.URL.Path
19✔
959
        }
19✔
960

961
        event := logger.Debug().
19✔
962
                Str("owner", owner).
19✔
963
                Str("wait_time", waitTime.String()).
19✔
964
                Str("error_type", errType)
19✔
965

19✔
966
        if method != "" {
38✔
967
                event = event.Str("method", method)
19✔
968
        }
19✔
969

970
        if path != "" {
38✔
971
                event = event.Str("path", path)
19✔
972
        }
19✔
973

974
        event.Msg("rate limit exceeded")
19✔
975
}
976

977
func performWithRetry[T any](ctx context.Context, op backoffv4.OperationWithData[T]) (T, error) {
23✔
978
        exponentialBackOff := backoffv4.NewExponentialBackOff()
23✔
979
        maxRetriesBackoff := backoffv4.WithMaxRetries(exponentialBackOff, MaxRateLimitRetries)
23✔
980
        return backoffv4.RetryWithData(op, backoffv4.WithContext(maxRetriesBackoff, ctx))
23✔
981
}
23✔
982

983
func isRateLimitError(err error) bool {
32✔
984
        var rateLimitErr *github.RateLimitError
32✔
985
        if errors.As(err, &rateLimitErr) {
48✔
986
                return true
16✔
987
        }
16✔
988
        var abuseRateLimitErr *github.AbuseRateLimitError
16✔
989
        return errors.As(err, &abuseRateLimitErr)
16✔
990
}
991

992
// IsMinderHook checks if a GitHub hook is a Minder hook
993
func IsMinderHook(hook *github.Hook, hostURL string) (bool, error) {
4✔
994
        configURL := hook.GetConfig().GetURL()
4✔
995
        if configURL == "" {
5✔
996
                return false, fmt.Errorf("unexpected hook config structure: %v", hook.Config)
1✔
997
        }
1✔
998
        parsedURL, err := url.Parse(configURL)
3✔
999
        if err != nil {
4✔
1000
                return false, err
1✔
1001
        }
1✔
1002
        if parsedURL.Host == hostURL {
3✔
1003
                return true, nil
1✔
1004
        }
1✔
1005

1006
        return false, nil
1✔
1007
}
1008

1009
// CanHandleOwner checks if the GitHub provider has the right credentials to handle the owner
1010
func CanHandleOwner(_ context.Context, prov db.Provider, owner string) bool {
4✔
1011
        // TODO: this is fragile and does not handle organization renames, in the future we can make sure the credential
4✔
1012
        // has admin permissions on the owner
4✔
1013
        if prov.Name == fmt.Sprintf("%s-%s", db.ProviderClassGithubApp, owner) {
5✔
1014
                return true
1✔
1015
        }
1✔
1016
        if prov.Class == db.ProviderClassGithub {
4✔
1017
                return true
1✔
1018
        }
1✔
1019
        return false
2✔
1020
}
1021

1022
// NewFallbackTokenClient creates a new GitHub client that uses the GitHub App's fallback token
1023
func NewFallbackTokenClient(appConfig config.ProviderConfig) *github.Client {
3✔
1024
        if appConfig.GitHubApp == nil {
4✔
1025
                return nil
1✔
1026
        }
1✔
1027
        fallbackToken, err := appConfig.GitHubApp.GetFallbackToken()
2✔
1028
        if err != nil || fallbackToken == "" {
3✔
1029
                return nil
1✔
1030
        }
1✔
1031
        var packageListingClient *github.Client
1✔
1032

1✔
1033
        fallbackTokenSource := oauth2.StaticTokenSource(
1✔
1034
                &oauth2.Token{AccessToken: fallbackToken},
1✔
1035
        )
1✔
1036
        fallbackTokenTC := &http.Client{
1✔
1037
                Transport: &oauth2.Transport{
1✔
1038
                        Base:   http.DefaultClient.Transport,
1✔
1039
                        Source: fallbackTokenSource,
1✔
1040
                },
1✔
1041
        }
1✔
1042

1✔
1043
        packageListingClient = github.NewClient(fallbackTokenTC)
1✔
1044
        return packageListingClient
1✔
1045
}
1046

1047
// StartCheckRun calls the GitHub API to initialize a new check using the
1048
// supplied options.
1049
func (c *GitHub) StartCheckRun(
1050
        ctx context.Context, owner, repo string, opts *github.CreateCheckRunOptions,
1051
) (*github.CheckRun, error) {
4✔
1052
        if opts.StartedAt == nil {
7✔
1053
                opts.StartedAt = &github.Timestamp{Time: time.Now()}
3✔
1054
        }
3✔
1055

1056
        run, resp, err := c.client.Checks.CreateCheckRun(ctx, owner, repo, *opts)
4✔
1057
        if err != nil {
7✔
1058
                if isRateLimitError(err) {
3✔
UNCOV
1059
                        return nil, c.waitForRateLimitReset(ctx, err)
×
UNCOV
1060
                }
×
1061
                // If error is 403 then it means we are missing permissions
1062
                if resp != nil && resp.StatusCode == 403 {
4✔
1063
                        return nil, ErroNoCheckPermissions
1✔
1064
                }
1✔
1065
                return nil, fmt.Errorf("creating check: %w", err)
2✔
1066
        }
1067
        return run, nil
1✔
1068
}
1069

1070
// UpdateCheckRun updates an existing check run in GitHub. The check run is referenced
1071
// using its run ID. This function returns the updated CheckRun srtuct.
1072
func (c *GitHub) UpdateCheckRun(
1073
        ctx context.Context, owner, repo string, checkRunID int64, opts *github.UpdateCheckRunOptions,
1074
) (*github.CheckRun, error) {
3✔
1075
        run, resp, err := c.client.Checks.UpdateCheckRun(ctx, owner, repo, checkRunID, *opts)
3✔
1076
        if err != nil {
5✔
1077
                if isRateLimitError(err) {
2✔
UNCOV
1078
                        return nil, c.waitForRateLimitReset(ctx, err)
×
UNCOV
1079
                }
×
1080
                // If error is 403 then it means we are missing permissions
1081
                if resp != nil && resp.StatusCode == 403 {
3✔
1082
                        return nil, ErroNoCheckPermissions
1✔
1083
                }
1✔
1084
                return nil, fmt.Errorf("updating check: %w", err)
1✔
1085
        }
1086
        return run, nil
1✔
1087
}
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