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 }