github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/alerts/jira.go (about)

     1  package alerts
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"text/template"
     8  
     9  	"github.com/evergreen-ci/evergreen"
    10  	"github.com/evergreen-ci/evergreen/model"
    11  	"github.com/evergreen-ci/evergreen/model/build"
    12  	"github.com/evergreen-ci/evergreen/model/host"
    13  	"github.com/evergreen-ci/evergreen/model/task"
    14  	"github.com/evergreen-ci/evergreen/model/version"
    15  	"github.com/evergreen-ci/evergreen/thirdparty"
    16  	"github.com/evergreen-ci/evergreen/util"
    17  	"github.com/mongodb/grip"
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  // DescriptionTemplateString defines the content of the alert ticket.
    22  const DescriptionTemplateString = `
    23  h2. [{{.Task.DisplayName}} failed on {{.Build.DisplayName}}|{{.UIRoot}}/task/{{.Task.Id}}/{{.Task.Execution}}]
    24  Host: [{{.Host.Host}}|{{.UIRoot}}/host/{{.Host.Id}}]
    25  Project: [{{.Project.DisplayName}}|{{.UIRoot}}/waterfall/{{.Project.Identifier}}]
    26  Commit: [diff|https://github.com/{{.Project.Owner}}/commit/{{.Version.Revision}}]: {{.Version.Message}}
    27  {{range .Tests}}*{{.Name}}* - [Logs|{{.URL}}] | [History|{{.HistoryURL}}]
    28  {{end}}
    29  `
    30  const (
    31  	jiraFailingTasksField     = "customfield_12950"
    32  	jiraFailingVariantField   = "customfield_14277"
    33  	jiraEvergreenProjectField = "customfield_14278"
    34  )
    35  
    36  // supportedJiraProjects are all of the projects, by name that we
    37  // expect to be compatible with the custom fields above.
    38  var supportedJiraProjects = []string{"BFG", "BF", "EVG", "MAKE", "BUILD"}
    39  
    40  // DescriptionTemplate is filled to create a JIRA alert ticket. Panics at start if invalid.
    41  var DescriptionTemplate = template.Must(template.New("Desc").Parse(DescriptionTemplateString))
    42  
    43  // jiraTestFailure contains the required fields for generating a failure report.
    44  type jiraTestFailure struct {
    45  	Name       string
    46  	URL        string
    47  	HistoryURL string
    48  }
    49  
    50  // jiraCreator is an interface for types that can create JIRA tickets.
    51  type jiraCreator interface {
    52  	CreateTicket(fields map[string]interface{}) (*thirdparty.JiraCreateTicketResponse, error)
    53  	JiraHost() string
    54  }
    55  
    56  // jiraDeliverer is an implementation of Deliverer that files JIRA tickets
    57  type jiraDeliverer struct {
    58  	project   string
    59  	issueType string
    60  	uiRoot    string
    61  	handler   jiraCreator
    62  }
    63  
    64  // isXgenProjBF is a gross function to figure out if the jira instance
    65  // and project are correctly configured for the specified kind of
    66  // requests/issue metadata.
    67  func isXgenProjBF(host, project string) bool {
    68  	if !strings.Contains(host, "mongodb") {
    69  		return false
    70  	}
    71  
    72  	return util.SliceContains(supportedJiraProjects, project)
    73  }
    74  
    75  // Deliver posts the alert defined by the AlertContext to JIRA.
    76  func (jd *jiraDeliverer) Deliver(ctx AlertContext, alertConf model.AlertConfig) error {
    77  	var err error
    78  	request := map[string]interface{}{}
    79  	request["project"] = map[string]string{"key": jd.project}
    80  	request["issuetype"] = map[string]string{"name": jd.issueType}
    81  	request["summary"] = getSummary(ctx)
    82  	request["description"], err = getDescription(ctx, jd.uiRoot)
    83  
    84  	if isXgenProjBF(jd.handler.JiraHost(), jd.project) {
    85  		request[jiraFailingTasksField] = []string{ctx.Task.DisplayName}
    86  		request[jiraFailingVariantField] = []string{ctx.Task.BuildVariant}
    87  		request[jiraEvergreenProjectField] = []string{ctx.ProjectRef.Identifier}
    88  	}
    89  
    90  	if err != nil {
    91  		return errors.Wrap(err, "error creating description")
    92  	}
    93  	grip.Infof("Creating '%v' JIRA ticket in %v for failure %v in project %s",
    94  		jd.issueType, jd.project, ctx.Task.Id, ctx.ProjectRef.Identifier)
    95  	result, err := jd.handler.CreateTicket(request)
    96  	if err != nil {
    97  		return errors.Wrap(err, "error creating JIRA ticket")
    98  	}
    99  	grip.Infof("Created JIRA ticket %v successfully", result.Key)
   100  	return nil
   101  }
   102  
   103  // getSummary creates a JIRA subject for a task failure in the style of
   104  //  Failures: Task_name on Variant (test1, test2) [ProjectName @ githash]
   105  // based on the given AlertContext.
   106  func getSummary(ctx AlertContext) string {
   107  	subj := &bytes.Buffer{}
   108  	failed := []string{}
   109  	for _, test := range ctx.Task.TestResults {
   110  		if test.Status == evergreen.TestFailedStatus {
   111  			failed = append(failed, cleanTestName(test.TestFile))
   112  		}
   113  	}
   114  	switch {
   115  	case ctx.Task.Details.TimedOut:
   116  		subj.WriteString("Timed Out: ")
   117  	case ctx.Task.Details.Type == model.SystemCommandType:
   118  		subj.WriteString("System Failure: ")
   119  	case len(failed) == 1:
   120  		subj.WriteString("Failure: ")
   121  	case len(failed) > 1:
   122  		subj.WriteString("Failures: ")
   123  	default:
   124  		subj.WriteString("Failed: ")
   125  	}
   126  
   127  	fmt.Fprintf(subj, "%s on %s ", ctx.Task.DisplayName, ctx.Build.DisplayName)
   128  
   129  	// include test names if <= 4 failed, otherwise print two plus the number remaining
   130  	if len(failed) > 0 {
   131  		subj.WriteString("(")
   132  		if len(failed) <= 4 {
   133  			subj.WriteString(strings.Join(failed, ", "))
   134  		} else {
   135  			fmt.Fprintf(subj, "%s, %s, +%v more", failed[0], failed[1], len(failed)-2)
   136  		}
   137  		subj.WriteString(") ")
   138  	}
   139  
   140  	fmt.Fprintf(subj, "[%s @ %s]", ctx.ProjectRef.DisplayName, ctx.Version.Revision[0:8])
   141  	return subj.String()
   142  }
   143  
   144  // historyURL provides a full URL to the test's task history page.
   145  func historyURL(t *task.Task, testName, uiRoot string) string {
   146  	return fmt.Sprintf("%v/task_history/%v/%v#%v=fail",
   147  		uiRoot, t.Project, t.DisplayName, testName)
   148  }
   149  
   150  // logURL returns the full URL for linking to a test's logs.
   151  // Returns the empty string if no internal or external log is referenced.
   152  func logURL(test task.TestResult, root string) string {
   153  	if test.LogId != "" {
   154  		return root + "/test_log/" + test.LogId
   155  	}
   156  	return test.URL
   157  }
   158  
   159  // getDescription returns the body of the JIRA ticket, with links.
   160  func getDescription(ctx AlertContext, uiRoot string) (string, error) {
   161  	// build a list of all failed tests to include
   162  	tests := []jiraTestFailure{}
   163  	for _, test := range ctx.Task.TestResults {
   164  		if test.Status == evergreen.TestFailedStatus {
   165  			tests = append(tests, jiraTestFailure{
   166  				Name:       cleanTestName(test.TestFile),
   167  				URL:        logURL(test, uiRoot),
   168  				HistoryURL: historyURL(ctx.Task, cleanTestName(test.TestFile), uiRoot),
   169  			})
   170  		}
   171  	}
   172  
   173  	args := struct {
   174  		Task    *task.Task
   175  		Build   *build.Build
   176  		Host    *host.Host
   177  		Project *model.ProjectRef
   178  		Version *version.Version
   179  		Tests   []jiraTestFailure
   180  		UIRoot  string
   181  	}{ctx.Task, ctx.Build, ctx.Host, ctx.ProjectRef, ctx.Version, tests, uiRoot}
   182  	buf := &bytes.Buffer{}
   183  	if err := DescriptionTemplate.Execute(buf, args); err != nil {
   184  		return "", err
   185  	}
   186  	return buf.String(), nil
   187  }