2022-02-17 16:16:55 +02:00
|
|
|
package reporting
|
|
|
|
|
|
|
|
import (
|
2022-08-02 08:26:26 +02:00
|
|
|
"context"
|
2022-02-17 16:16:55 +02:00
|
|
|
"fmt"
|
|
|
|
|
2022-03-17 16:32:48 +02:00
|
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
2022-08-02 08:26:26 +02:00
|
|
|
"github.com/google/go-github/v45/github"
|
2022-02-17 16:16:55 +02:00
|
|
|
)
|
|
|
|
|
2022-08-02 08:26:26 +02:00
|
|
|
type githubIssueService interface {
|
|
|
|
Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error)
|
|
|
|
CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
|
|
|
|
Edit(ctx context.Context, owner string, repo string, number int, issue *github.IssueRequest) (*github.Issue, *github.Response, error)
|
2022-02-17 16:16:55 +02:00
|
|
|
}
|
|
|
|
|
2022-08-02 08:26:26 +02:00
|
|
|
type githubSearchService interface {
|
|
|
|
Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GitHub contains metadata for reporting towards GitHub
|
|
|
|
type GitHub struct {
|
|
|
|
Owner *string
|
|
|
|
Repository *string
|
|
|
|
Assignees *[]string
|
|
|
|
IssueService githubIssueService
|
|
|
|
SearchService githubSearchService
|
|
|
|
}
|
|
|
|
|
|
|
|
// UploadSingleReport uploads a single report to GitHub
|
|
|
|
func (g *GitHub) UploadSingleReport(ctx context.Context, scanReport IssueDetail) error {
|
2022-02-17 16:16:55 +02:00
|
|
|
// JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub
|
|
|
|
// ignore JSON errors since structure is in our hands
|
2022-08-02 08:26:26 +02:00
|
|
|
title := scanReport.Title()
|
2022-02-17 16:16:55 +02:00
|
|
|
markdownReport, _ := scanReport.ToMarkdown()
|
2022-08-02 08:26:26 +02:00
|
|
|
|
|
|
|
log.Entry().Debugf("Creating/updating GitHub issue with title %v in org %v and repo %v", title, g.Owner, g.Repository)
|
|
|
|
if err := g.createIssueOrUpdateIssueComment(ctx, title, string(markdownReport)); err != nil {
|
|
|
|
return fmt.Errorf("failed to upload results for '%v' into GitHub issue: %w", title, err)
|
2022-02-17 16:16:55 +02:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2022-03-17 16:32:48 +02:00
|
|
|
|
2022-08-02 08:26:26 +02:00
|
|
|
// UploadMultipleReports uploads a number of reports to GitHub, one per IssueDetail to create transparency
|
|
|
|
func (g *GitHub) UploadMultipleReports(ctx context.Context, scanReports *[]IssueDetail) error {
|
|
|
|
for _, scanReport := range *scanReports {
|
|
|
|
if err := g.UploadSingleReport(ctx, scanReport); err != nil {
|
|
|
|
return err
|
2022-03-17 16:32:48 +02:00
|
|
|
}
|
2022-08-02 08:26:26 +02:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2022-03-17 16:32:48 +02:00
|
|
|
|
2022-08-02 08:26:26 +02:00
|
|
|
func (g *GitHub) createIssueOrUpdateIssueComment(ctx context.Context, title, issueContent string) error {
|
|
|
|
// check if issue is existing
|
|
|
|
issueNumber, issueBody, err := g.findExistingIssue(ctx, title)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error when looking up issue: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if issueNumber == 0 {
|
|
|
|
// issue not existing need to create it
|
|
|
|
issue := github.IssueRequest{Title: &title, Body: &issueContent, Assignees: g.Assignees}
|
|
|
|
if _, _, err := g.IssueService.Create(ctx, *g.Owner, *g.Repository, &issue); err != nil {
|
|
|
|
return fmt.Errorf("failed to create issue: %w", err)
|
2022-03-17 16:32:48 +02:00
|
|
|
}
|
2022-08-02 08:26:26 +02:00
|
|
|
return nil
|
2022-03-17 16:32:48 +02:00
|
|
|
}
|
|
|
|
|
2022-08-02 08:26:26 +02:00
|
|
|
// let's compare and only update in case an update is required
|
|
|
|
if issueContent != issueBody {
|
|
|
|
// update of issue required
|
|
|
|
issueRequest := github.IssueRequest{Body: &issueContent}
|
|
|
|
if _, _, err := g.IssueService.Edit(ctx, *g.Owner, *g.Repository, issueNumber, &issueRequest); err != nil {
|
|
|
|
return fmt.Errorf("failed to edit issue: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// now add a small comment that the issue content has been updated
|
|
|
|
updateText := "issue content has been updated"
|
|
|
|
updateComment := github.IssueComment{Body: &updateText}
|
|
|
|
if _, _, err := g.IssueService.CreateComment(ctx, *g.Owner, *g.Repository, issueNumber, &updateComment); err != nil {
|
|
|
|
return fmt.Errorf("failed to create comment: %w", err)
|
|
|
|
}
|
|
|
|
}
|
2022-03-17 16:32:48 +02:00
|
|
|
return nil
|
|
|
|
}
|
2022-08-02 08:26:26 +02:00
|
|
|
|
|
|
|
func (g *GitHub) findExistingIssue(ctx context.Context, title string) (int, string, error) {
|
|
|
|
queryString := fmt.Sprintf("is:issue repo:%v/%v in:title %v", *g.Owner, *g.Repository, title)
|
|
|
|
searchResult, _, err := g.SearchService.Issues(ctx, queryString, nil)
|
|
|
|
if err != nil {
|
|
|
|
return 0, "", fmt.Errorf("error occurred when looking for existing issue: %w", err)
|
|
|
|
}
|
|
|
|
for _, i := range searchResult.Issues {
|
|
|
|
if i != nil && *i.Title == title {
|
|
|
|
if i.GetState() == "closed" {
|
|
|
|
// reopen issue
|
|
|
|
open := "open"
|
|
|
|
ir := github.IssueRequest{State: &open}
|
|
|
|
if _, _, err := g.IssueService.Edit(ctx, *g.Owner, *g.Repository, i.GetNumber(), &ir); err != nil {
|
|
|
|
return i.GetNumber(), "", fmt.Errorf("failed to re-open issue: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return i.GetNumber(), i.GetBody(), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0, "", nil
|
|
|
|
}
|