github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/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  }