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  }