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

     1  /*
     2  Copyright 2017 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 milestone implements the `/milestone` command which allows members of the milestone
    18  // maintainers team to specify a milestone to be applied to an Issue or PR.
    19  package milestone
    20  
    21  import (
    22  	"fmt"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  
    27  	"github.com/sirupsen/logrus"
    28  
    29  	prowconfig "sigs.k8s.io/prow/pkg/config"
    30  	"sigs.k8s.io/prow/pkg/github"
    31  	"sigs.k8s.io/prow/pkg/pluginhelp"
    32  	"sigs.k8s.io/prow/pkg/plugins"
    33  )
    34  
    35  const pluginName = "milestone"
    36  
    37  var (
    38  	milestoneRegex   = regexp.MustCompile(`(?m)^/milestone\s+(.+?)\s*$`)
    39  	mustBeAuthorized = "You must be a member of the [%s/%s](https://github.com/orgs/%s/teams/%s/members) GitHub team to set the milestone. If you believe you should be able to issue the /milestone command, please contact your %s and have them propose you as an additional delegate for this responsibility."
    40  	invalidMilestone = "The provided milestone is not valid for this repository. Milestones in this repository: [%s]\n\nUse `/milestone %s` to clear the milestone."
    41  	milestoneTeamMsg = "The milestone maintainers team is the GitHub team %q with ID: %d."
    42  	clearKeyword     = "clear"
    43  )
    44  
    45  type githubClient interface {
    46  	CreateComment(owner, repo string, number int, comment string) error
    47  	ClearMilestone(org, repo string, num int) error
    48  	SetMilestone(org, repo string, issueNum, milestoneNum int) error
    49  	ListTeamMembers(org string, id int, role string) ([]github.TeamMember, error)
    50  	ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error)
    51  	ListMilestones(org, repo string) ([]github.Milestone, error)
    52  }
    53  
    54  func init() {
    55  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
    56  }
    57  
    58  func helpProvider(config *plugins.Configuration, enabledRepos []prowconfig.OrgRepo) (*pluginhelp.PluginHelp, error) {
    59  	msgForTeam := func(team plugins.Milestone) string {
    60  		return fmt.Sprintf(milestoneTeamMsg, team.MaintainersTeam, team.MaintainersID)
    61  	}
    62  
    63  	pluginHelp := &pluginhelp.PluginHelp{
    64  		Description: "The milestone plugin allows members of a configurable GitHub team to set the milestone on an issue or pull request.",
    65  		Config: func(repos []prowconfig.OrgRepo) map[string]string {
    66  			configMap := make(map[string]string)
    67  			for _, repo := range repos {
    68  				team, exists := config.RepoMilestone[repo.String()]
    69  				if exists {
    70  					configMap[repo.String()] = msgForTeam(team)
    71  				}
    72  			}
    73  			configMap[""] = msgForTeam(config.RepoMilestone[""])
    74  			return configMap
    75  		}(enabledRepos),
    76  	}
    77  	pluginHelp.AddCommand(pluginhelp.Command{
    78  		Usage:       "/milestone <version> or /milestone clear",
    79  		Description: "Updates the milestone for an issue or PR",
    80  		Featured:    false,
    81  		WhoCanUse:   "Members of the milestone maintainers GitHub team can use the '/milestone' command.",
    82  		Examples:    []string{"/milestone v1.10", "/milestone v1.9", "/milestone clear"},
    83  	})
    84  	return pluginHelp, nil
    85  }
    86  
    87  func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error {
    88  	return handle(pc.GitHubClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone)
    89  }
    90  
    91  func BuildMilestoneMap(milestones []github.Milestone) map[string]int {
    92  	m := make(map[string]int)
    93  	for _, ms := range milestones {
    94  		m[ms.Title] = ms.Number
    95  	}
    96  	return m
    97  }
    98  func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error {
    99  	if e.Action != github.GenericCommentActionCreated {
   100  		return nil
   101  	}
   102  
   103  	milestoneMatch := milestoneRegex.FindStringSubmatch(e.Body)
   104  	if len(milestoneMatch) != 2 {
   105  		return nil
   106  	}
   107  
   108  	org := e.Repo.Owner.Login
   109  	repo := e.Repo.Name
   110  
   111  	milestone, exists := repoMilestone[fmt.Sprintf("%s/%s", org, repo)]
   112  	if !exists {
   113  		// fallback default
   114  		milestone = repoMilestone[""]
   115  	}
   116  	milestoneMaintainers, err := determineMaintainers(gc, milestone, org)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	found := false
   121  	for _, person := range milestoneMaintainers {
   122  		login := github.NormLogin(e.User.Login)
   123  		if github.NormLogin(person.Login) == login {
   124  			found = true
   125  			break
   126  		}
   127  	}
   128  	if !found {
   129  		// not in the milestone maintainers team
   130  		msg := fmt.Sprintf(mustBeAuthorized, org, milestone.MaintainersTeam, org, milestone.MaintainersTeam, milestone.MaintainersFriendlyName)
   131  		return gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg))
   132  	}
   133  
   134  	milestones, err := gc.ListMilestones(org, repo)
   135  	if err != nil {
   136  		log.WithError(err).Errorf("Error listing the milestones in the %s/%s repo", org, repo)
   137  		return err
   138  	}
   139  	proposedMilestone := milestoneMatch[1]
   140  
   141  	// special case, if the clear keyword is used
   142  	if proposedMilestone == clearKeyword {
   143  		if err := gc.ClearMilestone(org, repo, e.Number); err != nil {
   144  			log.WithError(err).Errorf("Error clearing the milestone for %s/%s#%d.", org, repo, e.Number)
   145  		}
   146  		return nil
   147  	}
   148  
   149  	milestoneMap := BuildMilestoneMap(milestones)
   150  	milestoneNumber, ok := milestoneMap[proposedMilestone]
   151  	if !ok {
   152  		slice := make([]string, 0, len(milestoneMap))
   153  		for k := range milestoneMap {
   154  			slice = append(slice, fmt.Sprintf("`%s`", k))
   155  		}
   156  		sort.Strings(slice)
   157  
   158  		msg := fmt.Sprintf(invalidMilestone, strings.Join(slice, ", "), clearKeyword)
   159  		return gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg))
   160  	}
   161  
   162  	if err := gc.SetMilestone(org, repo, e.Number, milestoneNumber); err != nil {
   163  		log.WithError(err).Errorf("Error adding the milestone %s to %s/%s#%d.", proposedMilestone, org, repo, e.Number)
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  func determineMaintainers(gc githubClient, milestone plugins.Milestone, org string) ([]github.TeamMember, error) {
   170  	if milestone.MaintainersTeam != "" {
   171  		return gc.ListTeamMembersBySlug(org, milestone.MaintainersTeam, github.RoleAll)
   172  	}
   173  	return gc.ListTeamMembers(org, milestone.MaintainersID, github.RoleAll)
   174  }