github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/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  
   115  	switch {
   116  	case ctx.Task.Details.Description == task.AgentHeartbeat:
   117  		subj.WriteString("System Failure: ")
   118  	case ctx.Task.Details.Type == model.SystemCommandType:
   119  		subj.WriteString("System Failure: ")
   120  	case ctx.Task.Details.TimedOut:
   121  		subj.WriteString("Timed Out: ")
   122  	case len(failed) == 1:
   123  		subj.WriteString("Failure: ")
   124  	case len(failed) > 1:
   125  		subj.WriteString("Failures: ")
   126  	default:
   127  		subj.WriteString("Failed: ")
   128  	}
   129  
   130  	fmt.Fprintf(subj, "%s on %s ", ctx.Task.DisplayName, ctx.Build.DisplayName)
   131  
   132  	// include test names if <= 4 failed, otherwise print two plus the number remaining
   133  	if len(failed) > 0 {
   134  		subj.WriteString("(")
   135  		if len(failed) <= 4 {
   136  			subj.WriteString(strings.Join(failed, ", "))
   137  		} else {
   138  			fmt.Fprintf(subj, "%s, %s, +%v more", failed[0], failed[1], len(failed)-2)
   139  		}
   140  		subj.WriteString(") ")
   141  	}
   142  
   143  	fmt.Fprintf(subj, "[%s @ %s]", ctx.ProjectRef.DisplayName, ctx.Version.Revision[0:8])
   144  	return subj.String()
   145  }
   146  
   147  // historyURL provides a full URL to the test's task history page.
   148  func historyURL(t *task.Task, testName, uiRoot string) string {
   149  	return fmt.Sprintf("%v/task_history/%v/%v#%v=fail",
   150  		uiRoot, t.Project, t.DisplayName, testName)
   151  }
   152  
   153  // logURL returns the full URL for linking to a test's logs.
   154  // Returns the empty string if no internal or external log is referenced.
   155  func logURL(test task.TestResult, root string) string {
   156  	if test.LogId != "" {
   157  		return root + "/test_log/" + test.LogId
   158  	}
   159  	return test.URL
   160  }
   161  
   162  // getDescription returns the body of the JIRA ticket, with links.
   163  func getDescription(ctx AlertContext, uiRoot string) (string, error) {
   164  	// build a list of all failed tests to include
   165  	tests := []jiraTestFailure{}
   166  	for _, test := range ctx.Task.TestResults {
   167  		if test.Status == evergreen.TestFailedStatus {
   168  			tests = append(tests, jiraTestFailure{
   169  				Name:       cleanTestName(test.TestFile),
   170  				URL:        logURL(test, uiRoot),
   171  				HistoryURL: historyURL(ctx.Task, cleanTestName(test.TestFile), uiRoot),
   172  			})
   173  		}
   174  	}
   175  
   176  	args := struct {
   177  		Task    *task.Task
   178  		Build   *build.Build
   179  		Host    *host.Host
   180  		Project *model.ProjectRef
   181  		Version *version.Version
   182  		Tests   []jiraTestFailure
   183  		UIRoot  string
   184  	}{ctx.Task, ctx.Build, ctx.Host, ctx.ProjectRef, ctx.Version, tests, uiRoot}
   185  	buf := &bytes.Buffer{}
   186  	if err := DescriptionTemplate.Execute(buf, args); err != nil {
   187  		return "", err
   188  	}
   189  	return buf.String(), nil
   190  }