sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/welcome/welcome.go (about)

     1  /*
     2  Copyright 2018 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 welcome implements a prow plugin to welcome new contributors
    18  package welcome
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"html/template"
    24  
    25  	"github.com/sirupsen/logrus"
    26  
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"sigs.k8s.io/prow/pkg/config"
    29  	"sigs.k8s.io/prow/pkg/github"
    30  	"sigs.k8s.io/prow/pkg/pluginhelp"
    31  	"sigs.k8s.io/prow/pkg/plugins"
    32  	"sigs.k8s.io/prow/pkg/plugins/trigger"
    33  )
    34  
    35  const (
    36  	pluginName            = "welcome"
    37  	defaultWelcomeMessage = "Welcome @{{.AuthorLogin}}! It looks like this is your first PR to {{.Org}}/{{.Repo}} 🎉"
    38  )
    39  
    40  // PRInfo contains info used provided to the welcome message template
    41  type PRInfo struct {
    42  	Org         string
    43  	Repo        string
    44  	AuthorLogin string
    45  	AuthorName  string
    46  }
    47  
    48  func init() {
    49  	plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider)
    50  }
    51  
    52  func helpProvider(config *plugins.Configuration, enabledRepos []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
    53  	welcomeConfig := map[string]string{}
    54  	for _, repo := range enabledRepos {
    55  		messageTemplate := welcomeMessageForRepo(optionsForRepo(config, repo.Org, repo.Repo))
    56  		welcomeConfig[repo.String()] = fmt.Sprintf("The welcome plugin is configured to post using following welcome template: %s.", messageTemplate)
    57  	}
    58  
    59  	// The {WhoCanUse, Usage, Examples} fields are omitted because this plugin is not triggered with commands.
    60  	yamlSnippet, err := plugins.CommentMap.GenYaml(&plugins.Configuration{
    61  		Welcome: []plugins.Welcome{
    62  			{
    63  				Repos: []string{
    64  					"org/repo1",
    65  					"org/repo2",
    66  				},
    67  				MessageTemplate: "Welcome @{{.AuthorLogin}}!",
    68  				AlwaysPost:      false,
    69  			},
    70  		},
    71  	})
    72  	if err != nil {
    73  		logrus.WithError(err).Warnf("cannot generate comments for %s plugin", pluginName)
    74  	}
    75  	return &pluginhelp.PluginHelp{
    76  			Description: "The welcome plugin greets incoming PRs with a welcoming message.",
    77  			Config:      welcomeConfig,
    78  			Snippet:     yamlSnippet,
    79  		},
    80  		nil
    81  }
    82  
    83  type githubClient interface {
    84  	CreateComment(owner, repo string, number int, comment string) error
    85  	FindIssuesWithOrg(org, query, sort string, asc bool) ([]github.Issue, error)
    86  	IsCollaborator(org, repo, user string) (bool, error)
    87  	IsMember(org, user string) (bool, error)
    88  	BotUserChecker() (func(candidate string) bool, error)
    89  }
    90  
    91  type client struct {
    92  	GitHubClient githubClient
    93  	Logger       *logrus.Entry
    94  }
    95  
    96  func getClient(pc plugins.Agent) client {
    97  	return client{
    98  		GitHubClient: pc.GitHubClient,
    99  		Logger:       pc.Logger,
   100  	}
   101  }
   102  
   103  func handlePullRequest(pc plugins.Agent, pre github.PullRequestEvent) error {
   104  	t := pc.PluginConfig.TriggerFor(pre.PullRequest.Base.Repo.Owner.Login, pre.PullRequest.Base.Repo.Name)
   105  	options := optionsForRepo(pc.PluginConfig, pre.Repo.Owner.Login, pre.Repo.Name)
   106  	return handlePR(getClient(pc), t, pre, welcomeMessageForRepo(options), options.AlwaysPost)
   107  }
   108  
   109  func handlePR(c client, t plugins.Trigger, pre github.PullRequestEvent, welcomeTemplate string, alwaysPost bool) error {
   110  	// Only consider newly opened PRs
   111  	if pre.Action != github.PullRequestActionOpened {
   112  		return nil
   113  	}
   114  
   115  	org := pre.PullRequest.Base.Repo.Owner.Login
   116  	repo := pre.PullRequest.Base.Repo.Name
   117  	user := pre.PullRequest.User.Login
   118  	pullRequestNumber := pre.PullRequest.Number
   119  
   120  	log := c.Logger.WithFields(logrus.Fields{"org": org, "repo": repo, "user": user, "number": pullRequestNumber})
   121  
   122  	// ignore bots, we can't query their PRs
   123  	if pre.PullRequest.User.Type != github.UserTypeUser {
   124  		log.Debug("Ignoring bot user, as querying their PRs is not possible")
   125  		return nil
   126  	}
   127  
   128  	trustedResponse, err := trigger.TrustedUser(c.GitHubClient, t.OnlyOrgMembers, t.TrustedApps, t.TrustedOrg, user, org, repo)
   129  	if err != nil {
   130  		return fmt.Errorf("check if user %s is trusted: %w", user, err)
   131  	}
   132  	if !alwaysPost && trustedResponse.IsTrusted {
   133  		log.Debug("User is trusted. Skipping their welcome message")
   134  		return nil
   135  	}
   136  
   137  	// search for PRs from the author in this repo
   138  	query := fmt.Sprintf("is:pr repo:%s/%s author:%s", org, repo, user)
   139  	issues, err := c.GitHubClient.FindIssuesWithOrg(org, query, "", false)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	// if there are no results, or if configured to greet any PR - post the welcome comment
   145  	if alwaysPost || len(issues) == 0 || len(issues) == 1 && issues[0].Number == pre.Number {
   146  		// load the template, and run it over the PR info
   147  		parsedTemplate, err := template.New("welcome").Parse(welcomeTemplate)
   148  		if err != nil {
   149  			return err
   150  		}
   151  		var msgBuffer bytes.Buffer
   152  		err = parsedTemplate.Execute(&msgBuffer, PRInfo{
   153  			Org:         org,
   154  			Repo:        repo,
   155  			AuthorLogin: user,
   156  			AuthorName:  pre.PullRequest.User.Name,
   157  		})
   158  		if err != nil {
   159  			return err
   160  		}
   161  
   162  		log.Debug("Posting a welcome message for pull request")
   163  		return c.GitHubClient.CreateComment(org, repo, pullRequestNumber, msgBuffer.String())
   164  	} else {
   165  		log.WithField("issues_count", len(issues)).Debug("Ignoring PR, as user already has previous contributions")
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  func welcomeMessageForRepo(options *plugins.Welcome) string {
   172  	if options.MessageTemplate != "" {
   173  		return options.MessageTemplate
   174  	}
   175  	return defaultWelcomeMessage
   176  }
   177  
   178  // optionsForRepo gets the plugins.Welcome struct that is applicable to the indicated repo.
   179  func optionsForRepo(config *plugins.Configuration, org, repo string) *plugins.Welcome {
   180  	fullName := fmt.Sprintf("%s/%s", org, repo)
   181  
   182  	// First search for repo config
   183  	for _, c := range config.Welcome {
   184  		if !sets.NewString(c.Repos...).Has(fullName) {
   185  			continue
   186  		}
   187  		return &c
   188  	}
   189  
   190  	// If you don't find anything, loop again looking for an org config
   191  	for _, c := range config.Welcome {
   192  		if !sets.NewString(c.Repos...).Has(org) {
   193  			continue
   194  		}
   195  		return &c
   196  	}
   197  
   198  	// Return an empty config, and default to defaultWelcomeMessage
   199  	return &plugins.Welcome{}
   200  }