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