github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/report/report.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 report contains helpers for writing comments and updating
    18  // statuses in Github.
    19  package report
    20  
    21  import (
    22  	"bytes"
    23  	"fmt"
    24  	"strings"
    25  	"text/template"
    26  
    27  	"k8s.io/test-infra/prow/github"
    28  	"k8s.io/test-infra/prow/kube"
    29  	"k8s.io/test-infra/prow/pjutil"
    30  	"k8s.io/test-infra/prow/plugins"
    31  )
    32  
    33  const (
    34  	commentTag       = "<!-- test report -->"
    35  	parentJobChanged = "Parent Job Status Changed: "
    36  )
    37  
    38  type GithubClient interface {
    39  	BotName() (string, error)
    40  	CreateStatus(org, repo, ref string, s github.Status) error
    41  	ListIssueComments(org, repo string, number int) ([]github.IssueComment, error)
    42  	CreateComment(org, repo string, number int, comment string) error
    43  	DeleteComment(org, repo string, ID int) error
    44  	EditComment(org, repo string, ID int, comment string) error
    45  }
    46  
    47  // reportStatus should be called on status different from Success.
    48  // Once a parent ProwJob is pending, all children should be marked as Pending
    49  // Same goes for failed status.
    50  func reportStatus(ghc GithubClient, pj kube.ProwJob, cd string) error {
    51  	refs := pj.Spec.Refs
    52  	if pj.Spec.Report {
    53  		if err := ghc.CreateStatus(refs.Org, refs.Repo, refs.Pulls[0].SHA, github.Status{
    54  			State:       string(pj.Status.State),
    55  			Description: pj.Status.Description,
    56  			Context:     pj.Spec.Context,
    57  			TargetURL:   pj.Status.URL,
    58  		}); err != nil {
    59  			return err
    60  		}
    61  	}
    62  
    63  	// Updating Children
    64  	if pj.Status.State != kube.SuccessState {
    65  		for _, nj := range pj.Spec.RunAfterSuccess {
    66  			cpj := pjutil.NewProwJob(nj)
    67  			cpj.Status.State = pj.Status.State
    68  			cpj.Status.Description = cd
    69  			cpj.Spec.Refs = refs
    70  			if err := reportStatus(ghc, cpj, cd); err != nil {
    71  				return err
    72  			}
    73  		}
    74  	}
    75  	return nil
    76  }
    77  
    78  // Report is creating/updating/removing reports in Github based on the state of
    79  // the provided ProwJob.
    80  func Report(ghc GithubClient, reportTemplate *template.Template, pj kube.ProwJob) error {
    81  	if !pj.Spec.Report {
    82  		return nil
    83  	}
    84  	refs := pj.Spec.Refs
    85  	if len(refs.Pulls) != 1 {
    86  		return fmt.Errorf("prowjob %s has %d pulls, not 1", pj.Metadata.Name, len(refs.Pulls))
    87  	}
    88  	if err := reportStatus(ghc, pj, parentJobChanged+pj.Status.Description); err != nil {
    89  		return fmt.Errorf("error setting status: %v", err)
    90  	}
    91  	if pj.Status.State != github.StatusSuccess && pj.Status.State != github.StatusFailure {
    92  		return nil
    93  	}
    94  	ics, err := ghc.ListIssueComments(refs.Org, refs.Repo, refs.Pulls[0].Number)
    95  	if err != nil {
    96  		return fmt.Errorf("error listing comments: %v", err)
    97  	}
    98  	botName, err := ghc.BotName()
    99  	if err != nil {
   100  		return fmt.Errorf("error getting bot name: %v", err)
   101  	}
   102  	deletes, entries, updateID := parseIssueComments(pj, botName, ics)
   103  	for _, delete := range deletes {
   104  		if err := ghc.DeleteComment(refs.Org, refs.Repo, delete); err != nil {
   105  			return fmt.Errorf("error deleting comment: %v", err)
   106  		}
   107  	}
   108  	if len(entries) > 0 {
   109  		comment, err := createComment(reportTemplate, pj, entries)
   110  		if err != nil {
   111  			return fmt.Errorf("generating comment: %v", err)
   112  		}
   113  		if updateID == 0 {
   114  			if err := ghc.CreateComment(refs.Org, refs.Repo, refs.Pulls[0].Number, comment); err != nil {
   115  				return fmt.Errorf("error creating comment: %v", err)
   116  			}
   117  		} else {
   118  			if err := ghc.EditComment(refs.Org, refs.Repo, updateID, comment); err != nil {
   119  				return fmt.Errorf("error updating comment: %v", err)
   120  			}
   121  		}
   122  	}
   123  	return nil
   124  }
   125  
   126  // parseIssueComments returns a list of comments to delete, a list of table
   127  // entries, and the ID of the comment to update. If there are no table entries
   128  // then don't make a new comment. Otherwise, if the comment to update is 0,
   129  // create a new comment.
   130  func parseIssueComments(pj kube.ProwJob, botName string, ics []github.IssueComment) ([]int, []string, int) {
   131  	var delete []int
   132  	var previousComments []int
   133  	var latestComment int
   134  	var entries []string
   135  	// First accumulate result entries and comment IDs
   136  	for _, ic := range ics {
   137  		if ic.User.Login != botName {
   138  			continue
   139  		}
   140  		// Old report comments started with the context. Delete them.
   141  		// TODO(spxtr): Delete this check a few weeks after this merges.
   142  		if strings.HasPrefix(ic.Body, pj.Spec.Context) {
   143  			delete = append(delete, ic.ID)
   144  		}
   145  		if !strings.Contains(ic.Body, commentTag) {
   146  			continue
   147  		}
   148  		if latestComment != 0 {
   149  			previousComments = append(previousComments, latestComment)
   150  		}
   151  		latestComment = ic.ID
   152  		var tracking bool
   153  		for _, line := range strings.Split(ic.Body, "\n") {
   154  			line = strings.TrimSpace(line)
   155  			if strings.HasPrefix(line, "---") {
   156  				tracking = true
   157  			} else if len(line) == 0 {
   158  				tracking = false
   159  			} else if tracking {
   160  				entries = append(entries, line)
   161  			}
   162  		}
   163  	}
   164  	var newEntries []string
   165  	// Next decide which entries to keep.
   166  	for i := range entries {
   167  		keep := true
   168  		f1 := strings.Split(entries[i], " | ")
   169  		for j := range entries {
   170  			if i == j {
   171  				continue
   172  			}
   173  			f2 := strings.Split(entries[j], " | ")
   174  			// Use the newer results if there are multiple.
   175  			if j > i && f2[0] == f1[0] {
   176  				keep = false
   177  			}
   178  		}
   179  		// Use the current result if there is an old one.
   180  		if pj.Spec.Context == f1[0] {
   181  			keep = false
   182  		}
   183  		if keep {
   184  			newEntries = append(newEntries, entries[i])
   185  		}
   186  	}
   187  	var createNewComment bool
   188  	if string(pj.Status.State) == github.StatusFailure {
   189  		newEntries = append(newEntries, createEntry(pj))
   190  		createNewComment = true
   191  	}
   192  	delete = append(delete, previousComments...)
   193  	if (createNewComment || len(newEntries) == 0) && latestComment != 0 {
   194  		delete = append(delete, latestComment)
   195  		latestComment = 0
   196  	}
   197  	return delete, newEntries, latestComment
   198  }
   199  
   200  func createEntry(pj kube.ProwJob) string {
   201  	return strings.Join([]string{
   202  		pj.Spec.Context,
   203  		pj.Spec.Refs.Pulls[0].SHA,
   204  		fmt.Sprintf("[link](%s)", pj.Status.URL),
   205  		fmt.Sprintf("`%s`", pj.Spec.RerunCommand),
   206  	}, " | ")
   207  }
   208  
   209  // createComment take a ProwJob and a list of entries generated with
   210  // createEntry and returns a nicely formatted comment. It may fail if template
   211  // execution fails.
   212  func createComment(reportTemplate *template.Template, pj kube.ProwJob, entries []string) (string, error) {
   213  	plural := ""
   214  	if len(entries) > 1 {
   215  		plural = "s"
   216  	}
   217  	var b bytes.Buffer
   218  	if err := reportTemplate.Execute(&b, &pj); err != nil {
   219  		return "", err
   220  	}
   221  	lines := []string{
   222  		fmt.Sprintf("@%s: The following test%s **failed**, say `/retest` to rerun them all:", pj.Spec.Refs.Pulls[0].Author, plural),
   223  		"",
   224  		"Test name | Commit | Details | Rerun command",
   225  		"--- | --- | --- | ---",
   226  	}
   227  	lines = append(lines, entries...)
   228  	lines = append(lines, []string{
   229  		"",
   230  		b.String(),
   231  		"",
   232  		"<details>",
   233  		"",
   234  		plugins.AboutThisBot,
   235  		"</details>",
   236  		commentTag,
   237  	}...)
   238  	return strings.Join(lines, "\n"), nil
   239  }