github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/help/help.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 help
    18  
    19  import (
    20  	"regexp"
    21  	"strings"
    22  
    23  	"github.com/sirupsen/logrus"
    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  
    30  const pluginName = "help"
    31  
    32  var (
    33  	helpRe                     = regexp.MustCompile(`(?mi)^/help\s*$`)
    34  	helpRemoveRe               = regexp.MustCompile(`(?mi)^/remove-help\s*$`)
    35  	helpGoodFirstIssueRe       = regexp.MustCompile(`(?mi)^/good-first-issue\s*$`)
    36  	helpGoodFirstIssueRemoveRe = regexp.MustCompile(`(?mi)^/remove-good-first-issue\s*$`)
    37  	helpGuidelinesURL          = "https://git.k8s.io/community/contributors/devel/help-wanted.md"
    38  	helpMsgPruneMatch          = "This request has been marked as needing help from a contributor."
    39  	helpMsg                    = `
    40  	This request has been marked as needing help from a contributor.
    41  
    42  Please ensure the request meets the requirements listed [here](` + helpGuidelinesURL + `).
    43  
    44  If this request no longer meets these requirements, the label can be removed
    45  by commenting with the ` + "`/remove-help`" + ` command.
    46  `
    47  	goodFirstIssueMsgPruneMatch = "This request has been marked as suitable for new contributors."
    48  	goodFirstIssueMsg           = `
    49  	This request has been marked as suitable for new contributors.
    50  
    51  Please ensure the request meets the requirements listed [here](` + helpGuidelinesURL + "#good-first-issue" + `).
    52  
    53  If this request no longer meets these requirements, the label can be removed
    54  by commenting with the ` + "`/remove-good-first-issue`" + ` command.
    55  `
    56  )
    57  
    58  func init() {
    59  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
    60  }
    61  
    62  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    63  	// The Config field is omitted because this plugin is not configurable.
    64  	pluginHelp := &pluginhelp.PluginHelp{
    65  		Description: "The help plugin provides commands that add or remove the '" + labels.Help + "' and the '" + labels.GoodFirstIssue + "' labels from issues.",
    66  	}
    67  	pluginHelp.AddCommand(pluginhelp.Command{
    68  		Usage:       "/[remove-](help|good-first-issue)",
    69  		Description: "Applies or removes the '" + labels.Help + "' and '" + labels.GoodFirstIssue + "' labels to an issue.",
    70  		Featured:    false,
    71  		WhoCanUse:   "Anyone can trigger this command on a PR.",
    72  		Examples:    []string{"/help", "/remove-help", "/good-first-issue", "/remove-good-first-issue"},
    73  	})
    74  	return pluginHelp, nil
    75  }
    76  
    77  type githubClient interface {
    78  	BotName() (string, error)
    79  	CreateComment(owner, repo string, number int, comment string) error
    80  	AddLabel(owner, repo string, number int, label string) error
    81  	RemoveLabel(owner, repo string, number int, label string) error
    82  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
    83  }
    84  
    85  type commentPruner interface {
    86  	PruneComments(shouldPrune func(github.IssueComment) bool)
    87  }
    88  
    89  func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error {
    90  	cp, err := pc.CommentPruner()
    91  	if err != nil {
    92  		return err
    93  	}
    94  	return handle(pc.GitHubClient, pc.Logger, cp, &e)
    95  }
    96  
    97  func handle(gc githubClient, log *logrus.Entry, cp commentPruner, e *github.GenericCommentEvent) error {
    98  	// Only consider open issues and new comments.
    99  	if e.IsPR || e.IssueState != "open" || e.Action != github.GenericCommentActionCreated {
   100  		return nil
   101  	}
   102  
   103  	org := e.Repo.Owner.Login
   104  	repo := e.Repo.Name
   105  	commentAuthor := e.User.Login
   106  
   107  	// Determine if the issue has the help and the good-first-issue label
   108  	issueLabels, err := gc.GetIssueLabels(org, repo, e.Number)
   109  	if err != nil {
   110  		log.WithError(err).Errorf("Failed to get issue labels.")
   111  	}
   112  	hasHelp := github.HasLabel(labels.Help, issueLabels)
   113  	hasGoodFirstIssue := github.HasLabel(labels.GoodFirstIssue, issueLabels)
   114  
   115  	// If PR has help label and we're asking for it to be removed, remove label
   116  	if hasHelp && helpRemoveRe.MatchString(e.Body) {
   117  		if err := gc.RemoveLabel(org, repo, e.Number, labels.Help); err != nil {
   118  			log.WithError(err).Errorf("Github failed to remove the following label: %s", labels.Help)
   119  		}
   120  
   121  		botName, err := gc.BotName()
   122  		if err != nil {
   123  			log.WithError(err).Errorf("Failed to get bot name.")
   124  		}
   125  		cp.PruneComments(shouldPrune(log, botName, helpMsgPruneMatch))
   126  
   127  		// if it has the good-first-issue label, remove it too
   128  		if hasGoodFirstIssue {
   129  			if err := gc.RemoveLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil {
   130  				log.WithError(err).Errorf("Github failed to remove the following label: %s", labels.GoodFirstIssue)
   131  			}
   132  			cp.PruneComments(shouldPrune(log, botName, goodFirstIssueMsgPruneMatch))
   133  		}
   134  
   135  		return nil
   136  	}
   137  
   138  	// If PR does not have the good-first-issue label and we are asking for it to be added,
   139  	// add both the good-first-issue and help labels
   140  	if !hasGoodFirstIssue && helpGoodFirstIssueRe.MatchString(e.Body) {
   141  		if err := gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.IssueHTMLURL, commentAuthor, goodFirstIssueMsg)); err != nil {
   142  			log.WithError(err).Errorf("Failed to create comment \"%s\".", goodFirstIssueMsg)
   143  		}
   144  
   145  		if err := gc.AddLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil {
   146  			log.WithError(err).Errorf("Github failed to add the following label: %s", labels.GoodFirstIssue)
   147  		}
   148  
   149  		if !hasHelp {
   150  			if err := gc.AddLabel(org, repo, e.Number, labels.Help); err != nil {
   151  				log.WithError(err).Errorf("Github failed to add the following label: %s", labels.Help)
   152  			}
   153  		}
   154  
   155  		return nil
   156  	}
   157  
   158  	// If PR does not have the help label and we're asking it to be added,
   159  	// add the label
   160  	if !hasHelp && helpRe.MatchString(e.Body) {
   161  		if err := gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.IssueHTMLURL, commentAuthor, helpMsg)); err != nil {
   162  			log.WithError(err).Errorf("Failed to create comment \"%s\".", helpMsg)
   163  		}
   164  		if err := gc.AddLabel(org, repo, e.Number, labels.Help); err != nil {
   165  			log.WithError(err).Errorf("Github failed to add the following label: %s", labels.Help)
   166  		}
   167  
   168  		return nil
   169  	}
   170  
   171  	// If PR has good-first-issue label and we are asking for it to be removed,
   172  	// remove just the good-first-issue label
   173  	if hasGoodFirstIssue && helpGoodFirstIssueRemoveRe.MatchString(e.Body) {
   174  		if err := gc.RemoveLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil {
   175  			log.WithError(err).Errorf("Github failed to remove the following label: %s", labels.GoodFirstIssue)
   176  		}
   177  
   178  		botName, err := gc.BotName()
   179  		if err != nil {
   180  			log.WithError(err).Errorf("Failed to get bot name.")
   181  		}
   182  		cp.PruneComments(shouldPrune(log, botName, goodFirstIssueMsgPruneMatch))
   183  
   184  		return nil
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  // shouldPrune finds comments left by this plugin.
   191  func shouldPrune(log *logrus.Entry, botName, msgPruneMatch string) func(github.IssueComment) bool {
   192  	return func(comment github.IssueComment) bool {
   193  		if comment.User.Login != botName {
   194  			return false
   195  		}
   196  		return strings.Contains(comment.Body, msgPruneMatch)
   197  	}
   198  }