github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/reporting/github.go (about)

     1  package reporting
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/SAP/jenkins-library/pkg/log"
     8  	"github.com/google/go-github/v45/github"
     9  )
    10  
    11  type githubIssueService interface {
    12  	Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error)
    13  	CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
    14  	Edit(ctx context.Context, owner string, repo string, number int, issue *github.IssueRequest) (*github.Issue, *github.Response, error)
    15  }
    16  
    17  type githubSearchService interface {
    18  	Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error)
    19  }
    20  
    21  // GitHub contains metadata for reporting towards GitHub
    22  type GitHub struct {
    23  	Owner         *string
    24  	Repository    *string
    25  	Assignees     *[]string
    26  	IssueService  githubIssueService
    27  	SearchService githubSearchService
    28  }
    29  
    30  // UploadSingleReport uploads a single report to GitHub
    31  func (g *GitHub) UploadSingleReport(ctx context.Context, scanReport IssueDetail) error {
    32  	// JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub
    33  	// ignore JSON errors since structure is in our hands
    34  	title := scanReport.Title()
    35  	markdownReport, _ := scanReport.ToMarkdown()
    36  
    37  	log.Entry().Debugf("Creating/updating GitHub issue with title %v in org %v and repo %v", title, &g.Owner, &g.Repository)
    38  	if err := g.createIssueOrUpdateIssueComment(ctx, title, string(markdownReport)); err != nil {
    39  		return fmt.Errorf("failed to upload results for '%v' into GitHub issue: %w", title, err)
    40  	}
    41  	return nil
    42  }
    43  
    44  // UploadMultipleReports uploads a number of reports to GitHub, one per IssueDetail to create transparency
    45  func (g *GitHub) UploadMultipleReports(ctx context.Context, scanReports *[]IssueDetail) error {
    46  	for _, scanReport := range *scanReports {
    47  		if err := g.UploadSingleReport(ctx, scanReport); err != nil {
    48  			return err
    49  		}
    50  	}
    51  	return nil
    52  }
    53  
    54  func (g *GitHub) createIssueOrUpdateIssueComment(ctx context.Context, title, issueContent string) error {
    55  	// check if issue is existing
    56  	issueNumber, issueBody, err := g.findExistingIssue(ctx, title)
    57  	if err != nil {
    58  		return fmt.Errorf("error when looking up issue: %w", err)
    59  	}
    60  
    61  	if issueNumber == 0 {
    62  		// issue not existing need to create it
    63  		issue := github.IssueRequest{Title: &title, Body: &issueContent, Assignees: g.Assignees}
    64  		if _, _, err := g.IssueService.Create(ctx, *g.Owner, *g.Repository, &issue); err != nil {
    65  			return fmt.Errorf("failed to create issue: %w", err)
    66  		}
    67  		return nil
    68  	}
    69  
    70  	// let's compare and only update in case an update is required
    71  	if issueContent != issueBody {
    72  		// update of issue required
    73  		issueRequest := github.IssueRequest{Body: &issueContent}
    74  		if _, _, err := g.IssueService.Edit(ctx, *g.Owner, *g.Repository, issueNumber, &issueRequest); err != nil {
    75  			return fmt.Errorf("failed to edit issue: %w", err)
    76  		}
    77  
    78  		// now add a small comment that the issue content has been updated
    79  		updateText := "issue content has been updated"
    80  		updateComment := github.IssueComment{Body: &updateText}
    81  		if _, _, err := g.IssueService.CreateComment(ctx, *g.Owner, *g.Repository, issueNumber, &updateComment); err != nil {
    82  			return fmt.Errorf("failed to create comment: %w", err)
    83  		}
    84  	}
    85  	return nil
    86  }
    87  
    88  func (g *GitHub) findExistingIssue(ctx context.Context, title string) (int, string, error) {
    89  	queryString := fmt.Sprintf("is:issue repo:%v/%v in:title %v", *g.Owner, *g.Repository, title)
    90  	searchResult, _, err := g.SearchService.Issues(ctx, queryString, nil)
    91  	if err != nil {
    92  		return 0, "", fmt.Errorf("error occurred when looking for existing issue: %w", err)
    93  	}
    94  	for _, i := range searchResult.Issues {
    95  		if i != nil && *i.Title == title {
    96  			if i.GetState() == "closed" {
    97  				// reopen issue
    98  				open := "open"
    99  				ir := github.IssueRequest{State: &open}
   100  				if _, _, err := g.IssueService.Edit(ctx, *g.Owner, *g.Repository, i.GetNumber(), &ir); err != nil {
   101  					return i.GetNumber(), "", fmt.Errorf("failed to re-open issue: %w", err)
   102  				}
   103  			}
   104  			return i.GetNumber(), i.GetBody(), nil
   105  		}
   106  	}
   107  	return 0, "", nil
   108  }