k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/robots/pr-labeler/main.go (about)

     1  /*
     2  Copyright 2017 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  // PR labeler provides a way to add a missing ok-to-test label on trusted PRs.
    18  //
    19  // The --token-path determines who interacts with github.
    20  // By default PR labeler runs in dry mode, add --confirm to make it leave comments.
    21  package main
    22  
    23  import (
    24  	"flag"
    25  	"log"
    26  	"net/url"
    27  
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"sigs.k8s.io/prow/pkg/config/secret"
    30  	"sigs.k8s.io/prow/pkg/flagutil"
    31  	"sigs.k8s.io/prow/pkg/github"
    32  )
    33  
    34  const (
    35  	needsOkToTest = "needs-ok-to-test"
    36  	okToTest      = "ok-to-test"
    37  )
    38  
    39  type client interface {
    40  	AddLabel(org, repo string, number int, label string) error
    41  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
    42  	GetPullRequests(org, repo string) ([]github.PullRequest, error)
    43  	ListCollaborators(org, repo string) ([]github.User, error)
    44  	ListOrgMembers(org, role string) ([]github.TeamMember, error)
    45  }
    46  
    47  type options struct {
    48  	confirm            bool
    49  	endpoint           flagutil.Strings
    50  	graphqlEndpoint    string
    51  	org                string
    52  	repo               string
    53  	tokenPath          string
    54  	trustCollaborators bool
    55  }
    56  
    57  func flagOptions() options {
    58  	o := options{
    59  		endpoint: flagutil.NewStrings(github.DefaultAPIEndpoint),
    60  	}
    61  	flag.StringVar(&o.graphqlEndpoint, "graphql-endpoint", github.DefaultGraphQLEndpoint, "github graphql endpoint")
    62  	flag.BoolVar(&o.confirm, "confirm", false, "Mutate github if set")
    63  	flag.StringVar(&o.org, "org", "", "github org")
    64  	flag.StringVar(&o.repo, "repo", "", "github repo")
    65  	flag.StringVar(&o.tokenPath, "token-path", "", "Path to github token")
    66  	flag.BoolVar(&o.trustCollaborators, "trust-collaborators", false, "Also trust PRs from collaborators")
    67  	flag.Parse()
    68  	return o
    69  }
    70  
    71  func main() {
    72  	log.SetFlags(log.LstdFlags | log.Lshortfile)
    73  	o := flagOptions()
    74  
    75  	if o.org == "" {
    76  		log.Fatal("empty --org")
    77  	}
    78  	if o.repo == "" {
    79  		log.Fatal("empty --repo")
    80  	}
    81  	if o.tokenPath == "" {
    82  		log.Fatal("empty --token-path")
    83  	}
    84  
    85  	if err := secret.Add(o.tokenPath); err != nil {
    86  		log.Fatalf("Error starting secrets agent: %v", err)
    87  	}
    88  
    89  	var err error
    90  	for _, ep := range o.endpoint.Strings() {
    91  		_, err = url.ParseRequestURI(ep)
    92  		if err != nil {
    93  			log.Fatalf("Invalid --endpoint URL %q: %v.", ep, err)
    94  		}
    95  	}
    96  
    97  	_, err = url.Parse(o.graphqlEndpoint)
    98  	if err != nil {
    99  		log.Fatalf("Invalid --graphql-endpoint URL %q: %v.", o.graphqlEndpoint, err)
   100  	}
   101  
   102  	var c client
   103  	if o.confirm {
   104  		c, err = github.NewClient(secret.GetTokenGenerator(o.tokenPath), secret.Censor, o.graphqlEndpoint, o.endpoint.Strings()...)
   105  	} else {
   106  		c, err = github.NewDryRunClient(secret.GetTokenGenerator(o.tokenPath), secret.Censor, o.graphqlEndpoint, o.endpoint.Strings()...)
   107  	}
   108  	if err != nil {
   109  		log.Fatalf("failed to construgt GitHub client: %v", err)
   110  	}
   111  
   112  	// get all open PRs
   113  	prs, err := c.GetPullRequests(o.org, o.repo)
   114  	if err != nil {
   115  		log.Fatal(err)
   116  	}
   117  
   118  	// get the list of authors to skip once and use a set for lookups
   119  	skipAuthors := sets.NewString()
   120  
   121  	// skip org members
   122  	members, err := c.ListOrgMembers(o.org, "all")
   123  	if err != nil {
   124  		log.Fatal(err)
   125  	}
   126  	for _, member := range members {
   127  		skipAuthors.Insert(member.Login)
   128  	}
   129  
   130  	// eventually also skip collaborators
   131  	if o.trustCollaborators {
   132  		collaborators, err := c.ListCollaborators(o.org, o.repo)
   133  		if err != nil {
   134  			log.Fatal(err)
   135  		}
   136  		for _, collaborator := range collaborators {
   137  			skipAuthors.Insert(collaborator.Login)
   138  		}
   139  	}
   140  
   141  	for _, pr := range prs {
   142  		// skip PRs from these authors
   143  		if skipAuthors.Has(pr.User.Login) {
   144  			continue
   145  		}
   146  		// skip PRs with *ok-to-test labels
   147  		labels, err := c.GetIssueLabels(o.org, o.repo, pr.Number)
   148  		if err != nil {
   149  			log.Fatal(err)
   150  		}
   151  		if github.HasLabel(okToTest, labels) || github.HasLabel(needsOkToTest, labels) {
   152  			continue
   153  		}
   154  		// only add ok-to-test with --confirm
   155  		if !o.confirm {
   156  			log.Println("Use --confirm to add", okToTest, "to", pr.HTMLURL)
   157  			continue
   158  		}
   159  		if err := c.AddLabel(o.org, o.repo, pr.Number, okToTest); err != nil {
   160  			log.Fatal(err)
   161  		}
   162  	}
   163  }