github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/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 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 "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.Agent, 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 }