Skip to content

Commit c6a4c49

Browse files
committed
fix(github): recover fork PR re-runs when GitHub omits PR metadata
When someone clicks Re-run on a Pipelines as Code check for a pull request coming from a fork, GitHub can leave out the usual pull request details in the webhook payload. That left PAC with no obvious way to tell which pull request the check belonged to, so the re-run stopped with an error instead of starting again. This change teaches PAC to keep looking in a more practical way. If the first GitHub API says it cannot find the pull request for the commit, PAC now lists open pull requests in the repository and matches the one whose head commit SHA is the same. In simple terms: when GitHub does not hand us the answer directly, we now look through the open pull requests and find the one built from that exact commit. The change also checks pull request data attached directly to the check_run event before falling back to SHA-based lookup, and the tests now cover both the new fork pull request path and the empty-result error cases. Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent a6e4aab commit c6a4c49

2 files changed

Lines changed: 308 additions & 32 deletions

File tree

pkg/provider/github/parse_payload.go

Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,55 @@ func selectSingleOpenPullRequest(prs []*github.PullRequest) (*github.PullRequest
314314
}
315315
}
316316

317+
func (v *Provider) findOpenPullRequestBySHA(ctx context.Context, org, repo, sha string) (*github.PullRequest, error) {
318+
const maxPages = 10
319+
opts := &github.PullRequestListOptions{
320+
State: "open",
321+
Sort: "updated",
322+
ListOptions: github.ListOptions{PerPage: 100},
323+
}
324+
var matches []*github.PullRequest
325+
326+
for page := 0; page < maxPages; page++ {
327+
prs, resp, err := wrapAPI(v, "list_pull_requests", func() ([]*github.PullRequest, *github.Response, error) {
328+
return v.Client().PullRequests.List(ctx, org, repo, opts)
329+
})
330+
if err != nil {
331+
return nil, fmt.Errorf("failed to list open pull requests in %s/%s: %w", org, repo, err)
332+
}
333+
334+
for _, pr := range prs {
335+
if pr.GetHead().GetSHA() == sha {
336+
matches = append(matches, pr)
337+
}
338+
}
339+
340+
if resp.NextPage == 0 {
341+
break
342+
}
343+
opts.Page = resp.NextPage
344+
}
345+
346+
return selectSingleOpenPullRequest(matches)
347+
}
348+
317349
func (v *Provider) resolveReRequestPullRequest(ctx context.Context, runevent *info.Event) (*github.PullRequest, error) {
318350
prs, err := v.getPullRequestsWithCommit(ctx, runevent.SHA, runevent.Organization, runevent.Repository, false)
319351
if err != nil {
320352
return nil, err
321353
}
322354

323-
return selectSingleOpenPullRequest(prs)
355+
pr, err := selectSingleOpenPullRequest(prs)
356+
if err != nil {
357+
return nil, err
358+
}
359+
if pr != nil {
360+
return pr, nil
361+
}
362+
363+
// ListPullRequestsWithCommit may return no matches for fork PR commits.
364+
v.Logger.Infof("No PR found via commits API for SHA %s, falling back to open PR listing", runevent.SHA)
365+
return v.findOpenPullRequestBySHA(ctx, runevent.Organization, runevent.Repository, runevent.SHA)
324366
}
325367

