diff --git a/README.md b/README.md index 3c3c054..91f5bbd 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Usage of junit2jira: Convert XML to a CSV file (use dash [-] for stdout) -dry-run When set to true issues will NOT be created. + -html-output string + Generate HTML report (use dash [-] for stdout) -jira-url string Url of JIRA instance (default "https://issues.redhat.com/") -job-name string @@ -40,6 +42,9 @@ Usage of junit2jira: Number of reported failures that should cause single issue creation. (default 10) -timestamp string Timestamp of CI test. (default "2023-04-18T12:07:44+02:00") + -v short alias for -version + -version + print version information and exit ``` ## Example usage diff --git a/htmlOutput.tpl b/htmlOutput.tpl new file mode 100644 index 0000000..d9462d8 --- /dev/null +++ b/htmlOutput.tpl @@ -0,0 +1,20 @@ + + + <h4>Possible Flake Tests</h4> + + + + + + \ No newline at end of file diff --git a/main.go b/main.go index 2c2d4d7..9c2fd0c 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + _ "embed" "encoding/csv" "flag" "fmt" @@ -29,6 +30,7 @@ ORDER BY created DESC` func main() { p := params{} + flag.StringVar(&p.htmlOutput, "html-output", "", "Generate HTML report (use dash [-] for stdout)") flag.StringVar(&p.csvOutput, "csv-output", "", "Convert XML to a CSV file (use dash [-] for stdout)") flag.StringVar(&p.jiraUrl, "jira-url", "https://issues.redhat.com/", "Url of JIRA instance") flag.StringVar(&p.junitReportsDir, "junit-reports-dir", os.Getenv("ARTIFACT_DIR"), "Dir that contains jUnit reports XML files") @@ -88,10 +90,49 @@ func run(p params) error { return errors.Wrap(err, "could not find failed tests") } - err = j.createIssuesOrComments(failedTests) + issues, err := j.createIssuesOrComments(failedTests) if err != nil { return errors.Wrap(err, "could not create issues or comments") } + return j.createHtml(issues) +} + +//go:embed htmlOutput.tpl +var htmlOutputTemplate string + +func (j junit2jira) createHtml(issues []*jira.Issue) error { + if j.htmlOutput == "" || len(issues) == 0 { + return nil + } + out := os.Stdout + if j.htmlOutput != "-" { + file, err := os.Create(j.htmlOutput) + if err != nil { + return fmt.Errorf("could not create file %s: %w", j.htmlOutput, err) + } + out = file + defer file.Close() + } + return j.renderHtml(issues, out) +} + +type htmlData struct { + Issues []*jira.Issue + JiraUrl string +} + +func (j junit2jira) renderHtml(issues []*jira.Issue, out io.Writer) error { + t, err := template.New(j.htmlOutput).Parse(htmlOutputTemplate) + if err != nil { + return fmt.Errorf("could parse template: %w", err) + } + err = t.Execute(out, htmlData{ + Issues: issues, + JiraUrl: j.jiraUrl, + }) + if err != nil { + return fmt.Errorf("could not render template %s: %w", j.htmlOutput, err) + } return nil } @@ -111,31 +152,35 @@ func (j junit2jira) createCsv(testSuites []junit.Suite) error { return junit2csv(testSuites, j.params, out) } -func (j junit2jira) createIssuesOrComments(failedTests []testCase) error { +func (j junit2jira) createIssuesOrComments(failedTests []testCase) ([]*jira.Issue, error) { var result error + issues := make([]*jira.Issue, 0, len(failedTests)) for _, tc := range failedTests { - err := j.createIssueOrComment(tc) + issue, err := j.createIssueOrComment(tc) if err != nil { result = multierror.Append(result, err) } + if issue != nil { + issues = append(issues, issue) + } } - return result + return issues, result } -func (j junit2jira) createIssueOrComment(tc testCase) error { +func (j junit2jira) createIssueOrComment(tc testCase) (*jira.Issue, error) { summary, err := tc.summary() if err != nil { - return fmt.Errorf("could not get summary: %w", err) + return nil, fmt.Errorf("could not get summary: %w", err) } description, err := tc.description() if err != nil { - return fmt.Errorf("could not get description: %w", err) + return nil, fmt.Errorf("could not get description: %w", err) } log.Println("Searching for ", summary) search, response, err := j.jiraClient.Issue.Search(fmt.Sprintf(jql, summary), nil) if err != nil { logError(err, response) - return fmt.Errorf("could not search: %w", err) + return nil, fmt.Errorf("could not search: %w", err) } issue := findMatchingIssue(search, summary) @@ -148,15 +193,15 @@ func (j junit2jira) createIssueOrComment(tc testCase) error { log.Println("Dry run: will just print issue content") log.Println(summary) log.Println(description) - return nil + return nil, nil } create, response, err := j.jiraClient.Issue.Create(newIssue(summary, description)) if response != nil && err != nil { logError(err, response) - return fmt.Errorf("could not create issue %s: %w", summary, err) + return nil, fmt.Errorf("could not create issue %s: %w", summary, err) } log.Printf("Created new issues: %s:%s", create.Key, summary) - return nil + return create, nil } comment := jira.Comment{ @@ -168,16 +213,16 @@ func (j junit2jira) createIssueOrComment(tc testCase) error { if j.dryRun { log.Println("Dry run: will just print comment") log.Println(description) - return nil + return issue, nil } addComment, response, err := j.jiraClient.Issue.AddComment(issue.ID, &comment) if response != nil && err != nil { logError(err, response) - return fmt.Errorf("could not create issue %s: %w", summary, err) + return nil, fmt.Errorf("could not create issue %s: %w", summary, err) } log.Printf("Created comment %s for %s:%s ", addComment.ID, issue.Key, summary) - return nil + return issue, nil } func newIssue(summary string, description string) *jira.Issue { @@ -391,6 +436,7 @@ type params struct { junitReportsDir string timestamp string csvOutput string + htmlOutput string } func NewTestCase(tc junit.Test, p params) testCase { diff --git a/main_test.go b/main_test.go index 9aa4f29..e9c9aa1 100644 --- a/main_test.go +++ b/main_test.go @@ -2,8 +2,10 @@ package main import ( "bytes" + "github.com/andygrunwald/go-jira" "github.com/joshdk/go-junit" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" ) @@ -329,3 +331,36 @@ func TestCsvOutput(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "BuildId,Timestamp,Classname,Name,Duration,Status,JobName,BuildTag\n", buf.String()) } + +func TestHtmlOutput(t *testing.T) { + j := junit2jira{params: params{jiraUrl: "https://issues.redhat.com/"}} + + buf := bytes.NewBufferString("") + require.NoError(t, j.renderHtml(nil, buf)) + + issues := []*jira.Issue{ + {Key: "ROX-1", Fields: &jira.IssueFields{Summary: "abc"}}, + {Key: "ROX-2", Fields: &jira.IssueFields{Summary: "def"}}, + {Key: "ROX-3"}, + } + buf = bytes.NewBufferString("") + require.NoError(t, j.renderHtml(issues, buf)) + + assert.Equal(t, ` + + <h4>Possible Flake Tests</h4> + + + + + +`, buf.String()) +}