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  }