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 }