github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/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  	"fmt"
    21  	"regexp"
    22  	"strings"
    23  
    24  	"github.com/sirupsen/logrus"
    25  	"sigs.k8s.io/prow/pkg/config"
    26  	"sigs.k8s.io/prow/pkg/github"
    27  	"sigs.k8s.io/prow/pkg/labels"
    28  	"sigs.k8s.io/prow/pkg/pluginhelp"
    29  	"sigs.k8s.io/prow/pkg/plugins"
    30  )
    31  
    32  const pluginName = "help"
    33  
    34  var (
    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  	helpMsgPruneMatch           = "This request has been marked as needing help from a contributor."
    40  	goodFirstIssueMsgPruneMatch = "This request has been marked as suitable for new contributors."
    41  )
    42  
    43  type issueGuidelines struct {
    44  	issueGuidelinesURL     string
    45  	issueGuidelinesSummary string
    46  }
    47  
    48  func (ig issueGuidelines) helpMsg() string {
    49  	if len(ig.issueGuidelinesSummary) != 0 {
    50  		return ig.helpMsgWithGuidelineSummary()
    51  	}
    52  	return `
    53  	This request has been marked as needing help from a contributor.
    54  
    55  Please ensure the request meets the requirements listed [here](` + ig.issueGuidelinesURL + `).
    56  
    57  If this request no longer meets these requirements, the label can be removed
    58  by commenting with the ` + "`/remove-help`" + ` command.
    59  `
    60  }
    61  
    62  func (ig issueGuidelines) helpMsgWithGuidelineSummary() string {
    63  	return fmt.Sprintf(`
    64  	This request has been marked as needing help from a contributor.
    65  
    66  ### Guidelines
    67  %s
    68  
    69  For more details on the requirements of such an issue, please see [here](%s) and ensure that they are met.
    70  
    71  If this request no longer meets these requirements, the label can be removed
    72  by commenting with the `+"`/remove-help`"+` command.
    73  `, ig.issueGuidelinesSummary, ig.issueGuidelinesURL)
    74  }
    75  
    76  func (ig issueGuidelines) goodFirstIssueMsg() string {
    77  	if len(ig.issueGuidelinesSummary) != 0 {
    78  		return ig.goodFirstIssueMsgWithGuidelinesSummary()
    79  	}
    80  	return `
    81  	This request has been marked as suitable for new contributors.
    82  
    83  Please ensure the request meets the requirements listed [here](` + ig.issueGuidelinesURL + "#good-first-issue" + `).
    84  
    85  If this request no longer meets these requirements, the label can be removed
    86  by commenting with the ` + "`/remove-good-first-issue`" + ` command.
    87  `
    88  }
    89  
    90  func (ig issueGuidelines) goodFirstIssueMsgWithGuidelinesSummary() string {
    91  	return fmt.Sprintf(`
    92  	This request has been marked as suitable for new contributors.
    93  
    94  ### Guidelines
    95  %s
    96  
    97  For more details on the requirements of such an issue, please see [here](%s#good-first-issue) and ensure that they are met.
    98  
    99  If this request no longer meets these requirements, the label can be removed
   100  by commenting with the `+"`/remove-good-first-issue`"+` command.
   101  `, ig.issueGuidelinesSummary, ig.issueGuidelinesURL)
   102  }
   103  
   104  func init() {
   105  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
   106  }
   107  
   108  func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
   109  	// The Config field is omitted because this plugin is not configurable.
   110  	pluginHelp := &pluginhelp.PluginHelp{
   111  		Description: "The help plugin provides commands that add or remove the '" + labels.Help + "' and the '" + labels.GoodFirstIssue + "' labels from issues.",
   112  	}
   113  	pluginHelp.AddCommand(pluginhelp.Command{
   114  		Usage:       "/[remove-](help|good-first-issue)",
   115  		Description: "Applies or removes the '" + labels.Help + "' and '" + labels.GoodFirstIssue + "' labels to an issue.",
   116  		Featured:    false,
   117  		WhoCanUse:   "Anyone can trigger this command on a PR.",
   118  		Examples:    []string{"/help", "/remove-help", "/good-first-issue", "/remove-good-first-issue"},
   119  	})
   120  	return pluginHelp, nil
   121  }
   122  
   123  type githubClient interface {
   124  	BotUserChecker() (func(candidate string) bool, error)
   125  	CreateComment(owner, repo string, number int, comment string) error
   126  	AddLabel(owner, repo string, number int, label string) error
   127  	RemoveLabel(owner, repo string, number int, label string) error
   128  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
   129  }
   130  
   131  type commentPruner interface {
   132  	PruneComments(shouldPrune func(github.IssueComment) bool)
   133  }
   134  
   135  func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error {
   136  	cfg := pc.PluginConfig
   137  	cp, err := pc.CommentPruner()
   138  	if err != nil {
   139  		return err
   140  	}
   141  	ig := issueGuidelines{
   142  		issueGuidelinesURL:     cfg.Help.HelpGuidelinesURL,
   143  		issueGuidelinesSummary: cfg.Help.HelpGuidelinesSummary,
   144  	}
   145  	return handle(pc.GitHubClient, pc.Logger, cp, &e, ig)
   146  }
   147  
   148  func handle(gc githubClient, log *logrus.Entry, cp commentPruner, e *github.GenericCommentEvent, ig issueGuidelines) error {
   149  	// Only consider open issues and new comments.
   150  	if e.IsPR || e.IssueState != "open" || e.Action != github.GenericCommentActionCreated {
   151  		return nil
   152  	}
   153  
   154  	org := e.Repo.Owner.Login
   155  	repo := e.Repo.Name
   156  	commentAuthor := e.User.Login
   157  
   158  	// Determine if the issue has the help and the good-first-issue label
   159  	issueLabels, err := gc.GetIssueLabels(org, repo, e.Number)
   160  	if err != nil {
   161  		log.WithError(err).Errorf("Failed to get issue labels.")
   162  	}
   163  	hasHelp := github.HasLabel(labels.Help, issueLabels)
   164  	hasGoodFirstIssue := github.HasLabel(labels.GoodFirstIssue, issueLabels)
   165  
   166  	// If PR has help label and we're asking for it to be removed, remove label
   167  	if hasHelp && helpRemoveRe.MatchString(e.Body) {
   168  		if err := gc.RemoveLabel(org, repo, e.Number, labels.Help); err != nil {
   169  			log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.Help)
   170  		}
   171  
   172  		botUserChecker, err := gc.BotUserChecker()
   173  		if err != nil {
   174  			log.WithError(err).Errorf("Failed to get bot name.")
   175  		}
   176  		cp.PruneComments(shouldPrune(log, botUserChecker, helpMsgPruneMatch))
   177  
   178  		// if it has the good-first-issue label, remove it too
   179  		if hasGoodFirstIssue {
   180  			if err := gc.RemoveLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil {
   181  				log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.GoodFirstIssue)
   182  			}
   183  			cp.PruneComments(shouldPrune(log, botUserChecker, goodFirstIssueMsgPruneMatch))
   184  		}
   185  
   186  		return nil
   187  	}
   188  
   189  	// If PR does not have the good-first-issue label and we are asking for it to be added,
   190  	// add both the good-first-issue and help labels
   191  	if !hasGoodFirstIssue && helpGoodFirstIssueRe.MatchString(e.Body) {
   192  		if err := gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.IssueHTMLURL, commentAuthor, ig.goodFirstIssueMsg())); err != nil {
   193  			log.WithError(err).Errorf("Failed to create comment \"%s\".", ig.goodFirstIssueMsg())
   194  		}
   195  
   196  		if err := gc.AddLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil {
   197  			log.WithError(err).Errorf("GitHub failed to add the following label: %s", labels.GoodFirstIssue)
   198  		}
   199  
   200  		if !hasHelp {
   201  			if err := gc.AddLabel(org, repo, e.Number, labels.Help); err != nil {
   202  				log.WithError(err).Errorf("GitHub failed to add the following label: %s", labels.Help)
   203  			}
   204  		}
   205  
   206  		return nil
   207  	}
   208  
   209  	// If PR does not have the help label and we're asking it to be added,
   210  	// add the label
   211  	if !hasHelp && helpRe.MatchString(e.Body) {
   212  		if err := gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.IssueHTMLURL, commentAuthor, ig.helpMsg())); err != nil {
   213  			log.WithError(err).Errorf("Failed to create comment \"%s\".", ig.helpMsg())
   214  		}
   215  		if err := gc.AddLabel(org, repo, e.Number, labels.Help); err != nil {
   216  			log.WithError(err).Errorf("GitHub failed to add the following label: %s", labels.Help)
   217  		}
   218  
   219  		return nil
   220  	}
   221  
   222  	// If PR has good-first-issue label and we are asking for it to be removed,
   223  	// remove just the good-first-issue label
   224  	if hasGoodFirstIssue && helpGoodFirstIssueRemoveRe.MatchString(e.Body) {
   225  		if err := gc.RemoveLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil {
   226  			log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.GoodFirstIssue)
   227  		}
   228  
   229  		botUserChecker, err := gc.BotUserChecker()
   230  		if err != nil {
   231  			log.WithError(err).Errorf("Failed to get bot name.")
   232  		}
   233  		cp.PruneComments(shouldPrune(log, botUserChecker, goodFirstIssueMsgPruneMatch))
   234  
   235  		return nil
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  // shouldPrune finds comments left by this plugin.
   242  func shouldPrune(log *logrus.Entry, isBot func(string) bool, msgPruneMatch string) func(github.IssueComment) bool {
   243  	return func(comment github.IssueComment) bool {
   244  		if !isBot(comment.User.Login) {
   245  			return false
   246  		}
   247  		return strings.Contains(comment.Body, msgPruneMatch)
   248  	}
   249  }