github.com/abayer/test-infra@v0.0.5/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/pluginhelp"
    26  	"k8s.io/test-infra/prow/plugins"
    27  
    28  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  var (
    32  	labelPrefixes = []string{"sig/", "committee/", "wg/"}
    33  
    34  	sigCommandRe = regexp.MustCompile(`(?m)^/sig\s*(.*)$`)
    35  )
    36  
    37  const (
    38  	pluginName    = "require-sig"
    39  	needsSigLabel = "needs-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  }
    60  
    61  type commentPruner interface {
    62  	PruneComments(shouldPrune func(github.IssueComment) bool)
    63  }
    64  
    65  func init() {
    66  	plugins.RegisterIssueHandler(pluginName, handleIssue, helpProvider)
    67  }
    68  
    69  func helpProvider(config *plugins.Configuration, _ []string) (*pluginhelp.PluginHelp, error) {
    70  	url := config.RequireSIG.GroupListURL
    71  	if url == "" {
    72  		url = "<no url provided>"
    73  	}
    74  	// Only the 'Description' and 'Config' fields are necessary because this plugin does not react
    75  	// to any commands.
    76  	return &pluginhelp.PluginHelp{
    77  			Description: fmt.Sprintf(
    78  				`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.
    79  <br>Once a SIG label has been added to an issue, this plugin removes the %q label and deletes the comment it made previously.`,
    80  				needsSigLabel,
    81  				labelPrefixes,
    82  				needsSigLabel,
    83  			),
    84  			Config: map[string]string{
    85  				"": fmt.Sprintf("The comment the plugin creates includes this link to a list of the existing groups: %s", url),
    86  			},
    87  		},
    88  		nil
    89  }
    90  
    91  func handleIssue(pc plugins.PluginClient, ie github.IssueEvent) error {
    92  	return handle(pc.Logger, pc.GitHubClient, pc.CommentPruner, &ie, pc.PluginConfig.SigMention.Re)
    93  }
    94  
    95  func isSigLabel(label string) bool {
    96  	for i := range labelPrefixes {
    97  		if strings.HasPrefix(label, labelPrefixes[i]) {
    98  			return true
    99  		}
   100  	}
   101  	return false
   102  }
   103  
   104  func hasSigLabel(labels []github.Label) bool {
   105  	for i := range labels {
   106  		if isSigLabel(labels[i].Name) {
   107  			return true
   108  		}
   109  	}
   110  	return false
   111  }
   112  
   113  func shouldReact(mentionRe *regexp.Regexp, ie *github.IssueEvent) bool {
   114  	// Ignore PRs and closed issues.
   115  	if ie.Issue.IsPullRequest() || ie.Issue.State == "closed" {
   116  		return false
   117  	}
   118  
   119  	switch ie.Action {
   120  	case github.IssueActionOpened:
   121  		// Don't react if the new issue has a /sig command or sig team mention.
   122  		return !mentionRe.MatchString(ie.Issue.Body) && !sigCommandRe.MatchString(ie.Issue.Body)
   123  	case github.IssueActionLabeled, github.IssueActionUnlabeled:
   124  		// Only react to (un)label events for sig labels.
   125  		return isSigLabel(ie.Label.Name)
   126  	default:
   127  		return false
   128  	}
   129  }
   130  
   131  // handle is the workhorse notifying issue owner to add a sig label if there is none
   132  // The algorithm:
   133  // (1) return if this is not an opened, labelled, or unlabelled event or if the issue is closed.
   134  // (2) find if the issue has a sig label
   135  // (3) find if the issue has a needs-sig label
   136  // (4) if the issue has both the sig and needs-sig labels, remove the needs-sig label and delete the comment.
   137  // (5) if the issue has none of the labels, add the needs-sig label and comment
   138  // (6) if the issue has only the sig label, do nothing
   139  // (7) if the issue has only the needs-sig label, do nothing
   140  func handle(log *logrus.Entry, ghc githubClient, cp commentPruner, ie *github.IssueEvent, mentionRe *regexp.Regexp) error {
   141  	// Ignore PRs, closed issues, and events that aren't new issues or sig label
   142  	// changes.
   143  	if !shouldReact(mentionRe, ie) {
   144  		return nil
   145  	}
   146  
   147  	org := ie.Repo.Owner.Login
   148  	repo := ie.Repo.Name
   149  	number := ie.Issue.Number
   150  
   151  	hasSigLabel := hasSigLabel(ie.Issue.Labels)
   152  	hasNeedsSigLabel := github.HasLabel(needsSigLabel, ie.Issue.Labels)
   153  
   154  	if hasSigLabel && hasNeedsSigLabel {
   155  		if err := ghc.RemoveLabel(org, repo, number, needsSigLabel); err != nil {
   156  			log.WithError(err).Errorf("Failed to remove %s label.", needsSigLabel)
   157  		}
   158  		botName, err := ghc.BotName()
   159  		if err != nil {
   160  			return fmt.Errorf("error getting bot name: %v", err)
   161  		}
   162  		cp.PruneComments(shouldPrune(log, botName))
   163  	} else if !hasSigLabel && !hasNeedsSigLabel {
   164  		if err := ghc.AddLabel(org, repo, number, needsSigLabel); err != nil {
   165  			log.WithError(err).Errorf("Failed to add %s label.", needsSigLabel)
   166  		}
   167  		msg := plugins.FormatResponse(ie.Issue.User.Login, needsSIGMessage, needsSIGDetails)
   168  		if err := ghc.CreateComment(org, repo, number, msg); err != nil {
   169  			log.WithError(err).Error("Failed to create comment.")
   170  		}
   171  	}
   172  	return nil
   173  }
   174  
   175  // shouldPrune finds comments left by this plugin.
   176  func shouldPrune(log *logrus.Entry, botName string) func(github.IssueComment) bool {
   177  	return func(comment github.IssueComment) bool {
   178  		if comment.User.Login != botName {
   179  			return false
   180  		}
   181  		return strings.Contains(comment.Body, needsSIGMessage)
   182  	}
   183  }