Skip to content

Commit ccea763

Browse files
chmouelpipelines-as-code[bot]
authored andcommitted
fix(github): handle bot users in status checks
- Added logic to skip setting status for bot users in `CreateStatus` method. - Updated `processEvent` to capture user type from GitHub events. - Modified tests to include scenarios for bot users. - Corrected minor formatting issues in documentation. More details here: https://issues.redhat.com/browse/SRVKP-7242 Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent 286bcef commit ccea763

File tree

7 files changed

+98
-26
lines changed

7 files changed

+98
-26
lines changed

docs/content/docs/guide/running.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,40 +52,47 @@ run a PipelineRun on CI:
5252
owns the repository.
5353
- The author of the pull request has permissions to push to branches inside the
5454
repository.
55-
5655
- The author of the pull request is listed in the `OWNERS` file located in the main
5756
directory of the default branch on GitHub or your other service provider.
5857
(see below for the OWNERS file format).
5958

60-
If the author of the pull request does not have the necessary permissions to run a
61-
PipelineRun, another user who does have the permissions can comment
62-
`/ok-to-test` on the pull request to trigger the PipelineRuns.
59+
If an unauthorized user attempts to trigger a PipelineRun through the creation
60+
of a Pull Request or by any other means, Pipelines-as-Code will block the
61+
execution and post a `'Pending'` status check. This check will inform the user
62+
that they lack the necessary permissions. Only authorized users can initiate the
63+
PipelineRun by commenting `/ok-to-test` on the pull request.
64+
65+
GitHub bot users, identified through the GitHub API, are generally exempt from
66+
the `Pending` status check that would otherwise block a pull request. This
67+
means the status check is silently ignored for bots unless they have been
68+
explicitly authorized (using [OWNERS](#owners-file) file,
69+
[Policy]({{< relref "/docs/guide/policy" >}}) or other means).
6370

6471
## OWNERS file
6572

6673
The `OWNERS` file follows a specific format similar to the Prow `OWNERS` file
6774
format (detailed at <https://www.kubernetes.dev/docs/guide/owners/>). We
6875
support a basic `OWNERS` configuration with `approvers` and `reviewers` lists,
69-
both of which have equal permissions for executing a `PipelineRun`.
76+
both of which have equal permissions for executing a `PipelineRun`.
7077

7178
If the `OWNERS` file uses `filters` instead of a simple configuration, we only
7279
consider the `.*` filter and extract the `approvers` and `reviewers` lists from
73-
it. Any other filters targeting specific files or directories are ignored.
80+
it. Any other filters targeting specific files or directories are ignored.
7481

7582
Additionally, `OWNERS_ALIASES` is supported and allows mapping alias names to
76-
lists of usernames.
83+
lists of usernames.
7784

7885
Including contributors in the `approvers` or `reviewers` lists within your
7986
`OWNERS` file grants them the ability to execute a `PipelineRun` via
80-
Pipelines-as-Code.
87+
Pipelines-as-Code.
8188

8289
For example, if your repository’s `main` or `master` branch contains the
83-
following `approvers` section:
90+
following `approvers` section:
8491

8592
```yaml
8693
approvers:
8794
- approved
88-
```
95+
```
8996

9097
The user with the username `"approved"` will have the necessary
9198
permissions.

pkg/pipelineascode/match.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,11 @@ is that what you want? make sure you use -n when generating the secret, eg: echo
136136
// trigger CI on the repository, as any user is able to comment on a pushed commit in open-source repositories.
137137
if p.event.TriggerTarget == triggertype.Push && opscomments.IsAnyOpsEventType(p.event.EventType) {
138138
status := provider.StatusOpts{
139-
Status: CompletedStatus,
140-
Title: "Permission denied",
141-
Conclusion: failureConclusion,
142-
DetailsURL: p.event.URL,
139+
Status: CompletedStatus,
140+
Title: "Permission denied",
141+
Conclusion: failureConclusion,
142+
DetailsURL: p.event.URL,
143+
AccessDenied: true,
143144
}
144145
if allowed, err := p.checkAccessOrErrror(ctx, repo, status, "by GitOps comment on push commit"); !allowed {
145146
return nil, err
@@ -151,10 +152,11 @@ is that what you want? make sure you use -n when generating the secret, eg: echo
151152
// on comment we skip it for now, we are going to check later on
152153
if p.event.TriggerTarget != triggertype.Push && p.event.EventType != opscomments.NoOpsCommentEventType.String() {
153154
status := provider.StatusOpts{
154-
Status: queuedStatus,
155-
Title: "Pending approval, waiting for an /ok-to-test",
156-
Conclusion: pendingConclusion,
157-
DetailsURL: p.event.URL,
155+
Status: queuedStatus,
156+
Title: "Pending approval, waiting for an /ok-to-test",
157+
Conclusion: pendingConclusion,
158+
DetailsURL: p.event.URL,
159+
AccessDenied: true,
158160
}
159161
if allowed, err := p.checkAccessOrErrror(ctx, repo, status, "via "+p.event.TriggerTarget.String()); !allowed {
160162
return nil, err
@@ -266,10 +268,11 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
266268
// we skipped previously so we can get the match from the event to the pipelineruns
267269
if p.event.EventType == opscomments.NoOpsCommentEventType.String() || p.event.EventType == opscomments.OnCommentEventType.String() {
268270
status := provider.StatusOpts{
269-
Status: queuedStatus,
270-
Title: "Pending approval, waiting for an /ok-to-test",
271-
Conclusion: pendingConclusion,
272-
DetailsURL: p.event.URL,
271+
Status: queuedStatus,
272+
Title: "Pending approval, waiting for an /ok-to-test",
273+
Conclusion: pendingConclusion,
274+
DetailsURL: p.event.URL,
275+
AccessDenied: true,
273276
}
274277
if allowed, err := p.checkAccessOrErrror(ctx, repo, status, "by GitOps comment on push commit"); !allowed {
275278
return nil, err
@@ -410,6 +413,7 @@ func (p *PacRun) checkAccessOrErrror(ctx context.Context, repo *v1alpha1.Reposit
410413
}
411414
p.eventEmitter.EmitMessage(repo, zap.InfoLevel, "RepositoryPermissionDenied", msg)
412415
status.Text = msg
416+
413417
if err := p.vcx.CreateStatus(ctx, p.event, status); err != nil {
414418
return false, fmt.Errorf("failed to run create status, user is not allowed to run the CI:: %w", err)
415419
}

pkg/provider/github/github.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type Provider struct {
5252
repo *v1alpha1.Repository
5353
eventEmitter *events.EventEmitter
5454
PaginedNumber int
55+
userType string // The type of user i.e bot or not
5556
skippedRun
5657
}
5758

pkg/provider/github/parse_payload.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ func (v *Provider) processEvent(ctx context.Context, event *info.Event, eventInt
276276
processedEvent.HeadBranch = processedEvent.BaseBranch // in push events Head Branch is the same as Basebranch
277277
processedEvent.BaseURL = gitEvent.GetRepo().GetHTMLURL()
278278
processedEvent.HeadURL = processedEvent.BaseURL // in push events Head URL is the same as BaseURL
279+
v.userType = gitEvent.GetSender().GetType()
279280
case *github.PullRequestEvent:
280281
processedEvent.Repository = gitEvent.GetRepo().GetName()
281282
if gitEvent.GetRepo() == nil {
@@ -291,6 +292,7 @@ func (v *Provider) processEvent(ctx context.Context, event *info.Event, eventInt
291292
processedEvent.HeadURL = gitEvent.GetPullRequest().Head.GetRepo().GetHTMLURL()
292293
processedEvent.Sender = gitEvent.GetPullRequest().GetUser().GetLogin()
293294
processedEvent.EventType = event.EventType
295+
v.userType = gitEvent.GetPullRequest().GetUser().GetType()
294296

295297
if gitEvent.Action != nil && provider.Valid(*gitEvent.Action, pullRequestLabelEvent) {
296298
processedEvent.EventType = string(triggertype.LabelUpdate)
@@ -343,6 +345,7 @@ func (v *Provider) handleReRequestEvent(ctx context.Context, event *github.Check
343345
// we allow the rerequest user here, not the push user, i guess it's
344346
// fine because you can't do a rereq without being a github owner?
345347
runevent.Sender = event.GetSender().GetLogin()
348+
v.userType = event.GetSender().GetType()
346349
return runevent, nil
347350
}
348351
runevent.PullRequestNumber = event.GetCheckRun().GetCheckSuite().PullRequests[0].GetNumber()
@@ -373,6 +376,7 @@ func (v *Provider) handleCheckSuites(ctx context.Context, event *github.CheckSui
373376
// we allow the rerequest user here, not the push user, i guess it's
374377
// fine because you can't do a rereq without being a github owner?
375378
runevent.Sender = event.GetSender().GetLogin()
379+
v.userType = event.GetSender().GetType()
376380
return runevent, nil
377381
// return nil, fmt.Errorf("check suite event is not supported for push events")
378382
}
@@ -401,6 +405,7 @@ func (v *Provider) handleIssueCommentEvent(ctx context.Context, event *github.Is
401405
if !event.GetIssue().IsPullRequest() {
402406
return info.NewEvent(), fmt.Errorf("issue comment is not coming from a pull_request")
403407
}
408+
v.userType = event.GetSender().GetType()
404409
opscomments.SetEventTypeAndTargetPR(runevent, event.GetComment().GetBody())
405410
// We are getting the full URL so we have to get the last part to get the PR number,
406411
// we don't have to care about URL query string/hash and other stuff because
@@ -424,6 +429,7 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C
424429
runevent.Organization = event.GetRepo().GetOwner().GetLogin()
425430
runevent.Repository = event.GetRepo().GetName()
426431
runevent.Sender = event.GetSender().GetLogin()
432+
v.userType = event.GetSender().GetType()
427433
runevent.URL = event.GetRepo().GetHTMLURL()
428434
runevent.SHA = event.GetComment().GetCommitID()
429435
runevent.HeadURL = runevent.URL

pkg/provider/github/status.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import (
2020
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
2121
)
2222

23+
const (
24+
botType = "Bot"
25+
pendingApproval = "Pending approval, waiting for an /ok-to-test"
26+
)
27+
2328
const taskStatusTemplate = `
2429
<table>
2530
<tr><th>Status</th><th>Duration</th><th>Name</th></tr>
@@ -35,8 +40,6 @@ const taskStatusTemplate = `
3540
{{- end }}
3641
</table>`
3742

38-
const pendingApproval = "Pending approval, waiting for an /ok-to-test"
39-
4043
func (v *Provider) getExistingCheckRunID(ctx context.Context, runevent *info.Event, status provider.StatusOpts) (*int64, error) {
4144
opt := github.ListOptions{PerPage: v.PaginedNumber}
4245
for {
@@ -350,6 +353,11 @@ func (v *Provider) CreateStatus(ctx context.Context, runevent *info.Event, statu
350353
return fmt.Errorf("cannot set status on github no token or url set")
351354
}
352355

356+
// If the request comes from a bot user, skip setting the status and just log the event silently
357+
if statusOpts.AccessDenied && v.userType == botType {
358+
return nil
359+
}
360+
353361
switch statusOpts.Conclusion {
354362
case "success":
355363
statusOpts.Title = "Success"
@@ -386,7 +394,6 @@ func (v *Provider) CreateStatus(ctx context.Context, runevent *info.Event, statu
386394
onPr = "/" + statusOpts.OriginalPipelineRunName
387395
}
388396
statusOpts.Summary = fmt.Sprintf("%s%s %s", v.pacInfo.ApplicationName, onPr, statusOpts.Summary)
389-
390397
// If we have an installationID which mean we have a github apps and we can use the checkRun API
391398
if runevent.InstallationID > 0 {
392399
return v.getOrUpdateCheckRunStatus(ctx, runevent, statusOpts)

pkg/provider/github/status_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ func TestGithubProviderCreateStatus(t *testing.T) {
248248
titleSubstr string
249249
nilCompletedAtDate bool
250250
githubApps bool
251+
accessDenied bool
252+
isBot bool
251253
}
252254
tests := []struct {
253255
name string
@@ -334,6 +336,36 @@ func TestGithubProviderCreateStatus(t *testing.T) {
334336
want: &github.CheckRun{ID: &resultid},
335337
wantErr: false,
336338
},
339+
{
340+
name: "failure from bot",
341+
args: args{
342+
runevent: runEvent,
343+
status: "completed",
344+
conclusion: "failure",
345+
text: "Nay",
346+
detailsURL: "https://cireport.com",
347+
titleSubstr: "Failed",
348+
githubApps: true,
349+
accessDenied: true,
350+
isBot: true,
351+
},
352+
wantErr: false,
353+
},
354+
{
355+
name: "success from bot",
356+
args: args{
357+
runevent: runEvent,
358+
status: "completed",
359+
conclusion: "failure",
360+
text: "Nay",
361+
detailsURL: "https://cireport.com",
362+
titleSubstr: "Failed",
363+
githubApps: true,
364+
isBot: true,
365+
},
366+
wantErr: false,
367+
want: &github.CheckRun{ID: &resultid},
368+
},
337369
{
338370
name: "skipped",
339371
args: args{
@@ -378,14 +410,18 @@ func TestGithubProviderCreateStatus(t *testing.T) {
378410
gcvs.Client = fakeclient
379411
gcvs.Logger, _ = logger.GetLogger()
380412
gcvs.Run = params.New()
413+
if tt.args.isBot {
414+
gcvs.userType = "Bot"
415+
}
381416

417+
checkRunCreated := false
382418
mux.HandleFunc("/repos/check/run/statuses/sha", func(_ http.ResponseWriter, _ *http.Request) {})
383419
mux.HandleFunc(fmt.Sprintf("/repos/check/run/check-runs/%d", checkrunid), func(rw http.ResponseWriter, r *http.Request) {
384420
bit, _ := io.ReadAll(r.Body)
385421
checkRun := &github.CheckRun{}
386422
err := json.Unmarshal(bit, checkRun)
387423
assert.NilError(t, err)
388-
424+
checkRunCreated = true
389425
if tt.args.nilCompletedAtDate {
390426
// I guess that's the way you check for an undefined year,
391427
// or maybe i don't understand fully how go works😅
@@ -402,6 +438,7 @@ func TestGithubProviderCreateStatus(t *testing.T) {
402438
_, err = fmt.Fprintf(rw, `{"id": %d}`, resultid)
403439
assert.NilError(t, err)
404440
})
441+
405442
if tt.addExistingCheckruns {
406443
tt.args.runevent.SHA = "sha"
407444
mux.HandleFunc(fmt.Sprintf("/repos/%v/%v/commits/%v/check-runs", tt.args.runevent.Organization, tt.args.runevent.Repository, tt.args.runevent.SHA), func(w http.ResponseWriter, _ *http.Request) {
@@ -430,6 +467,7 @@ func TestGithubProviderCreateStatus(t *testing.T) {
430467
Conclusion: tt.args.conclusion,
431468
Text: tt.args.text,
432469
DetailsURL: tt.args.detailsURL,
470+
AccessDenied: tt.args.accessDenied,
433471
}
434472
if tt.pr != nil {
435473
status.PipelineRun = tt.pr
@@ -469,6 +507,14 @@ func TestGithubProviderCreateStatus(t *testing.T) {
469507
t.Errorf("GithubProvider.CreateStatus() error = %v, wantErr %v", err, tt.wantErr)
470508
return
471509
}
510+
if tt.want == nil && checkRunCreated {
511+
t.Errorf("Check run should have not be created for this test")
512+
return
513+
}
514+
if tt.want != nil && !checkRunCreated {
515+
t.Errorf("Check run should have been created for this test")
516+
return
517+
}
472518
})
473519
}
474520
}

pkg/provider/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type StatusOpts struct {
2424
Summary string
2525
Title string
2626
InstanceCountForCheckRun int
27+
AccessDenied bool
2728
}
2829

2930
type Interface interface {

0 commit comments

Comments
 (0)