326368
func (v *Provider) processEvent(ctx context.Context, event *info.Event, eventInt any) (*info.Event, error) {
@@ -491,44 +533,58 @@ func (v *Provider) handleReRequestEvent(ctx context.Context, event *github.Check
491533
if event.GetRepo() == nil {
492534
return nil, errors.New("error parsing payload the repository should not be nil")
493535
}
536+
checkRun := event.GetCheckRun()
537+
checkSuite := checkRun.GetCheckSuite()
538+
494539
runevent.Organization = event.GetRepo().GetOwner().GetLogin()
495540
runevent.Repository = event.GetRepo().GetName()
496541
runevent.URL = event.GetRepo().GetHTMLURL()
497542
runevent.DefaultBranch = event.GetRepo().GetDefaultBranch()
498-
runevent.SHA = event.GetCheckRun().GetCheckSuite().GetHeadSHA()
499-
runevent.HeadBranch = event.GetCheckRun().GetCheckSuite().GetHeadBranch()
500-
runevent.HeadURL = event.GetCheckRun().GetCheckSuite().GetRepository().GetHTMLURL()
501-
// If we don't have a pull_request in this it probably mean a push
502-
if len(event.GetCheckRun().GetCheckSuite().PullRequests) == 0 {
503-
// If head_branch is null, try to find a PR by SHA before assuming push
504-
if runevent.HeadBranch == "" && runevent.SHA != "" {
505-
pr, err := v.resolveReRequestPullRequest(ctx, runevent)
506-
if err != nil {
507-
return nil, fmt.Errorf("cannot determine pull request for check_run rerequest and SHA %s: %w", runevent.SHA, err)
508-
}
509-
if pr != nil {
510-
runevent.PullRequestNumber = pr.GetNumber()
511-
runevent.TriggerTarget = triggertype.PullRequest
512-
v.Logger.Infof("Recheck of PR %s/%s#%d has been requested (resolved from SHA)", runevent.Organization, runevent.Repository, runevent.PullRequestNumber)
513-
return v.populateRunEventFromPullRequest(runevent, pr), nil
514-
}
543+
runevent.SHA = checkSuite.GetHeadSHA()
544+
runevent.HeadBranch = checkSuite.GetHeadBranch()
545+
runevent.HeadURL = checkSuite.GetRepository().GetHTMLURL()
546+
547+
if len(checkSuite.PullRequests) > 0 {
548+
runevent.PullRequestNumber = checkSuite.PullRequests[0].GetNumber()
549+
runevent.TriggerTarget = triggertype.PullRequest
550+
v.Logger.Infof("Recheck of PR %s/%s#%d has been requested", runevent.Organization, runevent.Repository, runevent.PullRequestNumber)
551+
return v.getPullRequest(ctx, runevent)
552+
}
553+
554+
if len(checkRun.PullRequests) > 1 {
555+
return nil, fmt.Errorf("cannot determine pull request for check_run rerequest: found %d associated pull requests in webhook payload", len(checkRun.PullRequests))
556+
}
557+
if len(checkRun.PullRequests) == 1 {
558+
runevent.PullRequestNumber = checkRun.PullRequests[0].GetNumber()
559+
runevent.TriggerTarget = triggertype.PullRequest
560+
v.Logger.Infof("Recheck of PR %s/%s#%d has been requested (from check_run)", runevent.Organization, runevent.Repository, runevent.PullRequestNumber)
561+
return v.getPullRequest(ctx, runevent)
562+
}
563+
564+
// If head_branch is null, try to find a PR by SHA before assuming push.
565+
if runevent.HeadBranch == "" && runevent.SHA != "" {
566+
pr, err := v.resolveReRequestPullRequest(ctx, runevent)
567+
if err != nil {
568+
return nil, fmt.Errorf("cannot determine pull request for check_run rerequest and SHA %s: %w", runevent.SHA, err)
515569
}
516-
if runevent.HeadBranch == "" {
517-
return nil, fmt.Errorf("cannot determine branch for check_run rerequest: head_branch is null and no associated PR found for SHA %s", runevent.SHA)
570+
if pr != nil {
571+
runevent.PullRequestNumber = pr.GetNumber()
572+
runevent.TriggerTarget = triggertype.PullRequest
573+
v.Logger.Infof("Recheck of PR %s/%s#%d has been requested (resolved from SHA)", runevent.Organization, runevent.Repository, runevent.PullRequestNumber)
574+
return v.populateRunEventFromPullRequest(runevent, pr), nil
518575
}
519-
runevent.BaseBranch = runevent.HeadBranch
520-
runevent.BaseURL = runevent.HeadURL
521-
runevent.EventType = "push"
522-
// we allow the rerequest user here, not the push user, i guess it's
523-
// fine because you can't do a rereq without being a github owner?
524-
runevent.Sender = event.GetSender().GetLogin()
525-
v.userType = event.GetSender().GetType()
526-
return runevent, nil
527576
}
528-
runevent.PullRequestNumber = event.GetCheckRun().GetCheckSuite().PullRequests[0].GetNumber()
529-
runevent.TriggerTarget = triggertype.PullRequest
530-
v.Logger.Infof("Recheck of PR %s/%s#%d has been requested", runevent.Organization, runevent.Repository, runevent.PullRequestNumber)
531-
return v.getPullRequest(ctx, runevent)
577+
if runevent.HeadBranch == "" {
578+
return nil, fmt.Errorf("cannot determine branch for check_run rerequest: head_branch is null and no associated PR found for SHA %s", runevent.SHA)
579+
}
580+
runevent.BaseBranch = runevent.HeadBranch
581+
runevent.BaseURL = runevent.HeadURL
582+
runevent.EventType = "push"
583+
// we allow the rerequest user here, not the push user, i guess it's
584+
// fine because you can't do a rereq without being a github owner?
585+
runevent.Sender = event.GetSender().GetLogin()
586+
v.userType = event.GetSender().GetType()
587+
return runevent, nil
532588
}
533589

534590
func (v *Provider) handleCheckSuites(ctx context.Context, event *github.CheckSuiteEvent) (*info.Event, error) {

0 commit comments

Comments
 (0)