sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/transfer-issue/transfer-issue.go (about) 1 /* 2 Copyright 2021 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 transferissue implements the `/transfer-issue` command which allows members of the org 18 // to transfer issues between repos 19 package transferissue 20 21 import ( 22 "context" 23 "fmt" 24 "regexp" 25 "strings" 26 27 githubql "github.com/shurcooL/githubv4" 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/pluginhelp" 33 "sigs.k8s.io/prow/pkg/plugins" 34 ) 35 36 const pluginName = "transfer-issue" 37 38 var ( 39 transferRe = regexp.MustCompile(`(?mi)^/transfer(?:-issue)?(?: +(.*))?$`) 40 ) 41 42 type githubClient interface { 43 GetRepo(org, name string) (github.FullRepo, error) 44 CreateComment(org, repo string, number int, comment string) error 45 IsMember(org, user string) (bool, error) 46 MutateWithGitHubAppsSupport(context.Context, interface{}, githubql.Input, map[string]interface{}, string) error 47 } 48 49 func init() { 50 plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider) 51 } 52 53 func helpProvider(_ *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) { 54 pluginHelp := &pluginhelp.PluginHelp{ 55 Description: "The transfer-issue plugin transfers a GitHub issue from one repo to another in the same organization.", 56 } 57 pluginHelp.AddCommand(pluginhelp.Command{ 58 Usage: "/transfer[-issue] <destination repo in same org>", 59 Description: "Transfers an issue to a different repo in the same org.", 60 Featured: true, 61 WhoCanUse: "Org members.", 62 Examples: []string{"/transfer-issue kubectl", "/transfer test-infra"}, 63 }) 64 return pluginHelp, nil 65 } 66 67 func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error { 68 return handleTransfer(pc.GitHubClient, pc.Logger, e) 69 } 70 71 func handleTransfer(gc githubClient, log *logrus.Entry, e github.GenericCommentEvent) error { 72 org := e.Repo.Owner.Login 73 srcRepoName := e.Repo.Name 74 srcRepoPair := org + "/" + srcRepoName 75 user := e.User.Login 76 77 if e.IsPR || e.Action != github.GenericCommentActionCreated { 78 return nil 79 } 80 matches := transferRe.FindAllStringSubmatch(e.Body, -1) 81 if len(matches) == 0 { 82 return nil 83 } 84 if len(matches) != 1 || len(matches[0]) != 2 || len(matches[0][1]) == 0 { 85 return gc.CreateComment( 86 org, srcRepoName, e.Number, 87 plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, "/transfer-issue must only be used once and with a single destination repo."), 88 ) 89 } 90 91 dstRepoName := strings.TrimSpace(matches[0][1]) 92 dstRepoPair := org + "/" + dstRepoName 93 94 dstRepo, err := gc.GetRepo(org, dstRepoName) 95 if err != nil { 96 log.WithError(err).WithField("dstRepo", dstRepoPair).Warning("could not fetch destination repo") 97 // TODO: Might want to add another GetRepo type call that checks if a repo exists vs a bad request 98 return gc.CreateComment( 99 org, srcRepoName, e.Number, 100 plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, fmt.Sprintf("Something went wrong or the destination repo %s does not exist.", dstRepoPair)), 101 ) 102 } 103 104 isMember, err := gc.IsMember(org, user) 105 if err != nil { 106 return fmt.Errorf("unable to fetch if %s is an org member of %s: %w", user, org, err) 107 } 108 if !isMember { 109 return gc.CreateComment( 110 org, srcRepoName, e.Number, 111 plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, "You must be an org member to transfer this issue."), 112 ) 113 } 114 115 m, err := transferIssue(gc, org, dstRepo.NodeID, e.NodeID) 116 if err != nil { 117 log.WithError(err).WithFields(logrus.Fields{ 118 "issueNumber": e.Number, 119 "srcRepo": srcRepoPair, 120 "dstRepo": dstRepoPair, 121 }).Error("issue could not be transferred") 122 return err 123 } 124 log.WithFields(logrus.Fields{ 125 "user": user, 126 "org": org, 127 "srcRepo": srcRepoName, 128 "issueNumber": e.Number, 129 "dstURL": m.TransferIssue.Issue.URL, 130 }).Infof("successfully transferred issue") 131 return nil 132 } 133 134 // TransferIssueMutation is a GraphQL mutation struct compatible with shurcooL/githubql's client 135 // 136 // See https://docs.github.com/en/graphql/reference/input-objects#transferissueinput 137 type transferIssueMutation struct { 138 TransferIssue struct { 139 Issue struct { 140 URL githubql.URI 141 } 142 } `graphql:"transferIssue(input: $input)"` 143 } 144 145 // TransferIssue will move an issue from one repo to another in the same org. 146 // 147 // See https://docs.github.com/en/graphql/reference/mutations#transferissue 148 // 149 // In the future we may want to interact with the TransferredEvent on the issue IssueTimeline 150 // See https://docs.github.com/en/graphql/reference/objects#transferredevent 151 // https://docs.github.com/en/graphql/reference/unions#issuetimelineitem 152 func transferIssue(gc githubClient, org, dstRepoNodeID string, issueNodeID string) (*transferIssueMutation, error) { 153 m := &transferIssueMutation{} 154 input := githubql.TransferIssueInput{ 155 IssueID: githubql.ID(issueNodeID), 156 RepositoryID: githubql.ID(dstRepoNodeID), 157 } 158 err := gc.MutateWithGitHubAppsSupport(context.Background(), m, input, nil, org) 159 return m, err 160 }