github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/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  	"strings"
    25  
    26  	"github.com/sirupsen/logrus"
    27  
    28  	"k8s.io/test-infra/prow/github"
    29  	"k8s.io/test-infra/prow/pluginhelp"
    30  	"k8s.io/test-infra/prow/plugins"
    31  )
    32  
    33  const (
    34  	pluginName            = "welcome"
    35  	defaultWelcomeMessage = "Welcome @{{.AuthorLogin}}! It looks like this is your first PR to {{.Org}}/{{.Repo}} 🎉"
    36  )
    37  
    38  // PRInfo contains info used provided to the welcome message template
    39  type PRInfo struct {
    40  	Org         string
    41  	Repo        string
    42  	AuthorLogin string
    43  	AuthorName  string
    44  }
    45  
    46  func init() {
    47  	plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider)
    48  }
    49  
    50  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    51  	welcomeConfig := map[string]string{}
    52  	for _, repo := range enabledRepos {
    53  		parts := strings.Split(repo, "/")
    54  		if len(parts) != 2 {
    55  			return nil, fmt.Errorf("invalid repo in enabledRepos: %q", repo)
    56  		}
    57  		messageTemplate := welcomeMessageForRepo(config, parts[0], parts[1])
    58  		welcomeConfig[repo] = fmt.Sprintf("The welcome plugin is configured to post using following welcome template: %s.", messageTemplate)
    59  	}
    60  
    61  	// The {WhoCanUse, Usage, Examples} fields are omitted because this plugin is not triggered with commands.
    62  	return &pluginhelp.PluginHelp{
    63  			Description: "The welcome plugin posts a welcoming message when it detects a user's first contribution to a repo.",
    64  			Config:      welcomeConfig,
    65  		},
    66  		nil
    67  }
    68  
    69  type githubClient interface {
    70  	CreateComment(owner, repo string, number int, comment string) error
    71  	FindIssues(query, sort string, asc bool) ([]github.Issue, error)
    72  }
    73  
    74  type client struct {
    75  	GitHubClient githubClient
    76  	Logger       *logrus.Entry
    77  }
    78  
    79  func getClient(pc plugins.Agent) client {
    80  	return client{
    81  		GitHubClient: pc.GitHubClient,
    82  		Logger:       pc.Logger,
    83  	}
    84  }
    85  
    86  func handlePullRequest(pc plugins.Agent, pre github.PullRequestEvent) error {
    87  	return handlePR(getClient(pc), pre, welcomeMessageForRepo(pc.PluginConfig, pre.Repo.Owner.Login, pre.Repo.Name))
    88  }
    89  
    90  func handlePR(c client, pre github.PullRequestEvent, welcomeTemplate string) error {
    91  	// Only consider newly opened PRs
    92  	if pre.Action != github.PullRequestActionOpened {
    93  		return nil
    94  	}
    95  
    96  	// search for PRs from the author in this repo
    97  	org := pre.PullRequest.Base.Repo.Owner.Login
    98  	repo := pre.PullRequest.Base.Repo.Name
    99  	user := pre.PullRequest.User.Login
   100  	query := fmt.Sprintf("is:pr repo:%s/%s author:%s", org, repo, user)
   101  	issues, err := c.GitHubClient.FindIssues(query, "", false)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	// if there are no results, this is the first! post the welcome comment
   107  	if len(issues) == 0 || len(issues) == 1 && issues[0].Number == pre.Number {
   108  		// load the template, and run it over the PR info
   109  		parsedTemplate, err := template.New("welcome").Parse(welcomeTemplate)
   110  		if err != nil {
   111  			return err
   112  		}
   113  		var msgBuffer bytes.Buffer
   114  		err = parsedTemplate.Execute(&msgBuffer, PRInfo{
   115  			Org:         org,
   116  			Repo:        repo,
   117  			AuthorLogin: user,
   118  			AuthorName:  pre.PullRequest.User.Name,
   119  		})
   120  		if err != nil {
   121  			return err
   122  		}
   123  
   124  		// actually post the comment
   125  		return c.GitHubClient.CreateComment(org, repo, pre.PullRequest.Number, msgBuffer.String())
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  func welcomeMessageForRepo(config *plugins.Configuration, org, repo string) string {
   132  	opts := optionsForRepo(config, org, repo)
   133  	if opts.MessageTemplate != "" {
   134  		return opts.MessageTemplate
   135  	}
   136  	return defaultWelcomeMessage
   137  }
   138  
   139  // optionsForRepo gets the plugins.Welcome struct that is applicable to the indicated repo.
   140  func optionsForRepo(config *plugins.Configuration, org, repo string) *plugins.Welcome {
   141  	fullName := fmt.Sprintf("%s/%s", org, repo)
   142  
   143  	// First search for repo config
   144  	for _, c := range config.Welcome {
   145  		if !strInSlice(fullName, c.Repos) {
   146  			continue
   147  		}
   148  		return &c
   149  	}
   150  
   151  	// If you don't find anything, loop again looking for an org config
   152  	for _, c := range config.Welcome {
   153  		if !strInSlice(org, c.Repos) {
   154  			continue
   155  		}
   156  		return &c
   157  	}
   158  
   159  	// Return an empty config, and default to defaultWelcomeMessage
   160  	return &plugins.Welcome{}
   161  }
   162  
   163  func strInSlice(str string, slice []string) bool {
   164  	for _, elem := range slice {
   165  		if elem == str {
   166  			return true
   167  		}
   168  	}
   169  	return false
   170  }