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