github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/github/fakegithub/fakegithub.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package fakegithub
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  	"sync"
    27  
    28  	githubql "github.com/shurcooL/githubv4"
    29  
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  
    32  	"sigs.k8s.io/prow/pkg/github"
    33  )
    34  
    35  const botName = "k8s-ci-robot"
    36  
    37  const (
    38  	// Bot is the exported botName
    39  	Bot = botName
    40  	// TestRef is the ref returned when calling GetRef
    41  	TestRef = "abcde"
    42  )
    43  
    44  // FakeClient is like client, but fake.
    45  type FakeClient struct {
    46  	Issues                     map[int]*github.Issue
    47  	IssueID                    int
    48  	OrgMembers                 map[string][]string
    49  	Collaborators              []string
    50  	IssueComments              map[int][]github.IssueComment
    51  	IssueCommentID             int
    52  	PullRequests               map[int]*github.PullRequest
    53  	PullRequestChanges         map[int][]github.PullRequestChange
    54  	PullRequestComments        map[int][]github.ReviewComment
    55  	PullRequestReviewCommentID int
    56  	PullRequestReviewComments  map[int][]github.ReviewComment
    57  	ReviewID                   int
    58  	Reviews                    map[int][]github.Review
    59  	CombinedStatuses           map[string]*github.CombinedStatus
    60  	CreatedStatuses            map[string][]github.Status
    61  	IssueEvents                map[int][]github.ListedIssueEvent
    62  	Commits                    map[string]github.RepositoryCommit
    63  
    64  	// All Labels That Exist In The Repo
    65  	RepoLabelsExisting []string
    66  	// org/repo#number:label
    67  	IssueLabelsAdded    []string
    68  	IssueLabelsExisting []string
    69  	IssueLabelsRemoved  []string
    70  
    71  	// org/repo#number:body
    72  	IssueCommentsAdded []string
    73  	// org/repo#issuecommentid:body
    74  	IssueCommentsEdited []string
    75  	// org/repo#issuecommentid
    76  	IssueCommentsDeleted []string
    77  
    78  	// org/repo#number:body
    79  	PullRequestReviewCommentsAdded []string
    80  
    81  	// org/repo#issuecommentid:reaction
    82  	IssueReactionsAdded   []string
    83  	CommentReactionsAdded []string
    84  
    85  	// org/repo#number:assignee
    86  	AssigneesAdded []string
    87  
    88  	// org/repo#number:milestone (represents the milestone for a specific issue)
    89  	Milestone    int
    90  	MilestoneMap map[string]int
    91  
    92  	// list of commits for each PR
    93  	// org/repo#number:[]commit
    94  	CommitMap map[string][]github.RepositoryCommit
    95  
    96  	// Fake remote git storage. File name are keys
    97  	// and values map SHA to content
    98  	RemoteFiles map[string]map[string]string
    99  
   100  	// Fake remote git storage. Directory name are keys
   101  	// and values map SHA to directory content
   102  	RemoteDirectories map[string]map[string][]github.DirectoryContent
   103  
   104  	// A list of refs that got deleted via DeleteRef
   105  	RefsDeleted []struct{ Org, Repo, Ref string }
   106  
   107  	// A map of repo names to projects
   108  	RepoProjects map[string][]github.Project
   109  
   110  	// A map of project name to columns
   111  	ProjectColumnsMap map[string][]github.ProjectColumn
   112  
   113  	// Maps column ID to the list of project cards
   114  	ColumnCardsMap map[int][]github.ProjectCard
   115  
   116  	// Maps project name to maps of column ID to columnName
   117  	ColumnIDMap map[string]map[int]string
   118  
   119  	// The project and column names for an issue or PR
   120  	Project            string
   121  	Column             string
   122  	OrgRepoIssueLabels map[string][]github.Label
   123  	OrgProjects        map[string][]github.Project
   124  
   125  	// Maps org name to the list of hooks
   126  	OrgHooks map[string][]github.Hook
   127  	// Maps repo name to the list of hooks
   128  	RepoHooks map[string][]github.Hook
   129  
   130  	// A map of invitation id to user repository invitations
   131  	UserRepoInvitations map[int]github.UserRepoInvitation
   132  	// A map of organization invitations by name
   133  	UserOrgInvitations map[string]github.UserOrgInvitation
   134  
   135  	// Error will be returned if set. Currently only implemented for CreateStatus
   136  	Error error
   137  
   138  	// GetRepoError will be returned if set when GetRepo is called
   139  	GetRepoError error
   140  
   141  	// ListIssueCommentsWithContextError will be returned if set when ListIssueCommentsWithContext is called
   142  	ListIssueCommentsWithContextError error
   143  
   144  	// WasLabelAddedByHumanVal determines the return of the method with the same name
   145  	WasLabelAddedByHumanVal bool
   146  
   147  	// lock to be thread safe
   148  	lock sync.RWMutex
   149  
   150  	// Team is a map org->teamSlug->TeamWithMembers
   151  	Teams map[string]map[string]TeamWithMembers
   152  
   153  	// Reviewers Requested
   154  	ReviewersRequested []string
   155  }
   156  
   157  type TeamWithMembers struct {
   158  	Team    github.Team
   159  	Members sets.Set[string]
   160  }
   161  
   162  func (f *FakeClient) BotUser() (*github.UserData, error) {
   163  	return &github.UserData{Login: botName}, nil
   164  }
   165  
   166  func (f *FakeClient) BotUserCheckerWithContext(_ context.Context) (func(candidate string) bool, error) {
   167  	return f.BotUserChecker()
   168  }
   169  
   170  func (f *FakeClient) BotUserChecker() (func(candidate string) bool, error) {
   171  	return func(candidate string) bool {
   172  		candidate = strings.TrimSuffix(candidate, "[bot]")
   173  		return candidate == botName
   174  	}, nil
   175  }
   176  
   177  func NewFakeClient() *FakeClient {
   178  	return &FakeClient{
   179  		Issues:              make(map[int]*github.Issue),
   180  		OrgMembers:          make(map[string][]string),
   181  		IssueComments:       make(map[int][]github.IssueComment),
   182  		PullRequests:        make(map[int]*github.PullRequest),
   183  		PullRequestChanges:  make(map[int][]github.PullRequestChange),
   184  		PullRequestComments: make(map[int][]github.ReviewComment),
   185  		Reviews:             make(map[int][]github.Review),
   186  		CombinedStatuses:    make(map[string]*github.CombinedStatus),
   187  		CreatedStatuses:     make(map[string][]github.Status),
   188  		IssueEvents:         make(map[int][]github.ListedIssueEvent),
   189  		Commits:             make(map[string]github.RepositoryCommit),
   190  
   191  		MilestoneMap: make(map[string]int),
   192  		CommitMap:    make(map[string][]github.RepositoryCommit),
   193  		RemoteFiles:  make(map[string]map[string]string),
   194  
   195  		RepoProjects:        make(map[string][]github.Project),
   196  		ProjectColumnsMap:   make(map[string][]github.ProjectColumn),
   197  		ColumnCardsMap:      make(map[int][]github.ProjectCard),
   198  		ColumnIDMap:         make(map[string]map[int]string),
   199  		OrgRepoIssueLabels:  make(map[string][]github.Label),
   200  		OrgProjects:         make(map[string][]github.Project),
   201  		OrgHooks:            make(map[string][]github.Hook),
   202  		RepoHooks:           make(map[string][]github.Hook),
   203  		UserRepoInvitations: make(map[int]github.UserRepoInvitation),
   204  		UserOrgInvitations:  make(map[string]github.UserOrgInvitation),
   205  	}
   206  }
   207  
   208  // IsMember returns true if user is in org.
   209  func (f *FakeClient) IsMember(org, user string) (bool, error) {
   210  	f.lock.RLock()
   211  	defer f.lock.RUnlock()
   212  	for _, m := range f.OrgMembers[org] {
   213  		if m == user {
   214  			return true, nil
   215  		}
   216  	}
   217  	return false, nil
   218  }
   219  
   220  func (f *FakeClient) WasLabelAddedByHuman(_, _ string, _ int, _ string) (bool, error) {
   221  	f.lock.RLock()
   222  	defer f.lock.RUnlock()
   223  	return f.WasLabelAddedByHumanVal, nil
   224  }
   225  
   226  // ListOpenIssues returns f.issues
   227  // To mock a mix of issues and pull requests, see github.Issue.PullRequest
   228  func (f *FakeClient) ListOpenIssues(org, repo string) ([]github.Issue, error) {
   229  	f.lock.RLock()
   230  	defer f.lock.RUnlock()
   231  	var issues []github.Issue
   232  	for _, issue := range f.Issues {
   233  		issues = append(issues, *issue)
   234  	}
   235  	return issues, nil
   236  }
   237  
   238  // ListIssueComments returns comments.
   239  func (f *FakeClient) ListIssueComments(owner, repo string, number int) ([]github.IssueComment, error) {
   240  	return f.ListIssueCommentsWithContext(context.Background(), owner, repo, number)
   241  }
   242  
   243  func (f *FakeClient) ListIssueCommentsWithContext(ctx context.Context, owner, repo string, number int) ([]github.IssueComment, error) {
   244  	f.lock.RLock()
   245  	defer f.lock.RUnlock()
   246  	if f.ListIssueCommentsWithContextError != nil {
   247  		return nil, f.ListIssueCommentsWithContextError
   248  	}
   249  	return append([]github.IssueComment{}, f.IssueComments[number]...), nil
   250  }
   251  
   252  // ListPullRequestComments returns review comments.
   253  func (f *FakeClient) ListPullRequestComments(owner, repo string, number int) ([]github.ReviewComment, error) {
   254  	f.lock.RLock()
   255  	defer f.lock.RUnlock()
   256  	return append([]github.ReviewComment{}, f.PullRequestComments[number]...), nil
   257  }
   258  
   259  // ListReviews returns reviews.
   260  func (f *FakeClient) ListReviews(owner, repo string, number int) ([]github.Review, error) {
   261  	f.lock.RLock()
   262  	defer f.lock.RUnlock()
   263  	return append([]github.Review{}, f.Reviews[number]...), nil
   264  }
   265  
   266  // ListIssueEvents returns issue events
   267  func (f *FakeClient) ListIssueEvents(owner, repo string, number int) ([]github.ListedIssueEvent, error) {
   268  	f.lock.RLock()
   269  	defer f.lock.RUnlock()
   270  	return append([]github.ListedIssueEvent{}, f.IssueEvents[number]...), nil
   271  }
   272  
   273  // CreateComment adds a comment to a PR
   274  func (f *FakeClient) CreateComment(owner, repo string, number int, comment string) error {
   275  	return f.CreateCommentWithContext(context.Background(), owner, repo, number, comment)
   276  }
   277  
   278  func (f *FakeClient) CreateCommentWithContext(_ context.Context, owner, repo string, number int, comment string) error {
   279  	f.lock.Lock()
   280  	defer f.lock.Unlock()
   281  	f.IssueCommentID++
   282  	f.IssueCommentsAdded = append(f.IssueCommentsAdded, fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, comment))
   283  	f.IssueComments[number] = append(f.IssueComments[number], github.IssueComment{
   284  		ID:   f.IssueCommentID,
   285  		Body: comment,
   286  		User: github.User{Login: botName},
   287  	})
   288  	return nil
   289  }
   290  
   291  // EditComment edits a comment.
   292  func (f *FakeClient) EditComment(org, repo string, ID int, comment string) error {
   293  	return f.EditCommentWithContext(context.Background(), org, repo, ID, comment)
   294  }
   295  
   296  func (f *FakeClient) EditCommentWithContext(_ context.Context, org, repo string, ID int, comment string) error {
   297  	f.lock.Lock()
   298  	defer f.lock.Unlock()
   299  	f.IssueCommentsEdited = append(f.IssueCommentsEdited, fmt.Sprintf("%s/%s#%d:%s", org, repo, ID, comment))
   300  	for _, ics := range f.IssueComments {
   301  		for _, ic := range ics {
   302  			if ic.ID == ID {
   303  				ic.Body = comment
   304  			}
   305  		}
   306  	}
   307  	return nil
   308  }
   309  
   310  // CreateReview adds a review to a PR
   311  func (f *FakeClient) CreateReview(org, repo string, number int, r github.DraftReview) error {
   312  	f.lock.Lock()
   313  	defer f.lock.Unlock()
   314  	f.ReviewID++
   315  	f.Reviews[number] = append(f.Reviews[number], github.Review{
   316  		ID:   f.ReviewID,
   317  		User: github.User{Login: botName},
   318  		Body: r.Body,
   319  	})
   320  	return nil
   321  }
   322  
   323  // CreateCommentReaction adds emoji to a comment.
   324  func (f *FakeClient) CreateCommentReaction(org, repo string, ID int, reaction string) error {
   325  	f.lock.Lock()
   326  	defer f.lock.Unlock()
   327  	f.CommentReactionsAdded = append(f.CommentReactionsAdded, fmt.Sprintf("%s/%s#%d:%s", org, repo, ID, reaction))
   328  	return nil
   329  }
   330  
   331  // CreateIssueReaction adds an emoji to an issue.
   332  func (f *FakeClient) CreateIssueReaction(org, repo string, ID int, reaction string) error {
   333  	f.lock.Lock()
   334  	defer f.lock.Unlock()
   335  	f.IssueReactionsAdded = append(f.IssueReactionsAdded, fmt.Sprintf("%s/%s#%d:%s", org, repo, ID, reaction))
   336  	return nil
   337  }
   338  
   339  // DeleteComment deletes a comment.
   340  func (f *FakeClient) DeleteComment(owner, repo string, ID int) error {
   341  	return f.DeleteCommentWithContext(context.Background(), owner, repo, ID)
   342  }
   343  
   344  func (f *FakeClient) DeleteCommentWithContext(_ context.Context, owner, repo string, ID int) error {
   345  	f.lock.Lock()
   346  	defer f.lock.Unlock()
   347  	f.IssueCommentsDeleted = append(f.IssueCommentsDeleted, fmt.Sprintf("%s/%s#%d", owner, repo, ID))
   348  	for num, ics := range f.IssueComments {
   349  		for i, ic := range ics {
   350  			if ic.ID == ID {
   351  				f.IssueComments[num] = append(ics[:i], ics[i+1:]...)
   352  				return nil
   353  			}
   354  		}
   355  	}
   356  	return fmt.Errorf("could not find issue comment %d", ID)
   357  }
   358  
   359  // DeleteStaleComments deletes comments flagged by isStale.
   360  func (f *FakeClient) DeleteStaleComments(org, repo string, number int, comments []github.IssueComment, isStale func(github.IssueComment) bool) error {
   361  	return f.DeleteStaleCommentsWithContext(context.Background(), org, repo, number, comments, isStale)
   362  }
   363  
   364  // DeleteStaleCommentsWithContext deletes comments flagged by isStale with a provided context.
   365  func (f *FakeClient) DeleteStaleCommentsWithContext(ctx context.Context, org, repo string, number int, comments []github.IssueComment, isStale func(github.IssueComment) bool) error {
   366  	if comments == nil {
   367  		comments, _ = f.ListIssueComments(org, repo, number)
   368  	}
   369  	for _, comment := range comments {
   370  		if isStale(comment) {
   371  			if err := f.DeleteComment(org, repo, comment.ID); err != nil {
   372  				return fmt.Errorf("failed to delete stale comment with ID '%d'", comment.ID)
   373  			}
   374  		}
   375  	}
   376  	return nil
   377  }
   378  
   379  // GetPullRequest returns details about the PR.
   380  func (f *FakeClient) GetPullRequest(owner, repo string, number int) (*github.PullRequest, error) {
   381  	f.lock.RLock()
   382  	defer f.lock.RUnlock()
   383  	val, exists := f.PullRequests[number]
   384  	if !exists {
   385  		return nil, fmt.Errorf("pull request number %d does not exist", number)
   386  	}
   387  	return val, nil
   388  }
   389  
   390  // EditPullRequest edits the pull request.
   391  func (f *FakeClient) EditPullRequest(org, repo string, number int, issue *github.PullRequest) (*github.PullRequest, error) {
   392  	f.lock.Lock()
   393  	defer f.lock.Unlock()
   394  	if _, exists := f.PullRequests[number]; !exists {
   395  		return nil, fmt.Errorf("issue number %d does not exist", number)
   396  	}
   397  	f.PullRequests[number] = issue
   398  	return issue, nil
   399  }
   400  
   401  // GetIssue returns the issue.
   402  func (f *FakeClient) GetIssue(owner, repo string, number int) (*github.Issue, error) {
   403  	f.lock.RLock()
   404  	defer f.lock.RUnlock()
   405  	val, exists := f.Issues[number]
   406  	if !exists {
   407  		return nil, fmt.Errorf("issue number %d does not exist", number)
   408  	}
   409  	return val, nil
   410  }
   411  
   412  // EditIssue edits the issue.
   413  func (f *FakeClient) EditIssue(org, repo string, number int, issue *github.Issue) (*github.Issue, error) {
   414  	f.lock.Lock()
   415  	defer f.lock.Unlock()
   416  	if _, exists := f.Issues[number]; !exists {
   417  		return nil, fmt.Errorf("issue number %d does not exist", number)
   418  	}
   419  	f.Issues[number] = issue
   420  	return issue, nil
   421  }
   422  
   423  // CreateIssue creates the issue.
   424  func (f *FakeClient) CreateIssue(org, repo, title, body string, milestone int, labels, assignees []string) (int, error) {
   425  	f.lock.Lock()
   426  	defer f.lock.Unlock()
   427  	f.IssueID++
   428  	if f.Issues == nil {
   429  		f.Issues = make(map[int]*github.Issue)
   430  	}
   431  	var ls []github.Label
   432  	for _, l := range labels {
   433  		ls = append(ls, github.Label{Name: l})
   434  	}
   435  	var as []github.User
   436  	for _, a := range assignees {
   437  		as = append(as, github.User{Name: a})
   438  	}
   439  	new := &github.Issue{
   440  		ID:        f.IssueID,
   441  		Title:     title,
   442  		Body:      body,
   443  		Milestone: github.Milestone{Number: milestone},
   444  		Labels:    ls,
   445  		Assignees: as,
   446  	}
   447  	f.Issues[f.IssueID] = new
   448  	f.IssueComments[f.IssueID] = make([]github.IssueComment, 0)
   449  	return new.ID, nil
   450  }
   451  
   452  func (f *FakeClient) CloseIssue(org, repo string, number int) error {
   453  	f.lock.Lock()
   454  	defer f.lock.Unlock()
   455  
   456  	if _, ok := f.Issues[number]; !ok {
   457  		return fmt.Errorf("issue number %d does not exist", number)
   458  	}
   459  
   460  	f.Issues[number].State = "closed"
   461  	f.Issues[number].StateReason = "completed"
   462  
   463  	return nil
   464  }
   465  
   466  func (f *FakeClient) CloseIssueAsNotPlanned(org, repo string, number int) error {
   467  	f.lock.Lock()
   468  	defer f.lock.Unlock()
   469  
   470  	if _, ok := f.Issues[number]; !ok {
   471  		return fmt.Errorf("issue number %d does not exist", number)
   472  	}
   473  
   474  	f.Issues[number].State = "closed"
   475  	f.Issues[number].StateReason = "not_planned"
   476  
   477  	return nil
   478  }
   479  
   480  // GetPullRequestChanges returns the file modifications in a PR.
   481  func (f *FakeClient) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) {
   482  	f.lock.RLock()
   483  	defer f.lock.RUnlock()
   484  	return f.PullRequestChanges[number], nil
   485  }
   486  
   487  // GetRef returns the hash of a ref.
   488  func (f *FakeClient) GetRef(owner, repo, ref string) (string, error) {
   489  	return TestRef, nil
   490  }
   491  
   492  // DeleteRef returns an error indicating if deletion of the given ref was successful
   493  func (f *FakeClient) DeleteRef(owner, repo, ref string) error {
   494  	f.lock.Lock()
   495  	defer f.lock.Unlock()
   496  	f.RefsDeleted = append(f.RefsDeleted, struct{ Org, Repo, Ref string }{Org: owner, Repo: repo, Ref: ref})
   497  	return nil
   498  }
   499  
   500  // GetSingleCommit returns a single commit.
   501  func (f *FakeClient) GetSingleCommit(org, repo, SHA string) (github.RepositoryCommit, error) {
   502  	f.lock.RLock()
   503  	defer f.lock.RUnlock()
   504  	return f.Commits[SHA], nil
   505  }
   506  
   507  // CreateStatus adds a status context to a commit.
   508  func (f *FakeClient) CreateStatus(owner, repo, SHA string, s github.Status) error {
   509  	return f.CreateStatusWithContext(context.Background(), owner, repo, SHA, s)
   510  }
   511  func (f *FakeClient) CreateStatusWithContext(_ context.Context, owner, repo, SHA string, s github.Status) error {
   512  	f.lock.Lock()
   513  	defer f.lock.Unlock()
   514  	if f.Error != nil {
   515  		return f.Error
   516  	}
   517  	if f.CreatedStatuses == nil {
   518  		f.CreatedStatuses = make(map[string][]github.Status)
   519  	}
   520  	statuses := f.CreatedStatuses[SHA]
   521  	var updated bool
   522  	for i := range statuses {
   523  		if statuses[i].Context == s.Context {
   524  			statuses[i] = s
   525  			updated = true
   526  		}
   527  	}
   528  	if !updated {
   529  		statuses = append(statuses, s)
   530  	}
   531  	f.CreatedStatuses[SHA] = statuses
   532  	f.CombinedStatuses[SHA] = &github.CombinedStatus{
   533  		SHA:      SHA,
   534  		Statuses: statuses,
   535  	}
   536  	return nil
   537  }
   538  
   539  // ListStatuses returns individual status contexts on a commit.
   540  func (f *FakeClient) ListStatuses(org, repo, ref string) ([]github.Status, error) {
   541  	f.lock.RLock()
   542  	defer f.lock.RUnlock()
   543  	return f.CreatedStatuses[ref], nil
   544  }
   545  
   546  // GetCombinedStatus returns the overall status for a commit.
   547  func (f *FakeClient) GetCombinedStatus(owner, repo, ref string) (*github.CombinedStatus, error) {
   548  	f.lock.RLock()
   549  	defer f.lock.RUnlock()
   550  	return f.CombinedStatuses[ref], nil
   551  }
   552  
   553  // GetRepoLabels gets labels in a repo.
   554  func (f *FakeClient) GetRepoLabels(owner, repo string) ([]github.Label, error) {
   555  	f.lock.RLock()
   556  	defer f.lock.RUnlock()
   557  	la := []github.Label{}
   558  	for _, l := range f.RepoLabelsExisting {
   559  		la = append(la, github.Label{Name: l})
   560  	}
   561  	return la, nil
   562  }
   563  
   564  // AddRepoLabel adds a defined label given org/repo
   565  func (f *FakeClient) AddRepoLabel(org, repo, label, description, color string) error {
   566  	f.lock.RLock()
   567  	defer f.lock.RUnlock()
   568  
   569  	f.RepoLabelsExisting = append(f.RepoLabelsExisting, label)
   570  	return nil
   571  }
   572  
   573  // GetIssueLabels gets labels on an issue
   574  func (f *FakeClient) GetIssueLabels(owner, repo string, number int) ([]github.Label, error) {
   575  	f.lock.RLock()
   576  	defer f.lock.RUnlock()
   577  	re := regexp.MustCompile(fmt.Sprintf(`^%s/%s#%d:(.*)$`, owner, repo, number))
   578  	la := []github.Label{}
   579  	allLabels := sets.New[string](f.IssueLabelsExisting...)
   580  	allLabels.Insert(f.IssueLabelsAdded...)
   581  	allLabels.Delete(f.IssueLabelsRemoved...)
   582  	for _, l := range sets.List(allLabels) {
   583  		groups := re.FindStringSubmatch(l)
   584  		if groups != nil {
   585  			la = append(la, github.Label{Name: groups[1]})
   586  		}
   587  	}
   588  	return la, nil
   589  }
   590  
   591  // AddLabel adds a label
   592  func (f *FakeClient) AddLabel(owner, repo string, number int, label string) error {
   593  	return f.AddLabelsWithContext(context.Background(), owner, repo, number, label)
   594  }
   595  
   596  // AddLabelWithContext adds a label with a provided context
   597  func (f *FakeClient) AddLabelWithContext(ctx context.Context, owner, repo string, number int, label string) error {
   598  	return f.AddLabelsWithContext(context.Background(), owner, repo, number, label)
   599  }
   600  
   601  // AddLabels adds a list of labels
   602  func (f *FakeClient) AddLabels(owner, repo string, number int, labels ...string) error {
   603  	return f.AddLabelsWithContext(context.Background(), owner, repo, number, labels...)
   604  }
   605  
   606  // AddLabelsWithContext adds a list of labels with a provided context
   607  func (f *FakeClient) AddLabelsWithContext(ctx context.Context, owner, repo string, number int, labels ...string) error {
   608  	f.lock.Lock()
   609  	defer f.lock.Unlock()
   610  	for _, label := range labels {
   611  		labelString := fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, label)
   612  		if sets.New[string](f.IssueLabelsAdded...).Has(labelString) {
   613  			return fmt.Errorf("cannot add %v to %s/%s/#%d", label, owner, repo, number)
   614  		}
   615  		if f.RepoLabelsExisting == nil {
   616  			f.IssueLabelsAdded = append(f.IssueLabelsAdded, labelString)
   617  			continue
   618  		}
   619  
   620  		var repoLabelExists bool
   621  		for _, l := range f.RepoLabelsExisting {
   622  			if label == l {
   623  				f.IssueLabelsAdded = append(f.IssueLabelsAdded, labelString)
   624  				repoLabelExists = true
   625  				break
   626  			}
   627  		}
   628  		if !repoLabelExists {
   629  			return fmt.Errorf("cannot add %v to %s/%s/#%d", label, owner, repo, number)
   630  		}
   631  	}
   632  	return nil
   633  }
   634  
   635  // RemoveLabel removes a label
   636  func (f *FakeClient) RemoveLabel(owner, repo string, number int, label string) error {
   637  	return f.RemoveLabelWithContext(context.Background(), owner, repo, number, label)
   638  }
   639  
   640  // RemoveLabelWithContext removes a label with a provided context
   641  func (f *FakeClient) RemoveLabelWithContext(ctx context.Context, owner, repo string, number int, label string) error {
   642  	f.lock.Lock()
   643  	defer f.lock.Unlock()
   644  	labelString := fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, label)
   645  	if !sets.NewString(f.IssueLabelsRemoved...).Has(labelString) {
   646  		f.IssueLabelsRemoved = append(f.IssueLabelsRemoved, labelString)
   647  		return nil
   648  	}
   649  	return fmt.Errorf("cannot remove %v from %s/%s/#%d", label, owner, repo, number)
   650  }
   651  
   652  // FindIssues returns the same results as FindIssuesWithOrg
   653  func (f *FakeClient) FindIssues(query, sort string, asc bool) ([]github.Issue, error) {
   654  	return f.FindIssuesWithOrg("", query, sort, asc)
   655  }
   656  
   657  // FindIssuesWithOrg returns f.Issues
   658  func (f *FakeClient) FindIssuesWithOrg(org, query, sort string, asc bool) ([]github.Issue, error) {
   659  	f.lock.RLock()
   660  	defer f.lock.RUnlock()
   661  	var issues []github.Issue
   662  	for _, issue := range f.Issues {
   663  		issues = append(issues, *issue)
   664  	}
   665  	for _, pr := range f.PullRequests {
   666  		issues = append(issues, github.Issue{
   667  			User:   pr.User,
   668  			Number: pr.Number,
   669  		})
   670  	}
   671  	return issues, nil
   672  }
   673  
   674  // AssignIssue adds assignees.
   675  func (f *FakeClient) AssignIssue(owner, repo string, number int, assignees []string) error {
   676  	f.lock.Lock()
   677  	defer f.lock.Unlock()
   678  	var m github.MissingUsers
   679  	for _, a := range assignees {
   680  		if a == "not-in-the-org" {
   681  			m.Users = append(m.Users, a)
   682  			continue
   683  		}
   684  		f.AssigneesAdded = append(f.AssigneesAdded, fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, a))
   685  	}
   686  	if m.Users == nil {
   687  		return nil
   688  	}
   689  	return m
   690  }
   691  
   692  // GetFile returns the bytes of the file.
   693  func (f *FakeClient) GetFile(org, repo, file, commit string) ([]byte, error) {
   694  	f.lock.RLock()
   695  	defer f.lock.RUnlock()
   696  	contents, ok := f.RemoteFiles[file]
   697  	if !ok {
   698  		return nil, fmt.Errorf("could not find file %s", file)
   699  	}
   700  	if commit == "" {
   701  		if master, ok := contents["master"]; ok {
   702  			return []byte(master), nil
   703  		}
   704  
   705  		return nil, fmt.Errorf("could not find file %s in master", file)
   706  	}
   707  
   708  	if content, ok := contents[commit]; ok {
   709  		return []byte(content), nil
   710  	}
   711  
   712  	return nil, fmt.Errorf("could not find file %s with ref %s", file, commit)
   713  }
   714  
   715  // ListTeams return a list of fake teams that correspond to the fake team members returned by ListTeamMembers
   716  func (f *FakeClient) ListTeams(org string) ([]github.Team, error) {
   717  	f.lock.RLock()
   718  	defer f.lock.RUnlock()
   719  	return []github.Team{
   720  		{
   721  			ID:   0,
   722  			Slug: "admins",
   723  			Name: "Admins",
   724  		},
   725  		{
   726  			ID:   42,
   727  			Slug: "leads",
   728  			Name: "Leads",
   729  		},
   730  	}, nil
   731  }
   732  
   733  // ListTeamMembers return a fake team with a single "sig-lead" GitHub teammember
   734  func (f *FakeClient) ListTeamMembers(org string, teamID int, role string) ([]github.TeamMember, error) {
   735  	f.lock.RLock()
   736  	defer f.lock.RUnlock()
   737  	if role != github.RoleAll {
   738  		return nil, fmt.Errorf("unsupported role %v (only all supported)", role)
   739  	}
   740  	teams := map[int][]github.TeamMember{
   741  		0:  {{Login: "default-sig-lead"}},
   742  		42: {{Login: "sig-lead"}},
   743  	}
   744  	members, ok := teams[teamID]
   745  	if !ok {
   746  		return []github.TeamMember{}, nil
   747  	}
   748  	return members, nil
   749  }
   750  
   751  // ListTeamMembers return a fake team with a single "sig-lead" GitHub teammember
   752  func (f *FakeClient) ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error) {
   753  	f.lock.RLock()
   754  	defer f.lock.RUnlock()
   755  	if role != github.RoleAll {
   756  		return nil, fmt.Errorf("unsupported role %v (only all supported)", role)
   757  	}
   758  	teams := map[string][]github.TeamMember{
   759  		"admins": {{Login: "default-sig-lead"}},
   760  		"leads":  {{Login: "sig-lead"}},
   761  	}
   762  	members, ok := teams[teamSlug]
   763  	if !ok {
   764  		return []github.TeamMember{}, nil
   765  	}
   766  	return members, nil
   767  }
   768  
   769  func (f *FakeClient) TeamBySlugHasMember(org string, teamSlug string, memberLogin string) (bool, error) {
   770  	f.lock.RLock()
   771  	defer f.lock.RUnlock()
   772  	if f.Teams[org] != nil {
   773  		return f.Teams[org][teamSlug].Members.Has(memberLogin), nil
   774  	}
   775  	return false, nil
   776  }
   777  
   778  // IsCollaborator returns true if the user is a collaborator of the repo.
   779  func (f *FakeClient) IsCollaborator(org, repo, login string) (bool, error) {
   780  	f.lock.RLock()
   781  	defer f.lock.RUnlock()
   782  	normed := github.NormLogin(login)
   783  	for _, collab := range f.Collaborators {
   784  		if github.NormLogin(collab) == normed {
   785  			return true, nil
   786  		}
   787  	}
   788  	return false, nil
   789  }
   790  
   791  // ListCollaborators lists the collaborators.
   792  func (f *FakeClient) ListCollaborators(org, repo string) ([]github.User, error) {
   793  	f.lock.RLock()
   794  	defer f.lock.RUnlock()
   795  	result := make([]github.User, 0, len(f.Collaborators))
   796  	for _, login := range f.Collaborators {
   797  		result = append(result, github.User{Login: login})
   798  	}
   799  	return result, nil
   800  }
   801  
   802  // ClearMilestone removes the milestone
   803  func (f *FakeClient) ClearMilestone(org, repo string, issueNum int) error {
   804  	f.Milestone = 0
   805  	return nil
   806  }
   807  
   808  // SetMilestone sets the milestone.
   809  func (f *FakeClient) SetMilestone(org, repo string, issueNum, milestoneNum int) error {
   810  	f.lock.Lock()
   811  	defer f.lock.Unlock()
   812  	if milestoneNum < 0 {
   813  		return fmt.Errorf("Milestone Numbers Cannot Be Negative")
   814  	}
   815  	f.Milestone = milestoneNum
   816  	return nil
   817  }
   818  
   819  // ListMilestones lists milestones.
   820  func (f *FakeClient) ListMilestones(org, repo string) ([]github.Milestone, error) {
   821  	f.lock.RLock()
   822  	defer f.lock.RUnlock()
   823  	milestones := []github.Milestone{}
   824  	for k, v := range f.MilestoneMap {
   825  		milestones = append(milestones, github.Milestone{Title: k, Number: v, State: "open"})
   826  	}
   827  	return milestones, nil
   828  }
   829  
   830  // ListPullRequestCommits lists commits for a given PR.
   831  func (f *FakeClient) ListPullRequestCommits(org, repo string, prNumber int) ([]github.RepositoryCommit, error) {
   832  	f.lock.RLock()
   833  	defer f.lock.RUnlock()
   834  	k := fmt.Sprintf("%s/%s#%d", org, repo, prNumber)
   835  	return f.CommitMap[k], nil
   836  }
   837  
   838  // GetRepoProjects returns the list of projects under a repo.
   839  func (f *FakeClient) GetRepoProjects(owner, repo string) ([]github.Project, error) {
   840  	f.lock.Lock()
   841  	defer f.lock.Unlock()
   842  	return f.RepoProjects[fmt.Sprintf("%s/%s", owner, repo)], nil
   843  }
   844  
   845  // GetOrgProjects returns the list of projects under an org
   846  func (f *FakeClient) GetOrgProjects(org string) ([]github.Project, error) {
   847  	f.lock.RLock()
   848  	defer f.lock.RUnlock()
   849  	return f.RepoProjects[fmt.Sprintf("%s/*", org)], nil
   850  }
   851  
   852  // GetProjectColumns returns the list of columns for a given project.
   853  func (f *FakeClient) GetProjectColumns(org string, projectID int) ([]github.ProjectColumn, error) {
   854  	f.lock.RLock()
   855  	defer f.lock.RUnlock()
   856  	// Get project name
   857  	for _, projects := range f.RepoProjects {
   858  		for _, project := range projects {
   859  			if projectID == project.ID {
   860  				return f.ProjectColumnsMap[project.Name], nil
   861  			}
   862  		}
   863  	}
   864  	return nil, fmt.Errorf("Cannot find project ID")
   865  }
   866  
   867  // CreateProjectCard creates a project card under a given column.
   868  func (f *FakeClient) CreateProjectCard(org string, columnID int, projectCard github.ProjectCard) (*github.ProjectCard, error) {
   869  	cards, err := f.GetColumnProjectCards(org, columnID)
   870  	if err != nil {
   871  		return nil, err
   872  	}
   873  	f.lock.Lock()
   874  	defer f.lock.Unlock()
   875  
   876  	for project, columnIDMap := range f.ColumnIDMap {
   877  		if _, exists := columnIDMap[columnID]; exists {
   878  			for id := range columnIDMap {
   879  				// Make sure that we behave same as github API
   880  				// Create project will generate an error when the card already exist in the project
   881  				for _, existingCard := range cards {
   882  					if existingCard.ContentURL == projectCard.ContentURL {
   883  						return nil, fmt.Errorf("Card already exist in the project: %s, column %d, cannot add to column  %d", project, id, columnID)
   884  					}
   885  				}
   886  			}
   887  		}
   888  		columnName, exists := columnIDMap[columnID]
   889  		if exists {
   890  			f.ColumnCardsMap[columnID] = append(
   891  				f.ColumnCardsMap[columnID],
   892  				projectCard,
   893  			)
   894  			f.Column = columnName
   895  			f.Project = project
   896  			return &projectCard, nil
   897  		}
   898  	}
   899  	return nil, fmt.Errorf("Provided column %d does not exist, ColumnIDMap is %v", columnID, f.ColumnIDMap)
   900  }
   901  
   902  // DeleteProjectCard deletes the project card of a specific issue or PR
   903  func (f *FakeClient) DeleteProjectCard(org string, projectCardID int) error {
   904  	f.lock.Lock()
   905  	defer f.lock.Unlock()
   906  	if f.ColumnCardsMap == nil {
   907  		return fmt.Errorf("Project card doesn't exist")
   908  	}
   909  	f.Project = ""
   910  	f.Column = ""
   911  	newCards := []github.ProjectCard{}
   912  	oldColumnID := -1
   913  	for column, cards := range f.ColumnCardsMap {
   914  		removalIndex := -1
   915  		for i, existingCard := range cards {
   916  			if existingCard.ContentID == projectCardID {
   917  				oldColumnID = column
   918  				removalIndex = i
   919  				break
   920  			}
   921  		}
   922  		if removalIndex != -1 {
   923  			newCards = cards
   924  			newCards[removalIndex] = newCards[len(newCards)-1]
   925  			newCards = newCards[:len(newCards)-1]
   926  			break
   927  		}
   928  	}
   929  	// Update the old column's list of project cards
   930  	if oldColumnID != -1 {
   931  		f.ColumnCardsMap[oldColumnID] = newCards
   932  	}
   933  	return nil
   934  }
   935  
   936  // GetColumnProjectCards fetches project cards  under given column
   937  func (f *FakeClient) GetColumnProjectCards(org string, columnID int) ([]github.ProjectCard, error) {
   938  	f.lock.RLock()
   939  	if f.ColumnCardsMap == nil {
   940  		f.ColumnCardsMap = make(map[int][]github.ProjectCard)
   941  	}
   942  	res := f.ColumnCardsMap[columnID]
   943  	f.lock.RUnlock()
   944  	return res, nil
   945  }
   946  
   947  // GetColumnProjectCard fetches project card if the content_url in the card matched the issue/pr
   948  func (f *FakeClient) GetColumnProjectCard(org string, columnID int, contentURL string) (*github.ProjectCard, error) {
   949  	cards, err := f.GetColumnProjectCards(org, columnID)
   950  	if err != nil {
   951  		return nil, err
   952  	}
   953  	for _, existingCard := range cards {
   954  		if existingCard.ContentURL == contentURL {
   955  			return &existingCard, nil
   956  		}
   957  	}
   958  	return nil, nil
   959  }
   960  
   961  func (f *FakeClient) GetRepos(org string, isUser bool) ([]github.Repo, error) {
   962  	return []github.Repo{
   963  		{
   964  			Owner: github.User{
   965  				Login: "kubernetes",
   966  			},
   967  			Name: "kubernetes",
   968  		},
   969  		{
   970  			Owner: github.User{
   971  				Login: "kubernetes",
   972  			},
   973  			Name: "community",
   974  		},
   975  	}, nil
   976  }
   977  
   978  func (f *FakeClient) GetRepo(owner, name string) (github.FullRepo, error) {
   979  	if f.GetRepoError != nil {
   980  		return github.FullRepo{}, f.GetRepoError
   981  	}
   982  	return github.FullRepo{
   983  		Repo: github.Repo{
   984  			Owner:         github.User{Login: owner},
   985  			Name:          name,
   986  			HasIssues:     true,
   987  			HasWiki:       true,
   988  			DefaultBranch: "master",
   989  			Description:   fmt.Sprintf("Test Repo: %s", name),
   990  		},
   991  	}, nil
   992  }
   993  
   994  // MoveProjectCard moves a specific project card to a specified column in the same project
   995  func (f *FakeClient) MoveProjectCard(org string, projectCardID int, newColumnID int) error {
   996  	f.lock.Lock()
   997  	defer f.lock.Unlock()
   998  	// Remove project card from old column
   999  	newCards := []github.ProjectCard{}
  1000  	oldColumnID := -1
  1001  	projectCard := github.ProjectCard{}
  1002  	for column, cards := range f.ColumnCardsMap {
  1003  		removalIndex := -1
  1004  		for i, existingCard := range cards {
  1005  			if existingCard.ContentID == projectCardID {
  1006  				oldColumnID = column
  1007  				removalIndex = i
  1008  				projectCard = existingCard
  1009  				break
  1010  			}
  1011  		}
  1012  		if removalIndex != -1 {
  1013  			newCards = cards
  1014  			newCards[removalIndex] = newCards[len(newCards)-1]
  1015  			newCards = newCards[:len(newCards)-1]
  1016  		}
  1017  	}
  1018  	if oldColumnID != -1 {
  1019  		// Update the old column's list of project cards
  1020  		f.ColumnCardsMap[oldColumnID] = newCards
  1021  	}
  1022  
  1023  	for project, columnIDMap := range f.ColumnIDMap {
  1024  		if columnName, exists := columnIDMap[newColumnID]; exists {
  1025  			// Add project card to new column
  1026  			f.ColumnCardsMap[newColumnID] = append(
  1027  				f.ColumnCardsMap[newColumnID],
  1028  				projectCard,
  1029  			)
  1030  			f.Column = columnName
  1031  			f.Project = project
  1032  			break
  1033  		}
  1034  	}
  1035  
  1036  	return nil
  1037  }
  1038  
  1039  // TeamHasMember checks if a user belongs to a team
  1040  func (f *FakeClient) TeamHasMember(org string, teamID int, memberLogin string) (bool, error) {
  1041  	teamMembers, _ := f.ListTeamMembers(org, teamID, github.RoleAll)
  1042  	for _, member := range teamMembers {
  1043  		if member.Login == memberLogin {
  1044  			return true, nil
  1045  		}
  1046  	}
  1047  	return false, nil
  1048  }
  1049  
  1050  func (f *FakeClient) GetTeamBySlug(slug string, org string) (*github.Team, error) {
  1051  	teams, _ := f.ListTeams(org)
  1052  	for _, team := range teams {
  1053  		if team.Name == slug {
  1054  			return &team, nil
  1055  		}
  1056  	}
  1057  	return &github.Team{}, nil
  1058  }
  1059  
  1060  func (f *FakeClient) CreatePullRequest(org, repo, title, body, head, base string, canModify bool) (int, error) {
  1061  	f.lock.Lock()
  1062  	defer f.lock.Unlock()
  1063  	if f.PullRequests == nil {
  1064  		f.PullRequests = map[int]*github.PullRequest{}
  1065  	}
  1066  	if f.Issues == nil {
  1067  		f.Issues = map[int]*github.Issue{}
  1068  	}
  1069  	for i := 0; i < 999; i++ {
  1070  		if f.PullRequests[i] != nil || f.Issues[i] != nil {
  1071  			continue
  1072  		}
  1073  		f.PullRequests[i] = &github.PullRequest{
  1074  			Number: i,
  1075  			Base: github.PullRequestBranch{
  1076  				Ref:  base,
  1077  				Repo: github.Repo{Owner: github.User{Login: org}, Name: repo},
  1078  			},
  1079  		}
  1080  		f.Issues[i] = &github.Issue{Number: i}
  1081  		return i, nil
  1082  	}
  1083  
  1084  	return 0, errors.New("FakeClient supports only 999 PullRequests")
  1085  }
  1086  
  1087  func (f *FakeClient) UpdatePullRequest(org, repo string, number int, title, body *string, open *bool, branch *string, canModify *bool) error {
  1088  	f.lock.Lock()
  1089  	defer f.lock.Unlock()
  1090  	pr, found := f.PullRequests[number]
  1091  	if !found {
  1092  		return fmt.Errorf("no pr with number %d found", number)
  1093  	}
  1094  	if title != nil {
  1095  		pr.Title = *title
  1096  	}
  1097  	if body != nil {
  1098  		pr.Body = *body
  1099  	}
  1100  	return nil
  1101  }
  1102  
  1103  // Query simply exists to allow the fake client to match the interface for packages that need it.
  1104  // It does not modify the passed interface at all.
  1105  func (f *FakeClient) Query(ctx context.Context, q interface{}, vars map[string]interface{}) error {
  1106  	return nil
  1107  }
  1108  
  1109  // GetDirectory returns the contents of the file.
  1110  func (f *FakeClient) GetDirectory(org, repo, dir, commit string) ([]github.DirectoryContent, error) {
  1111  	contents, ok := f.RemoteDirectories[dir]
  1112  	if !ok {
  1113  		return nil, fmt.Errorf("could not find dir %s", dir)
  1114  	}
  1115  	if commit == "" {
  1116  		if master, ok := contents["master"]; ok {
  1117  			return master, nil
  1118  		}
  1119  
  1120  		return nil, fmt.Errorf("could not find dir %s in master", dir)
  1121  	}
  1122  
  1123  	if content, ok := contents[commit]; ok {
  1124  		return content, nil
  1125  	}
  1126  
  1127  	return nil, fmt.Errorf("could not find dir %s with ref %s", dir, commit)
  1128  }
  1129  
  1130  // CreatePullRequestReviewComment adds a comment on a PR.
  1131  func (f *FakeClient) CreatePullRequestReviewComment(owner, repo string, number int, rc github.ReviewComment) error {
  1132  	f.lock.Lock()
  1133  	defer f.lock.Unlock()
  1134  	f.PullRequestReviewCommentID++
  1135  	f.PullRequestReviewCommentsAdded = append(f.PullRequestReviewCommentsAdded, fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, rc.Body))
  1136  	f.PullRequestReviewComments[number] = append(f.PullRequestReviewComments[number], rc)
  1137  	return nil
  1138  }
  1139  
  1140  func (f *FakeClient) ListCurrentUserRepoInvitations() ([]github.UserRepoInvitation, error) {
  1141  	var ret []github.UserRepoInvitation
  1142  	for _, inv := range f.UserRepoInvitations {
  1143  		ret = append(ret, inv)
  1144  	}
  1145  
  1146  	sort.Slice(ret, func(p, q int) bool {
  1147  		return ret[p].InvitationID < ret[q].InvitationID
  1148  	})
  1149  
  1150  	return ret, nil
  1151  }
  1152  
  1153  func (f *FakeClient) AcceptUserRepoInvitation(invitationID int) error {
  1154  	if _, ok := f.UserRepoInvitations[invitationID]; !ok {
  1155  		return fmt.Errorf("couldn't find invitation id: %d", invitationID)
  1156  	}
  1157  
  1158  	delete(f.UserRepoInvitations, invitationID)
  1159  	return nil
  1160  }
  1161  
  1162  func (f *FakeClient) AcceptUserOrgInvitation(org string) error {
  1163  	if _, ok := f.UserOrgInvitations[org]; !ok {
  1164  		return fmt.Errorf("couldn't find invitation for org: %s", org)
  1165  	}
  1166  
  1167  	delete(f.UserOrgInvitations, org)
  1168  	return nil
  1169  }
  1170  
  1171  func (f *FakeClient) ListCurrentUserOrgInvitations() ([]github.UserOrgInvitation, error) {
  1172  	var ret []github.UserOrgInvitation
  1173  	for _, inv := range f.UserOrgInvitations {
  1174  		ret = append(ret, inv)
  1175  	}
  1176  
  1177  	sort.Slice(ret, func(p, q int) bool {
  1178  		return ret[p].Org.Login < ret[q].Org.Login
  1179  	})
  1180  
  1181  	return ret, nil
  1182  }
  1183  
  1184  func (f *FakeClient) MutateWithGitHubAppsSupport(ctx context.Context, m interface{}, input githubql.Input, vars map[string]interface{}, org string) error {
  1185  	return nil
  1186  }
  1187  
  1188  func (f *FakeClient) GetFailedActionRunsByHeadBranch(org, repo, branchName, headSHA string) ([]github.WorkflowRun, error) {
  1189  	return []github.WorkflowRun{}, nil
  1190  }
  1191  
  1192  func (f *FakeClient) TriggerGitHubWorkflow(org, repo string, id int) error {
  1193  	return nil
  1194  }
  1195  
  1196  func (f *FakeClient) TriggerFailedGitHubWorkflow(org, repo string, id int) error {
  1197  	return nil
  1198  }
  1199  
  1200  func (f *FakeClient) RequestReview(org, repo string, number int, logins []string) error {
  1201  	f.ReviewersRequested = logins
  1202  	return nil
  1203  }