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 }