github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/lifecycle/close.go (about)

     1  /*
     2  Copyright 2016 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 lifecycle
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  
    23  	"github.com/sirupsen/logrus"
    24  
    25  	"sigs.k8s.io/prow/pkg/github"
    26  	"sigs.k8s.io/prow/pkg/plugins"
    27  )
    28  
    29  var (
    30  	closeRe           = regexp.MustCompile(`(?mi)^/close\s*$`)
    31  	closeNotPlannedRe = regexp.MustCompile(`(?mi)^/close not-planned\s*$`)
    32  )
    33  
    34  type closeClient interface {
    35  	IsCollaborator(owner, repo, login string) (bool, error)
    36  	CreateComment(owner, repo string, number int, comment string) error
    37  	CloseIssue(owner, repo string, number int) error
    38  	CloseIssueAsNotPlanned(org, repo string, number int) error
    39  	ClosePullRequest(owner, repo string, number int) error
    40  	GetIssueLabels(owner, repo string, number int) ([]github.Label, error)
    41  }
    42  
    43  func isActive(gc closeClient, org, repo string, number int) (bool, error) {
    44  	labels, err := gc.GetIssueLabels(org, repo, number)
    45  	if err != nil {
    46  		return true, fmt.Errorf("list issue labels error: %w", err)
    47  	}
    48  	for _, label := range []string{"lifecycle/stale", "lifecycle/rotten"} {
    49  		if github.HasLabel(label, labels) {
    50  			return false, nil
    51  		}
    52  	}
    53  	return true, nil
    54  }
    55  
    56  func handleClose(gc closeClient, log *logrus.Entry, e *github.GenericCommentEvent) error {
    57  	// Only consider open issues and new comments.
    58  	if e.IssueState != "open" || e.Action != github.GenericCommentActionCreated {
    59  		return nil
    60  	}
    61  
    62  	if !closeRe.MatchString(e.Body) && !closeNotPlannedRe.MatchString(e.Body) {
    63  		return nil
    64  	}
    65  
    66  	org := e.Repo.Owner.Login
    67  	repo := e.Repo.Name
    68  	number := e.Number
    69  	commentAuthor := e.User.Login
    70  
    71  	isAuthor := e.IssueAuthor.Login == commentAuthor
    72  
    73  	isCollaborator, err := gc.IsCollaborator(org, repo, commentAuthor)
    74  	if err != nil {
    75  		log.WithError(err).Errorf("Failed IsCollaborator(%s, %s, %s)", org, repo, commentAuthor)
    76  	}
    77  
    78  	active, err := isActive(gc, org, repo, number)
    79  	if err != nil {
    80  		log.Infof("Cannot determine if issue is active: %v", err)
    81  		active = true // Fail active
    82  	}
    83  
    84  	// Only authors and collaborators are allowed to close active issues.
    85  	if !isAuthor && !isCollaborator && active {
    86  		response := "You can't close an active issue/PR unless you authored it or you are a collaborator."
    87  		log.Infof("Commenting \"%s\".", response)
    88  		return gc.CreateComment(
    89  			org,
    90  			repo,
    91  			number,
    92  			plugins.FormatResponseRaw(e.Body, e.HTMLURL, commentAuthor, response),
    93  		)
    94  	}
    95  
    96  	// Add a comment after closing the PR or issue
    97  	// to leave an audit trail of who asked to close it.
    98  	if e.IsPR {
    99  		// PRs cannot be closed as Not Planned because the
   100  		// "not_planned" state only exists for issues, which
   101  		// is why allowing PRs to be closed when /close not-planned
   102  		// is commented feels awkward.
   103  		if closeNotPlannedRe.MatchString(e.Body) {
   104  			response := "PRs cannot be closed as Not Planned."
   105  			log.Infof("Commenting \"%s\".", response)
   106  			return gc.CreateComment(
   107  				org,
   108  				repo,
   109  				number,
   110  				plugins.FormatResponseRaw(e.Body, e.HTMLURL, commentAuthor, response),
   111  			)
   112  		}
   113  		log.Info("Closing PR.")
   114  		if err := gc.ClosePullRequest(org, repo, number); err != nil {
   115  			return fmt.Errorf("Error closing PR: %w", err)
   116  		}
   117  		response := plugins.FormatResponseRaw(e.Body, e.HTMLURL, commentAuthor, "Closed this PR.")
   118  		return gc.CreateComment(org, repo, number, response)
   119  	}
   120  
   121  	log.Info("Closing issue.")
   122  	var reply string
   123  	if closeNotPlannedRe.MatchString(e.Body) {
   124  		if err := gc.CloseIssueAsNotPlanned(org, repo, number); err != nil {
   125  			return fmt.Errorf("Error closing issue as \"Not Planned\": %w", err)
   126  		}
   127  		reply = "Closing this issue, marking it as \"Not Planned\"."
   128  	} else {
   129  		if err := gc.CloseIssue(org, repo, number); err != nil {
   130  			return fmt.Errorf("Error closing issue: %w", err)
   131  		}
   132  		reply = "Closing this issue."
   133  	}
   134  
   135  	response := plugins.FormatResponseRaw(e.Body, e.HTMLURL, commentAuthor, reply)
   136  	return gc.CreateComment(org, repo, number, response)
   137  }