sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/slackevents/slackevents.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 slackevents 18 19 import ( 20 "fmt" 21 "regexp" 22 "strings" 23 24 "github.com/sirupsen/logrus" 25 26 "k8s.io/apimachinery/pkg/util/sets" 27 "sigs.k8s.io/prow/pkg/config" 28 "sigs.k8s.io/prow/pkg/github" 29 "sigs.k8s.io/prow/pkg/pluginhelp" 30 "sigs.k8s.io/prow/pkg/plugins" 31 ) 32 33 const ( 34 pluginName = "slackevents" 35 ) 36 37 var sigMatcher = regexp.MustCompile(`(?m)@kubernetes/sig-([\w-]*)-(misc|test-failures|bugs|feature-requests|proposals|pr-reviews|api-reviews)`) 38 39 type slackClient interface { 40 WriteMessage(text string, channel string) error 41 } 42 43 type githubClient interface { 44 BotUserChecker() (func(candidate string) bool, error) 45 } 46 47 type client struct { 48 GitHubClient githubClient 49 SlackClient slackClient 50 SlackConfig plugins.Slack 51 } 52 53 func init() { 54 plugins.RegisterPushEventHandler(pluginName, handlePush, helpProvider) 55 plugins.RegisterGenericCommentHandler(pluginName, handleComment, helpProvider) 56 } 57 58 func helpProvider(config *plugins.Configuration, enabledRepos []config.OrgRepo) (*pluginhelp.PluginHelp, error) { 59 configInfo := map[string]string{ 60 "": fmt.Sprintf("SIG mentions on GitHub are reiterated for the following SIG Slack channels: %s.", strings.Join(config.Slack.MentionChannels, ", ")), 61 } 62 for _, repo := range enabledRepos { 63 mw := getMergeWarning(config.Slack.MergeWarnings, repo) 64 if mw != nil { 65 configInfo[repo.String()] = fmt.Sprintf("In this repo merges are considered "+ 66 "manual and trigger manual merge warnings if the user who merged is not "+ 67 "a member of this universal exemption list: %s or merged to a branch they "+ 68 "are not specifically exempted for: %#v.<br>Warnings are sent to the "+ 69 "following Slack channels: %s.", strings.Join(mw.ExemptUsers, ", "), 70 mw.ExemptBranches, strings.Join(mw.Channels, ", ")) 71 } else { 72 configInfo[repo.String()] = "There are no manual merge warnings configured for this repo." 73 } 74 } 75 yamlSnippet, err := plugins.CommentMap.GenYaml(&plugins.Configuration{ 76 Slack: plugins.Slack{ 77 MentionChannels: []string{ 78 "channel1", 79 "channel2", 80 }, 81 MergeWarnings: []plugins.MergeWarning{ 82 { 83 Repos: []string{ 84 "org/repo1", 85 "org/repo2", 86 }, 87 Channels: []string{ 88 "channel3", 89 "channel4", 90 }, 91 ExemptUsers: []string{ 92 "alice", 93 "bob", 94 }, 95 ExemptBranches: map[string][]string{ 96 "dev": { 97 "james", 98 "joe", 99 }, 100 }, 101 }, 102 }, 103 }, 104 }) 105 if err != nil { 106 logrus.WithError(err).Warnf("cannot generate comments for %s plugin", pluginName) 107 } 108 return &pluginhelp.PluginHelp{ 109 Description: `The slackevents plugin reacts to various GitHub events by commenting in Slack channels. 110 <ol><li>The plugin can create comments to alert on manual merges. Manual merges are merges made by a normal user instead of a bot or trusted user.</li> 111 <li>The plugin can create comments to reiterate SIG mentions like '@kubernetes/sig-testing-bugs' from GitHub.</li></ol>`, 112 Config: configInfo, 113 Snippet: yamlSnippet, 114 }, 115 nil 116 } 117 118 func handleComment(pc plugins.Agent, e github.GenericCommentEvent) error { 119 c := client{ 120 GitHubClient: pc.GitHubClient, 121 SlackConfig: pc.PluginConfig.Slack, 122 SlackClient: pc.SlackClient, 123 } 124 return echoToSlack(c, e) 125 } 126 127 func handlePush(pc plugins.Agent, pe github.PushEvent) error { 128 c := client{ 129 GitHubClient: pc.GitHubClient, 130 SlackConfig: pc.PluginConfig.Slack, 131 SlackClient: pc.SlackClient, 132 } 133 return notifyOnSlackIfManualMerge(c, pe) 134 } 135 136 func notifyOnSlackIfManualMerge(pc client, pe github.PushEvent) error { 137 //Fetch MergeWarning for the repo we received the merge event. 138 if mw := getMergeWarning(pc.SlackConfig.MergeWarnings, config.OrgRepo{Org: pe.Repo.Owner.Login, Repo: pe.Repo.Name}); mw != nil { 139 //If the MergeWarning exemption list has the merge user then no need to send a message. 140 if ok := isExempted(mw, pe); !ok { 141 var message string 142 switch { 143 case pe.Created: 144 message = fmt.Sprintf("*Warning:* %s (<@%s>) pushed a new branch (%s): %s", pe.Sender.Login, pe.Sender.Login, pe.Branch(), pe.Compare) 145 case pe.Deleted: 146 message = fmt.Sprintf("*Warning:* %s (<@%s>) deleted a branch (%s): %s", pe.Sender.Login, pe.Sender.Login, pe.Branch(), pe.Compare) 147 case pe.Forced: 148 message = fmt.Sprintf("*Warning:* %s (<@%s>) *force* merged %d commit(s) into %s: %s", pe.Sender.Login, pe.Sender.Login, len(pe.Commits), pe.Branch(), pe.Compare) 149 default: 150 message = fmt.Sprintf("*Warning:* %s (<@%s>) manually merged %d commit(s) into %s: %s", pe.Sender.Login, pe.Sender.Login, len(pe.Commits), pe.Branch(), pe.Compare) 151 } 152 for _, channel := range mw.Channels { 153 if err := pc.SlackClient.WriteMessage(message, channel); err != nil { 154 return err 155 } 156 } 157 } 158 } 159 return nil 160 } 161 162 func isExempted(mw *plugins.MergeWarning, pe github.PushEvent) bool { 163 exemptedLogins := sets.Set[string]{} 164 for _, login := range append(mw.ExemptUsers, mw.ExemptBranches[pe.Branch()]...) { 165 exemptedLogins.Insert(github.NormLogin(login)) 166 } 167 168 return exemptedLogins.HasAny(github.NormLogin(pe.Pusher.Name), github.NormLogin(pe.Sender.Login)) 169 } 170 171 func getMergeWarning(mergeWarnings []plugins.MergeWarning, repo config.OrgRepo) *plugins.MergeWarning { 172 // First search for repo config 173 for _, mw := range mergeWarnings { 174 if !sets.NewString(mw.Repos...).Has(repo.String()) { 175 continue 176 } 177 return &mw 178 } 179 180 // If you don't find anything, loop again looking for an org config 181 for _, mw := range mergeWarnings { 182 if !sets.NewString(mw.Repos...).Has(repo.Org) { 183 continue 184 } 185 return &mw 186 } 187 188 return nil 189 } 190 191 func echoToSlack(pc client, e github.GenericCommentEvent) error { 192 // Ignore bot comments and comments that aren't new. 193 botUserChecker, err := pc.GitHubClient.BotUserChecker() 194 if err != nil { 195 return err 196 } 197 if botUserChecker(e.User.Login) { 198 return nil 199 } 200 if e.Action != github.GenericCommentActionCreated { 201 return nil 202 } 203 204 sigMatches := sigMatcher.FindAllStringSubmatch(e.Body, -1) 205 206 for _, match := range sigMatches { 207 sig := "sig-" + match[1] 208 // Check if this sig is a slack channel that should be messaged. 209 found := false 210 for _, channel := range pc.SlackConfig.MentionChannels { 211 if channel == sig { 212 found = true 213 break 214 } 215 } 216 if !found { 217 continue 218 } 219 220 msg := fmt.Sprintf("%s was mentioned by %s (<@%s>) on GitHub. (%s)\n>>>%s", sig, e.User.Login, e.User.Login, e.HTMLURL, e.Body) 221 if err := pc.SlackClient.WriteMessage(msg, sig); err != nil { 222 return fmt.Errorf("Failed to send message on slack channel: %q with message %q. Err: %w", sig, msg, err) 223 } 224 } 225 return nil 226 }