sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/cherrypickunapproved/cherrypick-unapproved.go (about)

     1  /*
     2  Copyright 2018 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 cherrypickunapproved adds the `do-not-merge/cherry-pick-not-approved`
    18  // label to PRs against a release branch which do not have the
    19  // `cherry-pick-approved` label.
    20  package cherrypickunapproved
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"regexp"
    26  	"strings"
    27  
    28  	"github.com/sirupsen/logrus"
    29  
    30  	"sigs.k8s.io/prow/pkg/config"
    31  	"sigs.k8s.io/prow/pkg/github"
    32  	"sigs.k8s.io/prow/pkg/labels"
    33  	"sigs.k8s.io/prow/pkg/pluginhelp"
    34  	"sigs.k8s.io/prow/pkg/plugins"
    35  )
    36  
    37  const (
    38  	// PluginName defines this plugin's registered name.
    39  	PluginName = "cherry-pick-unapproved"
    40  )
    41  
    42  func init() {
    43  	plugins.RegisterPullRequestHandler(PluginName, handlePullRequest, helpProvider)
    44  }
    45  
    46  func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
    47  	// Only the 'Config' and Description' fields are necessary because this
    48  	// plugin does not react to any commands.
    49  	yamlSnippet, err := plugins.CommentMap.GenYaml(&plugins.Configuration{
    50  		CherryPickUnapproved: plugins.CherryPickUnapproved{
    51  			BranchRegexp: "^release-*",
    52  			Comment:      "This is why your cherry-pick cannot be approved.",
    53  		},
    54  	})
    55  	if err != nil {
    56  		logrus.WithError(err).Warnf("cannot generate comments for %s plugin", PluginName)
    57  	}
    58  	pluginHelp := &pluginhelp.PluginHelp{
    59  		Description: "Label PRs against a release branch which do not have the `cherry-pick-approved` label with the `do-not-merge/cherry-pick-not-approved` label.",
    60  		Config: map[string]string{
    61  			"": fmt.Sprintf(
    62  				"The cherry-pick-unapproved plugin treats PRs against branch names satisfying the regular expression `%s` as cherry-pick PRs and adds the following comment:\n%s",
    63  				config.CherryPickUnapproved.BranchRegexp,
    64  				config.CherryPickUnapproved.Comment,
    65  			),
    66  		},
    67  		Snippet: yamlSnippet,
    68  	}
    69  	return pluginHelp, nil
    70  }
    71  
    72  type githubClient interface {
    73  	CreateComment(owner, repo string, number int, comment string) error
    74  	AddLabel(owner, repo string, number int, label string) error
    75  	RemoveLabel(owner, repo string, number int, label string) error
    76  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
    77  }
    78  
    79  type commentPruner interface {
    80  	PruneComments(shouldPrune func(github.IssueComment) bool)
    81  }
    82  
    83  func handlePullRequest(pc plugins.Agent, pr github.PullRequestEvent) error {
    84  	cp, err := pc.CommentPruner()
    85  	if err != nil {
    86  		return err
    87  	}
    88  	return handlePR(
    89  		pc.GitHubClient, pc.Logger, &pr, cp,
    90  		pc.PluginConfig.CherryPickUnapproved.BranchRe, pc.PluginConfig.CherryPickUnapproved.Comment,
    91  	)
    92  }
    93  
    94  func handlePR(gc githubClient, log *logrus.Entry, pr *github.PullRequestEvent, cp commentPruner, branchRe *regexp.Regexp, commentBody string) error {
    95  	var (
    96  		org    = pr.Repo.Owner.Login
    97  		repo   = pr.Repo.Name
    98  		branch = pr.PullRequest.Base.Ref
    99  	)
   100  
   101  	switch pr.Action {
   102  	case github.PullRequestActionOpened, github.PullRequestActionReopened:
   103  		if !branchRe.MatchString(branch) {
   104  			return nil
   105  		}
   106  		return ensureLabels(gc, org, repo, pr, log, cp, commentBody)
   107  	case github.PullRequestActionLabeled, github.PullRequestActionUnlabeled:
   108  		if !branchRe.MatchString(branch) {
   109  			return nil
   110  		}
   111  		if !(pr.Label.Name == labels.CpApproved || pr.Label.Name == labels.CpUnapproved) {
   112  			return nil
   113  		}
   114  		return ensureLabels(gc, org, repo, pr, log, cp, commentBody)
   115  	case github.PullRequestActionEdited:
   116  		// if someone changes the base of their PR, we will get this event
   117  		// and the changes field will list that the base SHA and ref changes
   118  		var changes struct {
   119  			Base struct {
   120  				Ref struct {
   121  					From string `json:"from"`
   122  				} `json:"ref"`
   123  				Sha struct {
   124  					From string `json:"from"`
   125  				} `json:"sha"`
   126  			} `json:"base"`
   127  		}
   128  		if err := json.Unmarshal(pr.Changes, &changes); err != nil {
   129  			// we're detecting this best-effort so we can forget about the event
   130  			return nil
   131  		}
   132  
   133  		if changes.Base.Ref.From == "" {
   134  			// PR base ref did not change, ignore the event
   135  			return nil
   136  		}
   137  
   138  		if branchRe.MatchString(branch) && !branchRe.MatchString(changes.Base.Ref.From) {
   139  			// base ref changed from a branch not allowed for cherry-picks to a branch that is allowed for cherry-picks
   140  			return ensureLabels(gc, org, repo, pr, log, cp, commentBody)
   141  		} else if !branchRe.MatchString(branch) && branchRe.MatchString(changes.Base.Ref.From) {
   142  			// base ref changed from a branch allowed for cherry-picks to a branch that is not allowed for cherry-picks
   143  			return pruneLabels(gc, org, repo, pr, log, cp, commentBody)
   144  		}
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  func ensureLabels(gc githubClient, org string, repo string, pr *github.PullRequestEvent, log *logrus.Entry, cp commentPruner, commentBody string) error {
   151  	issueLabels, err := gc.GetIssueLabels(org, repo, pr.Number)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	hasCherryPickApprovedLabel := github.HasLabel(labels.CpApproved, issueLabels)
   156  	hasCherryPickUnapprovedLabel := github.HasLabel(labels.CpUnapproved, issueLabels)
   157  
   158  	// if it has the approved label,
   159  	// remove the unapproved label (if it exists) and
   160  	// remove any comments left by this plugin
   161  	if hasCherryPickApprovedLabel {
   162  		if hasCherryPickUnapprovedLabel {
   163  			if err := gc.RemoveLabel(org, repo, pr.Number, labels.CpUnapproved); err != nil {
   164  				log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.CpUnapproved)
   165  			}
   166  		}
   167  		cp.PruneComments(func(comment github.IssueComment) bool {
   168  			return strings.Contains(comment.Body, commentBody)
   169  		})
   170  		return nil
   171  	}
   172  
   173  	// if it already has the unapproved label, we are done here
   174  	if hasCherryPickUnapprovedLabel {
   175  		return nil
   176  	}
   177  
   178  	// only add the label and comment if none of the approved and unapproved labels are present
   179  	if err := gc.AddLabel(org, repo, pr.Number, labels.CpUnapproved); err != nil {
   180  		log.WithError(err).Errorf("GitHub failed to add the following label: %s", labels.CpUnapproved)
   181  	}
   182  
   183  	formattedComment := plugins.FormatSimpleResponse(commentBody)
   184  	if err := gc.CreateComment(org, repo, pr.Number, formattedComment); err != nil {
   185  		log.WithError(err).Errorf("Failed to comment %q", formattedComment)
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  func pruneLabels(gc githubClient, org string, repo string, pr *github.PullRequestEvent, log *logrus.Entry, cp commentPruner, commentBody string) error {
   192  	issueLabels, err := gc.GetIssueLabels(org, repo, pr.Number)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	hasCherryPickApprovedLabel := github.HasLabel(labels.CpApproved, issueLabels)
   197  	hasCherryPickUnapprovedLabel := github.HasLabel(labels.CpUnapproved, issueLabels)
   198  
   199  	if hasCherryPickApprovedLabel {
   200  		if err := gc.RemoveLabel(org, repo, pr.Number, labels.CpApproved); err != nil {
   201  			log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.CpApproved)
   202  		}
   203  	}
   204  
   205  	if hasCherryPickUnapprovedLabel {
   206  		if err := gc.RemoveLabel(org, repo, pr.Number, labels.CpUnapproved); err != nil {
   207  			log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.CpUnapproved)
   208  		}
   209  	}
   210  
   211  	cp.PruneComments(func(comment github.IssueComment) bool {
   212  		return strings.Contains(comment.Body, commentBody)
   213  	})
   214  
   215  	return nil
   216  }