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