github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/experiment/commenter/main.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 // Commenter provides a way to --query for issues and append a --comment to matches. 18 // 19 // The --token determines who interacts with github. 20 // By default commenter runs in dry mode, add --confirm to make it leave comments. 21 // The --updated, --include-closed, --ceiling options provide minor safeguards 22 // around leaving excessive comments. 23 package main 24 25 import ( 26 "bytes" 27 "flag" 28 "fmt" 29 "io/ioutil" 30 "log" 31 "net/url" 32 "regexp" 33 "strconv" 34 "strings" 35 "text/template" 36 "time" 37 38 "k8s.io/test-infra/prow/github" 39 ) 40 41 const ( 42 TemplateHelp = `--comment is a golang text/template if set. 43 Valid placeholders: 44 .Org - github org 45 .Repo - github repo 46 .Number - issue number 47 Advanced (see kubernetes/test-infra/prow/github/types.go): 48 .Issue.User.Login - github account 49 .Issue.Title 50 .Issue.State 51 .Issue.HTMLURL 52 .Issue.Assignees - list of assigned .Users 53 .Issue.Labels - list of applied labels (.Name) 54 ` 55 ) 56 57 func flagOptions() options { 58 o := options{} 59 flag.StringVar(&o.query, "query", "", "See https://help.github.com/articles/searching-issues-and-pull-requests/") 60 flag.DurationVar(&o.updated, "updated", 2*time.Hour, "Filter to issues unmodified for at least this long if set") 61 flag.BoolVar(&o.includeClosed, "include-closed", false, "Match closed issues if set") 62 flag.BoolVar(&o.confirm, "confirm", false, "Mutate github if set") 63 flag.StringVar(&o.comment, "comment", "", "Append the following comment to matching issues") 64 flag.BoolVar(&o.useTemplate, "template", false, TemplateHelp) 65 flag.IntVar(&o.ceiling, "ceiling", 3, "Maximum number of issues to modify, 0 for infinite") 66 flag.StringVar(&o.endpoint, "endpoint", "https://api.github.com", "GitHub's API endpoint") 67 flag.StringVar(&o.token, "token", "", "Path to github token") 68 flag.Parse() 69 return o 70 } 71 72 type meta struct { 73 Number int 74 Org string 75 Repo string 76 Issue github.Issue 77 } 78 79 type options struct { 80 asc bool 81 ceiling int 82 comment string 83 includeClosed bool 84 useTemplate bool 85 query string 86 sort string 87 endpoint string 88 token string 89 updated time.Duration 90 confirm bool 91 } 92 93 func parseHTMLURL(url string) (string, string, int, error) { 94 // Example: https://github.com/batterseapower/pinyin-toolkit/issues/132 95 re := regexp.MustCompile(`.+/(.+)/(.+)/(issues|pull)/(\d+)$`) 96 mat := re.FindStringSubmatch(url) 97 if mat == nil { 98 return "", "", 0, fmt.Errorf("failed to parse: %s", url) 99 } 100 n, err := strconv.Atoi(mat[4]) 101 if err != nil { 102 return "", "", 0, err 103 } 104 return mat[1], mat[2], n, nil 105 } 106 107 func makeQuery(query string, includeClosed bool, minUpdated time.Duration) (string, error) { 108 parts := []string{query} 109 if !includeClosed { 110 if strings.Contains(query, "is:closed") { 111 return "", fmt.Errorf("--query='%s' containing is:closed requires --include-closed", query) 112 } 113 parts = append(parts, "is:open") 114 } else if strings.Contains(query, "is:open") { 115 return "", fmt.Errorf("--query='%s' should not contain is:open when using --include-closed", query) 116 } 117 if minUpdated != 0 { 118 latest := time.Now().Add(-minUpdated) 119 parts = append(parts, "updated:<="+latest.Format(time.RFC3339)) 120 } 121 return strings.Join(parts, " "), nil 122 } 123 124 type client interface { 125 CreateComment(owner, repo string, number int, comment string) error 126 FindIssues(query, sort string, asc bool) ([]github.Issue, error) 127 } 128 129 func main() { 130 log.SetFlags(log.LstdFlags | log.Lshortfile) 131 o := flagOptions() 132 133 if o.query == "" { 134 log.Fatal("empty --query") 135 } 136 if o.token == "" { 137 log.Fatal("empty --token") 138 } 139 if o.comment == "" { 140 log.Fatal("empty --comment") 141 } 142 b, err := ioutil.ReadFile(o.token) 143 if err != nil { 144 log.Fatalf("cannot read --token: %v", err) 145 } 146 _, err = url.Parse(o.endpoint) 147 if err != nil { 148 log.Fatal("Must specify a valid --endpoint URL.") 149 } 150 151 var c client 152 tok := strings.TrimSpace(string(b)) 153 if o.confirm { 154 c = github.NewClient(tok, o.endpoint) 155 } else { 156 c = github.NewDryRunClient(tok, o.endpoint) 157 } 158 159 query, err := makeQuery(o.query, o.includeClosed, o.updated) 160 if err != nil { 161 log.Fatalf("Bad query: %v", err) 162 } 163 sort := "" 164 asc := false 165 if o.updated > 0 { 166 sort = "updated" 167 asc = true 168 } 169 commenter := makeCommenter(o.comment, o.useTemplate) 170 if err := run(c, query, sort, asc, commenter, o.ceiling); err != nil { 171 log.Fatalf("Failed run: %v", err) 172 } 173 } 174 175 func makeCommenter(comment string, useTemplate bool) func(meta) (string, error) { 176 if !useTemplate { 177 return func(_ meta) (string, error) { 178 return comment, nil 179 } 180 } 181 t := template.Must(template.New("comment").Parse(comment)) 182 return func(m meta) (string, error) { 183 out := bytes.Buffer{} 184 err := t.Execute(&out, m) 185 return string(out.Bytes()), err 186 } 187 } 188 189 func run(c client, query, sort string, asc bool, commenter func(meta) (string, error), ceiling int) error { 190 log.Printf("Searching: %s", query) 191 issues, err := c.FindIssues(query, sort, asc) 192 if err != nil { 193 return fmt.Errorf("search failed: %v", err) 194 } 195 problems := []string{} 196 log.Printf("Found %d matches", len(issues)) 197 for n, i := range issues { 198 if ceiling > 0 && n == ceiling { 199 log.Printf("Stopping at --ceiling=%d of %d results", n, len(issues)) 200 break 201 } 202 log.Printf("Matched %s (%s)", i.HTMLURL, i.Title) 203 org, repo, number, err := parseHTMLURL(i.HTMLURL) 204 if err != nil { 205 msg := fmt.Sprintf("Failed to parse %s: %v", i.HTMLURL, err) 206 log.Print(msg) 207 problems = append(problems, msg) 208 } 209 comment, err := commenter(meta{Number: number, Org: org, Repo: repo, Issue: i}) 210 if err != nil { 211 msg := fmt.Sprintf("Failed to create comment for %s/%s#%d: %v", org, repo, number, err) 212 log.Print(msg) 213 problems = append(problems, msg) 214 continue 215 } 216 if err := c.CreateComment(org, repo, number, comment); err != nil { 217 msg := fmt.Sprintf("Failed to apply comment to %s/%s#%d: %v", org, repo, number, err) 218 log.Print(msg) 219 problems = append(problems, msg) 220 continue 221 } 222 log.Printf("Commented on %s", i.HTMLURL) 223 } 224 if len(problems) > 0 { 225 return fmt.Errorf("encoutered %d failures: %v", len(problems), problems) 226 } 227 return nil 228 }