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 }