github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/requiresig/requiresig.go (about)

     1  /*
     2  Copyright 2017 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 requiresig
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"strings"
    23  
    24  	"k8s.io/test-infra/prow/github"
    25  	"k8s.io/test-infra/prow/labels"
    26  	"k8s.io/test-infra/prow/pluginhelp"
    27  	"k8s.io/test-infra/prow/plugins"
    28  
    29  	"github.com/sirupsen/logrus"
    30  )
    31  
    32  var (
    33  	labelPrefixes = []string{"sig/", "committee/", "wg/"}
    34  
    35  	sigCommandRe = regexp.MustCompile(`(?m)^/sig\s*(.*)$`)
    36  )
    37  
    38  const (
    39  	pluginName = "require-sig"
    40  
    41  	needsSIGMessage = "There are no sig labels on this issue. Please add a sig label."
    42  	needsSIGDetails = `A sig label can be added by either:
    43  
    44  1. mentioning a sig: ` + "`@kubernetes/sig-<group-name>-<group-suffix>`" + `
    45      e.g., ` + "`@kubernetes/sig-contributor-experience-<group-suffix>`" + ` to notify the contributor experience sig, OR
    46  
    47  2. specifying the label manually: ` + "`/sig <group-name>`" + `
    48      e.g., ` + "`/sig scalability`" + ` to apply the ` + "`sig/scalability`" + ` label
    49  
    50  Note: Method 1 will trigger an email to the group. See the [group list](https://git.k8s.io/community/sig-list.md).
    51  The ` + "`<group-suffix>`" + ` in method 1 has to be replaced with one of these: _**bugs, feature-requests, pr-reviews, test-failures, proposals**_`
    52  )
    53  
    54  type githubClient interface {
    55  	BotName() (string, error)
    56  	AddLabel(org, repo string, number int, label string) error
    57  	RemoveLabel(org, repo string, number int, label string) error
    58  	CreateComment(org, repo string, number int, content string) error
    59  	ListIssueComments(org, repo string, number int) ([]github.IssueComment, error)
    60  	DeleteComment(org, repo string, id int) error
    61  }
    62  
    63  type commentPruner interface {
    64  	PruneComments(shouldPrune func(github.IssueComment) bool)
    65  }
    66  
    67  func init() {
    68  	plugins.RegisterIssueHandler(pluginName, handleIssue, helpProvider)
    69  }
    70  
    71  func helpProvider(config *plugins.Configuration, _ []string) (*pluginhelp.PluginHelp, error) {
    72  	url := config.RequireSIG.GroupListURL
    73  	if url == "" {
    74  		url = "<no url provided>"
    75  	}
    76  	// Only the 'Description' and 'Config' fields are necessary because this plugin does not react
    77  	// to any commands.
    78  	return &pluginhelp.PluginHelp{
    79  			Description: fmt.Sprintf(
    80  				`When a new issue is opened the require-sig plugin adds the %q label and leaves a comment requesting that a SIG (Special Interest Group) label be added to the issue. SIG labels are labels that have one of the following prefixes: %q.
    81  <br>Once a SIG label has been added to an issue, this plugin removes the %q label and deletes the comment it made previously.`,
    82  				labels.NeedsSig,
    83  				labelPrefixes,
    84  				labels.NeedsSig,
    85  			),
    86  			Config: map[string]string{
    87  				"": fmt.Sprintf("The comment the plugin creates includes this link to a list of the existing groups: %s", url),
    88  			},
    89  		},
    90  		nil
    91  }
    92  
    93  func handleIssue(pc plugins.Agent, ie github.IssueEvent) error {
    94  	cp, err := pc.CommentPruner()
    95  	if err != nil {
    96  		return err
    97  	}
    98  	return handle(pc.Logger, pc.GitHubClient, cp, &ie, pc.PluginConfig.SigMention.Re)
    99  }
   100  
   101  func isSigLabel(label string) bool {
   102  	for i := range labelPrefixes {
   103  		if strings.HasPrefix(label, labelPrefixes[i]) {
   104  			return true
   105  		}
   106  	}
   107  	return false
   108  }
   109  
   110  func hasSigLabel(labels []github.Label) bool {
   111  	for i := range labels {
   112  		if isSigLabel(labels[i].Name) {
   113  			return true
   114  		}
   115  	}
   116  	return false
   117  }
   118  
   119  func shouldReact(mentionRe *regexp.Regexp, ie *github.IssueEvent) bool {
   120  	// Ignore PRs and closed issues.
   121  	if ie.Issue.IsPullRequest() || ie.Issue.State == "closed" {
   122  		return false
   123  	}
   124  
   125  	switch ie.Action {
   126  	case github.IssueActionOpened:
   127  		// Don't react if the new issue has a /sig command or sig team mention.
   128  		return !mentionRe.MatchString(ie.Issue.Body) && !sigCommandRe.MatchString(ie.Issue.Body)
   129  	case github.IssueActionLabeled, github.IssueActionUnlabeled:
   130  		// Only react to (un)label events for sig labels.
   131  		return isSigLabel(ie.Label.Name)
   132  	default:
   133  		return false
   134  	}
   135  }
   136  
   137  // handle is the workhorse notifying issue owner to add a sig label if there is none
   138  // The algorithm:
   139  // (1) return if this is not an opened, labelled, or unlabelled event or if the issue is closed.
   140  // (2) find if the issue has a sig label
   141  // (3) find if the issue has a needs-sig label
   142  // (4) if the issue has both the sig and needs-sig labels, remove the needs-sig label and delete the comment.
   143  // (5) if the issue has none of the labels, add the needs-sig label and comment
   144  // (6) if the issue has only the sig label, do nothing
   145  // (7) if the issue has only the needs-sig label, do nothing
   146  func handle(log *logrus.Entry, ghc githubClient, cp commentPruner, ie *github.IssueEvent, mentionRe *regexp.Regexp) error {
   147  	// Ignore PRs, closed issues, and events that aren't new issues or sig label
   148  	// changes.
   149  	if !shouldReact(mentionRe, ie) {
   150  		return nil
   151  	}
   152  
   153  	org := ie.Repo.Owner.Login
   154  	repo := ie.Repo.Name
   155  	number := ie.Issue.Number
   156  
   157  	hasSigLabel := hasSigLabel(ie.Issue.Labels)
   158  	hasNeedsSigLabel := github.HasLabel(labels.NeedsSig, ie.Issue.Labels)
   159  
   160  	if hasSigLabel && hasNeedsSigLabel {
   161  		if err := ghc.RemoveLabel(org, repo, number, labels.NeedsSig); err != nil {
   162  			log.WithError(err).Errorf("Failed to remove %s label.", labels.NeedsSig)
   163  		}
   164  		botName, err := ghc.BotName()
   165  		if err != nil {
   166  			return fmt.Errorf("error getting bot name: %v", err)
   167  		}
   168  		cp.PruneComments(shouldPrune(log, botName))
   169  	} else if !hasSigLabel && !hasNeedsSigLabel {
   170  		if err := ghc.AddLabel(org, repo, number, labels.NeedsSig); err != nil {
   171  			log.WithError(err).Errorf("Failed to add %s label.", labels.NeedsSig)
   172  		}
   173  		msg := plugins.FormatResponse(ie.Issue.User.Login, needsSIGMessage, needsSIGDetails)
   174  		if err := ghc.CreateComment(org, repo, number, msg); err != nil {
   175  			log.WithError(err).Error("Failed to create comment.")
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  // shouldPrune finds comments left by this plugin.
   182  func shouldPrune(log *logrus.Entry, botName string) func(github.IssueComment) bool {
   183  	return func(comment github.IssueComment) bool {
   184  		if comment.User.Login != botName {
   185  			return false
   186  		}
   187  		return strings.Contains(comment.Body, needsSIGMessage)
   188  	}
   189  }