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 }