From 37447873480f7a9410bea11e08976ba76fd899ee Mon Sep 17 00:00:00 2001 From: Googlom <36107508+Googlom@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:38:45 +0500 Subject: [PATCH] chore(refactor): Switch GitHub actions provider to use github sdk (#4563) * refactor github package and use builder pattern for client * switch to github package * some renamings * fix panic on uninitialized provider * fix according to review comments --------- Co-authored-by: Gulom Alimov Co-authored-by: Jordi van Liempt <35920075+jliempt@users.noreply.github.com> --- cmd/checkmarxExecuteScan.go | 2 +- cmd/checkmarxOneExecuteScan.go | 2 +- cmd/detectExecuteScan.go | 4 +- cmd/fortifyExecuteScan.go | 4 +- cmd/githubCheckBranchProtection.go | 2 +- cmd/githubCommentIssue.go | 2 +- cmd/githubCreatePullRequest.go | 2 +- cmd/githubPublishRelease.go | 4 +- cmd/githubSetCommitStatus.go | 2 +- cmd/vaultRotateSecretId.go | 6 +- cmd/whitesourceExecuteScan.go | 4 +- pkg/codeql/codeql.go | 6 +- pkg/github/commit.go | 2 +- pkg/github/create_issue.go | 103 ++++++++++ pkg/github/create_issue_test.go | 239 +++++++++++++++++++++++ pkg/github/github.go | 164 ++++++---------- pkg/github/github_test.go | 258 ++++--------------------- pkg/orchestrator/gitHubActions.go | 117 +++++++---- pkg/orchestrator/gitHubActions_test.go | 75 +++---- pkg/orchestrator/orchestrator.go | 7 +- pkg/piperutils/pointer.go | 10 + pkg/piperutils/pointer_test.go | 62 ++++++ 22 files changed, 661 insertions(+), 416 deletions(-) create mode 100644 pkg/github/create_issue.go create mode 100644 pkg/github/create_issue_test.go create mode 100644 pkg/piperutils/pointer.go create mode 100644 pkg/piperutils/pointer_test.go diff --git a/cmd/checkmarxExecuteScan.go b/cmd/checkmarxExecuteScan.go index 5de67bee13..5a284f8748 100644 --- a/cmd/checkmarxExecuteScan.go +++ b/cmd/checkmarxExecuteScan.go @@ -105,7 +105,7 @@ func checkmarxExecuteScan(config checkmarxExecuteScanOptions, _ *telemetry.Custo options := piperHttp.ClientOptions{MaxRetries: config.MaxRetries} client.SetOptions(options) // TODO provide parameter for trusted certs - ctx, ghClient, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "", []string{}) + ctx, ghClient, err := piperGithub.NewClientBuilder(config.GithubToken, config.GithubAPIURL).Build() if err != nil { log.Entry().WithError(err).Warning("Failed to get GitHub client") } diff --git a/cmd/checkmarxOneExecuteScan.go b/cmd/checkmarxOneExecuteScan.go index 4c2e254b6f..80bcc01e93 100644 --- a/cmd/checkmarxOneExecuteScan.go +++ b/cmd/checkmarxOneExecuteScan.go @@ -181,7 +181,7 @@ func runStep(config checkmarxOneExecuteScanOptions, influx *checkmarxOneExecuteS func Authenticate(config checkmarxOneExecuteScanOptions, influx *checkmarxOneExecuteScanInflux) (checkmarxOneExecuteScanHelper, error) { client := &piperHttp.Client{} - ctx, ghClient, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "", []string{}) + ctx, ghClient, err := piperGithub.NewClientBuilder(config.GithubToken, config.GithubAPIURL).Build() if err != nil { log.Entry().WithError(err).Warning("Failed to get GitHub client") } diff --git a/cmd/detectExecuteScan.go b/cmd/detectExecuteScan.go index 9ee9335931..5527b28ed8 100644 --- a/cmd/detectExecuteScan.go +++ b/cmd/detectExecuteScan.go @@ -132,7 +132,9 @@ func newBlackduckSystem(config detectExecuteScanOptions) *blackduckSystem { func detectExecuteScan(config detectExecuteScanOptions, _ *telemetry.CustomData, influx *detectExecuteScanInflux) { influx.step_data.fields.detect = false - ctx, client, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "", config.CustomTLSCertificateLinks) + ctx, client, err := piperGithub. + NewClientBuilder(config.GithubToken, config.GithubAPIURL). + WithTrustedCerts(config.CustomTLSCertificateLinks).Build() if err != nil { log.Entry().WithError(err).Warning("Failed to get GitHub client") } diff --git a/cmd/fortifyExecuteScan.go b/cmd/fortifyExecuteScan.go index 62e7297765..f2110fa552 100644 --- a/cmd/fortifyExecuteScan.go +++ b/cmd/fortifyExecuteScan.go @@ -114,7 +114,7 @@ var execInPath = exec.LookPath func fortifyExecuteScan(config fortifyExecuteScanOptions, telemetryData *telemetry.CustomData, influx *fortifyExecuteScanInflux) { // TODO provide parameter for trusted certs - ctx, client, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "", []string{}) + ctx, client, err := piperGithub.NewClientBuilder(config.GithubToken, config.GithubAPIURL).Build() if err != nil { log.Entry().WithError(err).Warning("Failed to get GitHub client") } @@ -1116,7 +1116,7 @@ func scanProject(config *fortifyExecuteScanOptions, command fortifyUtils, buildI func determinePullRequestMerge(config fortifyExecuteScanOptions) (string, string) { author := "" // TODO provide parameter for trusted certs - ctx, client, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "", []string{}) + ctx, client, err := piperGithub.NewClientBuilder(config.GithubToken, config.GithubAPIURL).Build() if err == nil && ctx != nil && client != nil { prID, author, err := determinePullRequestMergeGithub(ctx, config, client.PullRequests) if err != nil { diff --git a/cmd/githubCheckBranchProtection.go b/cmd/githubCheckBranchProtection.go index f8fc890330..2f85ebfee3 100644 --- a/cmd/githubCheckBranchProtection.go +++ b/cmd/githubCheckBranchProtection.go @@ -20,7 +20,7 @@ type gitHubBranchProtectionRepositoriesService interface { func githubCheckBranchProtection(config githubCheckBranchProtectionOptions, telemetryData *telemetry.CustomData) { // TODO provide parameter for trusted certs - ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, "", []string{}) + ctx, client, err := piperGithub.NewClientBuilder(config.Token, config.APIURL).Build() if err != nil { log.Entry().WithError(err).Fatal("Failed to get GitHub client") } diff --git a/cmd/githubCommentIssue.go b/cmd/githubCommentIssue.go index 72116d3d69..3b4c8bc264 100644 --- a/cmd/githubCommentIssue.go +++ b/cmd/githubCommentIssue.go @@ -17,7 +17,7 @@ type githubIssueCommentService interface { func githubCommentIssue(config githubCommentIssueOptions, telemetryData *telemetry.CustomData) { // TODO provide parameter for trusted certs - ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, "", []string{}) + ctx, client, err := piperGithub.NewClientBuilder(config.Token, config.APIURL).Build() if err != nil { log.Entry().WithError(err).Fatal("Failed to get GitHub client") } diff --git a/cmd/githubCreatePullRequest.go b/cmd/githubCreatePullRequest.go index 5494c1d92a..32c007c6ee 100644 --- a/cmd/githubCreatePullRequest.go +++ b/cmd/githubCreatePullRequest.go @@ -21,7 +21,7 @@ type githubIssueService interface { func githubCreatePullRequest(config githubCreatePullRequestOptions, telemetryData *telemetry.CustomData) { // TODO provide parameter for trusted certs - ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, "", []string{}) + ctx, client, err := piperGithub.NewClientBuilder(config.Token, config.APIURL).Build() if err != nil { log.Entry().WithError(err).Fatal("Failed to get GitHub client") } diff --git a/cmd/githubPublishRelease.go b/cmd/githubPublishRelease.go index ca0ac421a7..8f14adc03d 100644 --- a/cmd/githubPublishRelease.go +++ b/cmd/githubPublishRelease.go @@ -31,7 +31,9 @@ type githubIssueClient interface { func githubPublishRelease(config githubPublishReleaseOptions, telemetryData *telemetry.CustomData) { // TODO provide parameter for trusted certs - ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, config.UploadURL, []string{}) + ctx, client, err := piperGithub. + NewClientBuilder(config.Token, config.APIURL). + WithUploadURL(config.UploadURL).Build() if err != nil { log.Entry().WithError(err).Fatal("Failed to get GitHub client.") } diff --git a/cmd/githubSetCommitStatus.go b/cmd/githubSetCommitStatus.go index e5eae9f483..ac73c15723 100644 --- a/cmd/githubSetCommitStatus.go +++ b/cmd/githubSetCommitStatus.go @@ -20,7 +20,7 @@ type gitHubCommitStatusRepositoriesService interface { func githubSetCommitStatus(config githubSetCommitStatusOptions, telemetryData *telemetry.CustomData) { // TODO provide parameter for trusted certs - ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, "", []string{}) + ctx, client, err := piperGithub.NewClientBuilder(config.Token, config.APIURL).Build() if err != nil { log.Entry().WithError(err).Fatal("Failed to get GitHub client") } diff --git a/cmd/vaultRotateSecretId.go b/cmd/vaultRotateSecretId.go index 5f2f9a0b88..3844c40edd 100644 --- a/cmd/vaultRotateSecretId.go +++ b/cmd/vaultRotateSecretId.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/vault/api" "github.com/SAP/jenkins-library/pkg/ado" - "github.com/SAP/jenkins-library/pkg/github" + piperGithub "github.com/SAP/jenkins-library/pkg/github" "github.com/SAP/jenkins-library/pkg/jenkins" "github.com/SAP/jenkins-library/pkg/vault" @@ -136,7 +136,7 @@ func writeVaultSecretIDToStore(config *vaultRotateSecretIdOptions, secretID stri // Additional info: // https://github.com/google/go-github/blob/master/example/newreposecretwithxcrypto/main.go - ctx, client, err := github.NewClient(config.GithubToken, config.GithubAPIURL, "", []string{}) + ctx, client, err := piperGithub.NewClientBuilder(config.GithubToken, config.GithubAPIURL).Build() if err != nil { log.Entry().Warnf("Could not write secret ID back to GitHub Actions: GitHub client not created: %v", err) return err @@ -148,7 +148,7 @@ func writeVaultSecretIDToStore(config *vaultRotateSecretIdOptions, secretID stri return err } - encryptedSecret, err := github.CreateEncryptedSecret(config.VaultAppRoleSecretTokenCredentialsID, secretID, publicKey) + encryptedSecret, err := piperGithub.CreateEncryptedSecret(config.VaultAppRoleSecretTokenCredentialsID, secretID, publicKey) if err != nil { log.Entry().Warnf("Could not write secret ID back to GitHub Actions: secret encryption failed: %v", err) return err diff --git a/cmd/whitesourceExecuteScan.go b/cmd/whitesourceExecuteScan.go index 15a50730a4..aa02b4cc97 100644 --- a/cmd/whitesourceExecuteScan.go +++ b/cmd/whitesourceExecuteScan.go @@ -139,7 +139,9 @@ func newWhitesourceScan(config *ScanOptions) *ws.Scan { } func whitesourceExecuteScan(config ScanOptions, _ *telemetry.CustomData, commonPipelineEnvironment *whitesourceExecuteScanCommonPipelineEnvironment, influx *whitesourceExecuteScanInflux) { - ctx, client, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "", config.CustomTLSCertificateLinks) + ctx, client, err := piperGithub. + NewClientBuilder(config.GithubToken, config.GithubAPIURL). + WithTrustedCerts(config.CustomTLSCertificateLinks).Build() if err != nil { log.Entry().WithError(err).Warning("Failed to get GitHub client") } diff --git a/pkg/codeql/codeql.go b/pkg/codeql/codeql.go index 80b44bdf3c..a365c96502 100644 --- a/pkg/codeql/codeql.go +++ b/pkg/codeql/codeql.go @@ -3,7 +3,7 @@ package codeql import ( "context" - sapgithub "github.com/SAP/jenkins-library/pkg/github" + piperGithub "github.com/SAP/jenkins-library/pkg/github" "github.com/google/go-github/v45/github" ) @@ -35,7 +35,9 @@ type CodeqlScanAuditInstance struct { func (codeqlScanAudit *CodeqlScanAuditInstance) GetVulnerabilities(analyzedRef string) ([]CodeqlFindings, error) { apiUrl := getApiUrl(codeqlScanAudit.serverUrl) - ctx, client, err := sapgithub.NewClient(codeqlScanAudit.token, apiUrl, "", codeqlScanAudit.trustedCerts) + ctx, client, err := piperGithub. + NewClientBuilder(codeqlScanAudit.token, apiUrl). + WithTrustedCerts(codeqlScanAudit.trustedCerts).Build() if err != nil { return []CodeqlFindings{}, err } diff --git a/pkg/github/commit.go b/pkg/github/commit.go index 9ce1104720..64b9ec7297 100644 --- a/pkg/github/commit.go +++ b/pkg/github/commit.go @@ -27,7 +27,7 @@ type FetchCommitResult struct { // FetchCommitStatistics looks up the statistics for a certain commit SHA. func FetchCommitStatistics(options *FetchCommitOptions) (FetchCommitResult, error) { // create GitHub client - ctx, client, err := NewClient(options.Token, options.APIURL, "", options.TrustedCerts) + ctx, client, err := NewClientBuilder(options.Token, options.APIURL).WithTrustedCerts(options.TrustedCerts).Build() if err != nil { return FetchCommitResult{}, errors.Wrap(err, "failed to get GitHub client") } diff --git a/pkg/github/create_issue.go b/pkg/github/create_issue.go new file mode 100644 index 0000000000..f4cc7fc35f --- /dev/null +++ b/pkg/github/create_issue.go @@ -0,0 +1,103 @@ +package github + +import ( + "context" + "fmt" + + "github.com/SAP/jenkins-library/pkg/log" + "github.com/google/go-github/v45/github" + "github.com/pkg/errors" +) + +// CreateIssueOptions to configure the creation +type CreateIssueOptions struct { + APIURL string `json:"apiUrl,omitempty"` + Assignees []string `json:"assignees,omitempty"` + Body []byte `json:"body,omitempty"` + Owner string `json:"owner,omitempty"` + Repository string `json:"repository,omitempty"` + Title string `json:"title,omitempty"` + UpdateExisting bool `json:"updateExisting,omitempty"` + Token string `json:"token,omitempty"` + TrustedCerts []string `json:"trustedCerts,omitempty"` + Issue *github.Issue `json:"issue,omitempty"` +} + +func CreateIssue(options *CreateIssueOptions) (*github.Issue, error) { + ctx, client, err := NewClientBuilder(options.Token, options.APIURL).WithTrustedCerts(options.TrustedCerts).Build() + if err != nil { + return nil, errors.Wrap(err, "failed to get GitHub client") + } + return createIssueLocal(ctx, options, client.Issues, client.Search, client.Issues) +} + +func createIssueLocal( + ctx context.Context, + options *CreateIssueOptions, + createIssueService githubCreateIssueService, + searchIssuesService githubSearchIssuesService, + createCommentService githubCreateCommentService, +) (*github.Issue, error) { + issue := github.IssueRequest{ + Title: &options.Title, + } + var bodyString string + if len(options.Body) > 0 { + bodyString = string(options.Body) + } else { + bodyString = "" + } + issue.Body = &bodyString + if len(options.Assignees) > 0 { + issue.Assignees = &options.Assignees + } else { + issue.Assignees = &[]string{} + } + + var existingIssue *github.Issue = nil + + if options.UpdateExisting { + existingIssue = options.Issue + if existingIssue == nil { + queryString := fmt.Sprintf("is:open is:issue repo:%v/%v in:title %v", options.Owner, options.Repository, options.Title) + searchResult, resp, err := searchIssuesService.Issues(ctx, queryString, nil) + if err != nil { + if resp != nil { + log.Entry().Errorf("GitHub search issue returned response code %v", resp.Status) + } + return nil, errors.Wrap(err, "error occurred when looking for existing issue") + } else { + for _, value := range searchResult.Issues { + if value != nil && *value.Title == options.Title { + existingIssue = value + } + } + } + } + + if existingIssue != nil { + comment := &github.IssueComment{Body: issue.Body} + _, resp, err := createCommentService.CreateComment(ctx, options.Owner, options.Repository, *existingIssue.Number, comment) + if err != nil { + if resp != nil { + log.Entry().Errorf("GitHub create comment returned response code %v", resp.Status) + } + return nil, errors.Wrap(err, "error occurred when adding comment to existing issue") + } + } + } + + if existingIssue == nil { + newIssue, resp, err := createIssueService.Create(ctx, options.Owner, options.Repository, &issue) + if err != nil { + if resp != nil { + log.Entry().Errorf("GitHub create issue returned response code %v", resp.Status) + } + return nil, errors.Wrap(err, "error occurred when creating issue") + } + log.Entry().Debugf("New issue created: %v", newIssue) + existingIssue = newIssue + } + + return existingIssue, nil +} diff --git a/pkg/github/create_issue_test.go b/pkg/github/create_issue_test.go new file mode 100644 index 0000000000..d58805435a --- /dev/null +++ b/pkg/github/create_issue_test.go @@ -0,0 +1,239 @@ +//go:build unit +// +build unit + +package github + +import ( + "context" + "fmt" + "net/http" + "regexp" + "testing" + + "github.com/google/go-github/v45/github" + "github.com/stretchr/testify/assert" +) + +type ghCreateIssueMock struct { + issue *github.IssueRequest + issueID int64 + issueError error + owner string + repo string + number int + assignees []string +} + +func (g *ghCreateIssueMock) Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error) { + g.issue = issue + g.owner = owner + g.repo = repo + g.assignees = *issue.Assignees + + issueResponse := github.Issue{ID: &g.issueID, Title: issue.Title, Body: issue.Body} + + ghRes := github.Response{Response: &http.Response{Status: "200"}} + if g.issueError != nil { + ghRes.Status = "401" + } + + return &issueResponse, &ghRes, g.issueError +} + +type ghSearchIssuesMock struct { + issueID int64 + issueNumber int + issueTitle string + issueBody string + issuesSearchResult *github.IssuesSearchResult + issuesSearchError error +} + +func (g *ghSearchIssuesMock) Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error) { + regex := regexp.MustCompile(`.*in:title (?P(.*))`) + matches := regex.FindStringSubmatch(query) + + g.issueTitle = matches[1] + + issues := []*github.Issue{ + { + ID: &g.issueID, + Number: &g.issueNumber, + Title: &g.issueTitle, + Body: &g.issueBody, + }, + } + + total := len(issues) + incompleteResults := false + + g.issuesSearchResult = &github.IssuesSearchResult{ + Issues: issues, + Total: &total, + IncompleteResults: &incompleteResults, + } + + ghRes := github.Response{Response: &http.Response{Status: "200"}} + if g.issuesSearchError != nil { + ghRes.Status = "401" + } + + return g.issuesSearchResult, &ghRes, g.issuesSearchError +} + +type ghCreateCommentMock struct { + issueComment *github.IssueComment + issueNumber int + issueCommentError error +} + +func (g *ghCreateCommentMock) CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + g.issueComment = comment + g.issueNumber = number + ghRes := github.Response{Response: &http.Response{Status: "200"}} + if g.issueCommentError != nil { + ghRes.Status = "401" + } + return g.issueComment, &ghRes, g.issueCommentError +} + +func TestRunGithubCreateIssue(t *testing.T) { + ctx := context.Background() + t.Parallel() + + t.Run("Success", func(t *testing.T) { + // init + ghCreateIssueService := ghCreateIssueMock{ + issueID: 1, + } + ghSearchIssuesMock := ghSearchIssuesMock{ + issueID: 1, + } + ghCreateCommentMock := ghCreateCommentMock{} + config := CreateIssueOptions{ + Owner: "TEST", + Repository: "test", + Body: []byte("This is my test body"), + Title: "This is my title", + Assignees: []string{"userIdOne", "userIdTwo"}, + } + + // test + _, err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock) + + // assert + assert.NoError(t, err) + assert.Equal(t, config.Owner, ghCreateIssueService.owner) + assert.Equal(t, config.Repository, ghCreateIssueService.repo) + assert.Equal(t, "This is my test body", ghCreateIssueService.issue.GetBody()) + assert.Equal(t, config.Title, ghCreateIssueService.issue.GetTitle()) + assert.Equal(t, config.Assignees, ghCreateIssueService.issue.GetAssignees()) + assert.Nil(t, ghSearchIssuesMock.issuesSearchResult) + assert.Nil(t, ghCreateCommentMock.issueComment) + }) + + t.Run("Success update existing", func(t *testing.T) { + // init + ghSearchIssuesMock := ghSearchIssuesMock{ + issueID: 1, + } + ghCreateCommentMock := ghCreateCommentMock{} + config := CreateIssueOptions{ + Owner: "TEST", + Repository: "test", + Body: []byte("This is my test body"), + Title: "This is my title", + Assignees: []string{"userIdOne", "userIdTwo"}, + UpdateExisting: true, + } + + // test + _, err := createIssueLocal(ctx, &config, nil, &ghSearchIssuesMock, &ghCreateCommentMock) + + // assert + assert.NoError(t, err) + assert.NotNil(t, ghSearchIssuesMock.issuesSearchResult) + assert.NotNil(t, ghCreateCommentMock.issueComment) + assert.Equal(t, config.Title, ghSearchIssuesMock.issueTitle) + assert.Equal(t, config.Title, *ghSearchIssuesMock.issuesSearchResult.Issues[0].Title) + assert.Equal(t, "This is my test body", ghCreateCommentMock.issueComment.GetBody()) + }) + + t.Run("Success update existing based on instance", func(t *testing.T) { + // init + ghSearchIssuesMock := ghSearchIssuesMock{ + issueID: 1, + } + ghCreateCommentMock := ghCreateCommentMock{} + var id int64 = 2 + var number int = 123 + config := CreateIssueOptions{ + Owner: "TEST", + Repository: "test", + Body: []byte("This is my test body"), + Title: "This is my title", + Assignees: []string{"userIdOne", "userIdTwo"}, + UpdateExisting: true, + Issue: &github.Issue{ + ID: &id, + Number: &number, + }, + } + + // test + _, err := createIssueLocal(ctx, &config, nil, &ghSearchIssuesMock, &ghCreateCommentMock) + + // assert + assert.NoError(t, err) + assert.Nil(t, ghSearchIssuesMock.issuesSearchResult) + assert.NotNil(t, ghCreateCommentMock.issueComment) + assert.Equal(t, ghCreateCommentMock.issueNumber, number) + assert.Equal(t, "This is my test body", ghCreateCommentMock.issueComment.GetBody()) + }) + + t.Run("Empty body", func(t *testing.T) { + // init + ghCreateIssueService := ghCreateIssueMock{ + issueID: 1, + } + ghSearchIssuesMock := ghSearchIssuesMock{ + issueID: 1, + } + ghCreateCommentMock := ghCreateCommentMock{} + config := CreateIssueOptions{ + Owner: "TEST", + Repository: "test", + Body: []byte(""), + Title: "This is my title", + Assignees: []string{"userIdOne", "userIdTwo"}, + UpdateExisting: true, + } + + // test + _, err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock) + + // assert + assert.NoError(t, err) + assert.NotNil(t, ghSearchIssuesMock.issuesSearchResult) + assert.NotNil(t, ghCreateCommentMock.issueComment) + assert.Equal(t, config.Title, ghSearchIssuesMock.issueTitle) + assert.Equal(t, config.Title, *ghSearchIssuesMock.issuesSearchResult.Issues[0].Title) + assert.Equal(t, "", ghCreateCommentMock.issueComment.GetBody()) + }) + + t.Run("Create error", func(t *testing.T) { + // init + ghCreateIssueService := ghCreateIssueMock{ + issueError: fmt.Errorf("error creating issue"), + } + config := CreateIssueOptions{ + Body: []byte("test content"), + } + + // test + _, err := createIssueLocal(ctx, &config, &ghCreateIssueService, nil, nil) + + // assert + assert.EqualError(t, err, "error occurred when creating issue: error creating issue") + }) +} diff --git a/pkg/github/github.go b/pkg/github/github.go index c70e048091..412f596c1a 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -2,12 +2,11 @@ package github import ( "context" - "fmt" "net/url" "strings" + "time" piperhttp "github.com/SAP/jenkins-library/pkg/http" - "github.com/SAP/jenkins-library/pkg/log" "github.com/google/go-github/v45/github" "github.com/pkg/errors" "golang.org/x/oauth2" @@ -25,125 +24,86 @@ type githubCreateCommentService interface { CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) } -// CreateIssueOptions to configure the creation -type CreateIssueOptions struct { - APIURL string `json:"apiUrl,omitempty"` - Assignees []string `json:"assignees,omitempty"` - Body []byte `json:"body,omitempty"` - Owner string `json:"owner,omitempty"` - Repository string `json:"repository,omitempty"` - Title string `json:"title,omitempty"` - UpdateExisting bool `json:"updateExisting,omitempty"` - Token string `json:"token,omitempty"` - TrustedCerts []string `json:"trustedCerts,omitempty"` - Issue *github.Issue `json:"issue,omitempty"` +type ClientBuilder struct { + token string // GitHub token, required + baseURL string // GitHub API URL, required + uploadURL string // Base URL for uploading files, optional + timeout time.Duration + maxRetries int + trustedCerts []string // Trusted TLS certificates, optional } -// NewClient creates a new GitHub client using an OAuth token for authentication -func NewClient(token, apiURL, uploadURL string, trustedCerts []string) (context.Context, *github.Client, error) { - httpClient := piperhttp.Client{} - httpClient.SetOptions(piperhttp.ClientOptions{ - TrustedCerts: trustedCerts, - DoLogRequestBodyOnDebug: true, - DoLogResponseBodyOnDebug: true, - }) - stdClient := httpClient.StandardClient() - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, stdClient) - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token, TokenType: "Bearer"}) - tc := oauth2.NewClient(ctx, ts) - - if !strings.HasSuffix(apiURL, "/") { - apiURL += "/" +func NewClientBuilder(token, baseURL string) *ClientBuilder { + if !strings.HasSuffix(baseURL, "/") { + baseURL += "/" } - baseURL, err := url.Parse(apiURL) - if err != nil { - return ctx, nil, err + + return &ClientBuilder{ + token: token, + baseURL: baseURL, + uploadURL: "", + timeout: 0, + maxRetries: 0, + trustedCerts: nil, } +} +func (b *ClientBuilder) WithTrustedCerts(trustedCerts []string) *ClientBuilder { + b.trustedCerts = trustedCerts + return b +} + +func (b *ClientBuilder) WithUploadURL(uploadURL string) *ClientBuilder { if !strings.HasSuffix(uploadURL, "/") { uploadURL += "/" } - uploadTargetURL, err := url.Parse(uploadURL) - if err != nil { - return ctx, nil, err - } - client := github.NewClient(tc) + b.uploadURL = uploadURL + return b +} - client.BaseURL = baseURL - client.UploadURL = uploadTargetURL - return ctx, client, nil +func (b *ClientBuilder) WithTimeout(timeout time.Duration) *ClientBuilder { + b.timeout = timeout + return b } -func CreateIssue(ghCreateIssueOptions *CreateIssueOptions) (*github.Issue, error) { - ctx, client, err := NewClient(ghCreateIssueOptions.Token, ghCreateIssueOptions.APIURL, "", ghCreateIssueOptions.TrustedCerts) - if err != nil { - return nil, errors.Wrap(err, "failed to get GitHub client") - } - return createIssueLocal(ctx, ghCreateIssueOptions, client.Issues, client.Search, client.Issues) +func (b *ClientBuilder) WithMaxRetries(maxRetries int) *ClientBuilder { + b.maxRetries = maxRetries + return b } -func createIssueLocal(ctx context.Context, ghCreateIssueOptions *CreateIssueOptions, ghCreateIssueService githubCreateIssueService, ghSearchIssuesService githubSearchIssuesService, ghCreateCommentService githubCreateCommentService) (*github.Issue, error) { - issue := github.IssueRequest{ - Title: &ghCreateIssueOptions.Title, - } - var bodyString string - if len(ghCreateIssueOptions.Body) > 0 { - bodyString = string(ghCreateIssueOptions.Body) - } else { - bodyString = "" +func (b *ClientBuilder) Build() (context.Context, *github.Client, error) { + baseURL, err := url.Parse(b.baseURL) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to parse baseURL") } - issue.Body = &bodyString - if len(ghCreateIssueOptions.Assignees) > 0 { - issue.Assignees = &ghCreateIssueOptions.Assignees - } else { - issue.Assignees = &[]string{} + + uploadURL, err := url.Parse(b.uploadURL) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to parse uploadURL") } - var existingIssue *github.Issue = nil - - if ghCreateIssueOptions.UpdateExisting { - existingIssue = ghCreateIssueOptions.Issue - if existingIssue == nil { - queryString := fmt.Sprintf("is:open is:issue repo:%v/%v in:title %v", ghCreateIssueOptions.Owner, ghCreateIssueOptions.Repository, ghCreateIssueOptions.Title) - searchResult, resp, err := ghSearchIssuesService.Issues(ctx, queryString, nil) - if err != nil { - if resp != nil { - log.Entry().Errorf("GitHub search issue returned response code %v", resp.Status) - } - return nil, errors.Wrap(err, "error occurred when looking for existing issue") - } else { - for _, value := range searchResult.Issues { - if value != nil && *value.Title == ghCreateIssueOptions.Title { - existingIssue = value - } - } - } - } - - if existingIssue != nil { - comment := &github.IssueComment{Body: issue.Body} - _, resp, err := ghCreateCommentService.CreateComment(ctx, ghCreateIssueOptions.Owner, ghCreateIssueOptions.Repository, *existingIssue.Number, comment) - if err != nil { - if resp != nil { - log.Entry().Errorf("GitHub create comment returned response code %v", resp.Status) - } - return nil, errors.Wrap(err, "error occurred when adding comment to existing issue") - } - } + if b.timeout == 0 { + b.timeout = 30 * time.Second } - if existingIssue == nil { - newIssue, resp, err := ghCreateIssueService.Create(ctx, ghCreateIssueOptions.Owner, ghCreateIssueOptions.Repository, &issue) - if err != nil { - if resp != nil { - log.Entry().Errorf("GitHub create issue returned response code %v", resp.Status) - } - return nil, errors.Wrap(err, "error occurred when creating issue") - } - log.Entry().Debugf("New issue created: %v", newIssue) - existingIssue = newIssue + if b.maxRetries == 0 { + b.maxRetries = 5 } - return existingIssue, nil + piperHttp := piperhttp.Client{} + piperHttp.SetOptions(piperhttp.ClientOptions{ + TrustedCerts: b.trustedCerts, + DoLogRequestBodyOnDebug: true, + DoLogResponseBodyOnDebug: true, + TransportTimeout: b.timeout, + MaxRetries: b.maxRetries, + }) + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, piperHttp.StandardClient()) + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: b.token, TokenType: "Bearer"}) + + client := github.NewClient(oauth2.NewClient(ctx, tokenSource)) + client.BaseURL = baseURL + client.UploadURL = uploadURL + return ctx, client, nil } diff --git a/pkg/github/github_test.go b/pkg/github/github_test.go index d58805435a..1799e04f30 100644 --- a/pkg/github/github_test.go +++ b/pkg/github/github_test.go @@ -1,239 +1,47 @@ -//go:build unit -// +build unit - package github import ( - "context" - "fmt" - "net/http" - "regexp" "testing" - "github.com/google/go-github/v45/github" "github.com/stretchr/testify/assert" ) -type ghCreateIssueMock struct { - issue *github.IssueRequest - issueID int64 - issueError error - owner string - repo string - number int - assignees []string -} - -func (g *ghCreateIssueMock) Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error) { - g.issue = issue - g.owner = owner - g.repo = repo - g.assignees = *issue.Assignees - - issueResponse := github.Issue{ID: &g.issueID, Title: issue.Title, Body: issue.Body} - - ghRes := github.Response{Response: &http.Response{Status: "200"}} - if g.issueError != nil { - ghRes.Status = "401" +func TestNewClientBuilder(t *testing.T) { + type args struct { + token string + baseURL string } - - return &issueResponse, &ghRes, g.issueError -} - -type ghSearchIssuesMock struct { - issueID int64 - issueNumber int - issueTitle string - issueBody string - issuesSearchResult *github.IssuesSearchResult - issuesSearchError error -} - -func (g *ghSearchIssuesMock) Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error) { - regex := regexp.MustCompile(`.*in:title (?P<Title>(.*))`) - matches := regex.FindStringSubmatch(query) - - g.issueTitle = matches[1] - - issues := []*github.Issue{ + tests := []struct { + name string + args args + want *ClientBuilder + }{ { - ID: &g.issueID, - Number: &g.issueNumber, - Title: &g.issueTitle, - Body: &g.issueBody, + name: "token and baseURL", + args: args{ + token: "test_token", + baseURL: "https://test.com/", + }, + want: &ClientBuilder{ + token: "test_token", + baseURL: "https://test.com/", + }, + }, + { + name: "baseURL without prefix", + args: args{ + token: "test_token", + baseURL: "https://test.com", + }, + want: &ClientBuilder{ + token: "test_token", + baseURL: "https://test.com/", + }, }, } - - total := len(issues) - incompleteResults := false - - g.issuesSearchResult = &github.IssuesSearchResult{ - Issues: issues, - Total: &total, - IncompleteResults: &incompleteResults, - } - - ghRes := github.Response{Response: &http.Response{Status: "200"}} - if g.issuesSearchError != nil { - ghRes.Status = "401" - } - - return g.issuesSearchResult, &ghRes, g.issuesSearchError -} - -type ghCreateCommentMock struct { - issueComment *github.IssueComment - issueNumber int - issueCommentError error -} - -func (g *ghCreateCommentMock) CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { - g.issueComment = comment - g.issueNumber = number - ghRes := github.Response{Response: &http.Response{Status: "200"}} - if g.issueCommentError != nil { - ghRes.Status = "401" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, NewClientBuilder(tt.args.token, tt.args.baseURL), "NewClientBuilder(%v, %v)", tt.args.token, tt.args.baseURL) + }) } - return g.issueComment, &ghRes, g.issueCommentError -} - -func TestRunGithubCreateIssue(t *testing.T) { - ctx := context.Background() - t.Parallel() - - t.Run("Success", func(t *testing.T) { - // init - ghCreateIssueService := ghCreateIssueMock{ - issueID: 1, - } - ghSearchIssuesMock := ghSearchIssuesMock{ - issueID: 1, - } - ghCreateCommentMock := ghCreateCommentMock{} - config := CreateIssueOptions{ - Owner: "TEST", - Repository: "test", - Body: []byte("This is my test body"), - Title: "This is my title", - Assignees: []string{"userIdOne", "userIdTwo"}, - } - - // test - _, err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock) - - // assert - assert.NoError(t, err) - assert.Equal(t, config.Owner, ghCreateIssueService.owner) - assert.Equal(t, config.Repository, ghCreateIssueService.repo) - assert.Equal(t, "This is my test body", ghCreateIssueService.issue.GetBody()) - assert.Equal(t, config.Title, ghCreateIssueService.issue.GetTitle()) - assert.Equal(t, config.Assignees, ghCreateIssueService.issue.GetAssignees()) - assert.Nil(t, ghSearchIssuesMock.issuesSearchResult) - assert.Nil(t, ghCreateCommentMock.issueComment) - }) - - t.Run("Success update existing", func(t *testing.T) { - // init - ghSearchIssuesMock := ghSearchIssuesMock{ - issueID: 1, - } - ghCreateCommentMock := ghCreateCommentMock{} - config := CreateIssueOptions{ - Owner: "TEST", - Repository: "test", - Body: []byte("This is my test body"), - Title: "This is my title", - Assignees: []string{"userIdOne", "userIdTwo"}, - UpdateExisting: true, - } - - // test - _, err := createIssueLocal(ctx, &config, nil, &ghSearchIssuesMock, &ghCreateCommentMock) - - // assert - assert.NoError(t, err) - assert.NotNil(t, ghSearchIssuesMock.issuesSearchResult) - assert.NotNil(t, ghCreateCommentMock.issueComment) - assert.Equal(t, config.Title, ghSearchIssuesMock.issueTitle) - assert.Equal(t, config.Title, *ghSearchIssuesMock.issuesSearchResult.Issues[0].Title) - assert.Equal(t, "This is my test body", ghCreateCommentMock.issueComment.GetBody()) - }) - - t.Run("Success update existing based on instance", func(t *testing.T) { - // init - ghSearchIssuesMock := ghSearchIssuesMock{ - issueID: 1, - } - ghCreateCommentMock := ghCreateCommentMock{} - var id int64 = 2 - var number int = 123 - config := CreateIssueOptions{ - Owner: "TEST", - Repository: "test", - Body: []byte("This is my test body"), - Title: "This is my title", - Assignees: []string{"userIdOne", "userIdTwo"}, - UpdateExisting: true, - Issue: &github.Issue{ - ID: &id, - Number: &number, - }, - } - - // test - _, err := createIssueLocal(ctx, &config, nil, &ghSearchIssuesMock, &ghCreateCommentMock) - - // assert - assert.NoError(t, err) - assert.Nil(t, ghSearchIssuesMock.issuesSearchResult) - assert.NotNil(t, ghCreateCommentMock.issueComment) - assert.Equal(t, ghCreateCommentMock.issueNumber, number) - assert.Equal(t, "This is my test body", ghCreateCommentMock.issueComment.GetBody()) - }) - - t.Run("Empty body", func(t *testing.T) { - // init - ghCreateIssueService := ghCreateIssueMock{ - issueID: 1, - } - ghSearchIssuesMock := ghSearchIssuesMock{ - issueID: 1, - } - ghCreateCommentMock := ghCreateCommentMock{} - config := CreateIssueOptions{ - Owner: "TEST", - Repository: "test", - Body: []byte(""), - Title: "This is my title", - Assignees: []string{"userIdOne", "userIdTwo"}, - UpdateExisting: true, - } - - // test - _, err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock) - - // assert - assert.NoError(t, err) - assert.NotNil(t, ghSearchIssuesMock.issuesSearchResult) - assert.NotNil(t, ghCreateCommentMock.issueComment) - assert.Equal(t, config.Title, ghSearchIssuesMock.issueTitle) - assert.Equal(t, config.Title, *ghSearchIssuesMock.issuesSearchResult.Issues[0].Title) - assert.Equal(t, "", ghCreateCommentMock.issueComment.GetBody()) - }) - - t.Run("Create error", func(t *testing.T) { - // init - ghCreateIssueService := ghCreateIssueMock{ - issueError: fmt.Errorf("error creating issue"), - } - config := CreateIssueOptions{ - Body: []byte("test content"), - } - - // test - _, err := createIssueLocal(ctx, &config, &ghCreateIssueService, nil, nil) - - // assert - assert.EqualError(t, err, "error occurred when creating issue: error creating issue") - }) } diff --git a/pkg/orchestrator/gitHubActions.go b/pkg/orchestrator/gitHubActions.go index 65d01b7e9e..9847fbacce 100644 --- a/pkg/orchestrator/gitHubActions.go +++ b/pkg/orchestrator/gitHubActions.go @@ -2,21 +2,27 @@ package orchestrator import ( "bytes" + "context" "fmt" "io" - "net/http" + "strconv" "strings" "sync" "time" - piperHttp "github.com/SAP/jenkins-library/pkg/http" + piperGithub "github.com/SAP/jenkins-library/pkg/github" "github.com/SAP/jenkins-library/pkg/log" - + "github.com/SAP/jenkins-library/pkg/piperutils" + "github.com/google/go-github/v45/github" + "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) type GitHubActionsConfigProvider struct { - client piperHttp.Client + client *github.Client + ctx context.Context + owner string + repo string runData run jobs []job jobsFetched bool @@ -30,7 +36,7 @@ type run struct { } type job struct { - ID int `json:"id"` + ID int64 `json:"id"` Name string `json:"name"` HtmlURL string `json:"html_url"` } @@ -40,18 +46,16 @@ type fullLog struct { b [][]byte } -var httpHeaders = http.Header{ - "Accept": {"application/vnd.github+json"}, - "X-GitHub-Api-Version": {"2022-11-28"}, -} - // InitOrchestratorProvider initializes http client for GitHubActionsDevopsConfigProvider func (g *GitHubActionsConfigProvider) InitOrchestratorProvider(settings *OrchestratorSettings) { - g.client.SetOptions(piperHttp.ClientOptions{ - Token: "Bearer " + settings.GitHubToken, - MaxRetries: 3, - TransportTimeout: time.Second * 10, - }) + var err error + g.ctx, g.client, err = piperGithub.NewClientBuilder(settings.GitHubToken, getEnv("GITHUB_API_URL", "")).Build() + if err != nil { + log.Entry().Errorf("failed to create github client: %v", err) + return + } + + g.owner, g.repo = getOwnerAndRepoNames() log.Entry().Debug("Successfully initialized GitHubActions config provider") } @@ -94,15 +98,15 @@ func (g *GitHubActionsConfigProvider) GetLog() ([]byte, error) { for i := range jobs { i := i // https://golang.org/doc/faq#closures_and_goroutines wg.Go(func() error { - resp, err := g.client.GetRequest(fmt.Sprintf("%s/jobs/%d/logs", actionsURL(), jobs[i].ID), httpHeaders, nil) + _, resp, err := g.client.Actions.GetWorkflowJobLogs(g.ctx, g.owner, g.repo, jobs[i].ID, true) if err != nil { - return fmt.Errorf("failed to get API data: %w", err) + return errors.Wrap(err, "fetching job logs failed") } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("failed to read response body: %w", err) + return errors.Wrap(err, "failed to read response body") } fullLogs.Lock() @@ -113,7 +117,7 @@ func (g *GitHubActionsConfigProvider) GetLog() ([]byte, error) { }) } if err := wg.Wait(); err != nil { - return nil, fmt.Errorf("failed to get logs: %w", err) + return nil, errors.Wrap(err, "failed to fetch all logs") } return bytes.Join(fullLogs.b, []byte("")), nil @@ -232,49 +236,65 @@ func (g *GitHubActionsConfigProvider) fetchRunData() { return } - url := fmt.Sprintf("%s/runs/%s", actionsURL(), getEnv("GITHUB_RUN_ID", "")) - resp, err := g.client.GetRequest(url, httpHeaders, nil) + runId, err := g.runIdInt64() + if err != nil { + log.Entry().Errorf("fetchRunData: %s", err) + } + + runData, resp, err := g.client.Actions.GetWorkflowRunByID(g.ctx, g.owner, g.repo, runId) if err != nil || resp.StatusCode != 200 { log.Entry().Errorf("failed to get API data: %s", err) return } - err = piperHttp.ParseHTTPResponseBodyJSON(resp, &g.runData) - if err != nil { - log.Entry().Errorf("failed to parse JSON data: %s", err) - return - } + g.runData = convertRunData(runData) g.runData.fetched = true } +func convertRunData(runData *github.WorkflowRun) run { + startedAtTs := piperutils.SafeDereference(runData.RunStartedAt) + return run{ + Status: piperutils.SafeDereference(runData.Status), + StartedAt: startedAtTs.Time, + } +} + func (g *GitHubActionsConfigProvider) fetchJobs() error { if g.jobsFetched { return nil } - url := fmt.Sprintf("%s/runs/%s/jobs", actionsURL(), g.GetBuildID()) - resp, err := g.client.GetRequest(url, httpHeaders, nil) + runId, err := g.runIdInt64() if err != nil { - return fmt.Errorf("failed to get API data: %w", err) + return err } - var result struct { - Jobs []job `json:"jobs"` - } - err = piperHttp.ParseHTTPResponseBodyJSON(resp, &result) - if err != nil { - return fmt.Errorf("failed to parse JSON data: %w", err) + jobs, resp, err := g.client.Actions.ListWorkflowJobs(g.ctx, g.owner, g.repo, runId, nil) + if err != nil || resp.StatusCode != 200 { + return errors.Wrap(err, "failed to get API data") } - - if len(result.Jobs) == 0 { + if len(jobs.Jobs) == 0 { return fmt.Errorf("no jobs found in response") } - g.jobs = result.Jobs + + g.jobs = convertJobs(jobs.Jobs) g.jobsFetched = true return nil } +func convertJobs(jobs []*github.WorkflowJob) []job { + result := make([]job, 0, len(jobs)) + for _, j := range jobs { + result = append(result, job{ + ID: j.GetID(), + Name: j.GetName(), + HtmlURL: j.GetHTMLURL(), + }) + } + return result +} + func (g *GitHubActionsConfigProvider) guessCurrentJob() { // check if the current job has already been guessed if g.currentJob.ID != 0 { @@ -300,3 +320,24 @@ func (g *GitHubActionsConfigProvider) guessCurrentJob() { } } } + +func (g *GitHubActionsConfigProvider) runIdInt64() (int64, error) { + strRunId := g.GetBuildID() + runId, err := strconv.ParseInt(strRunId, 10, 64) + if err != nil { + return 0, errors.Wrapf(err, "invalid GITHUB_RUN_ID value %s: %s", strRunId, err) + } + + return runId, nil +} + +func getOwnerAndRepoNames() (string, string) { + ownerAndRepo := getEnv("GITHUB_REPOSITORY", "") + s := strings.Split(ownerAndRepo, "/") + if len(s) != 2 { + log.Entry().Errorf("unable to determine owner and repo: invalid value of GITHUB_REPOSITORY envvar: %s", ownerAndRepo) + return "", "" + } + + return s[0], s[1] +} diff --git a/pkg/orchestrator/gitHubActions_test.go b/pkg/orchestrator/gitHubActions_test.go index 93a124bff6..c8aa8e632e 100644 --- a/pkg/orchestrator/gitHubActions_test.go +++ b/pkg/orchestrator/gitHubActions_test.go @@ -11,8 +11,7 @@ import ( "testing" "time" - piperHttp "github.com/SAP/jenkins-library/pkg/http" - + "github.com/google/go-github/v45/github" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) @@ -163,12 +162,18 @@ func TestGitHubActionsConfigProvider_fetchRunData(t *testing.T) { StartedAt: startedAt, } + // setup env vars + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("GITHUB_API_URL", "https://api.github.com") + _ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library") + _ = os.Setenv("GITHUB_RUN_ID", "11111") + // setup provider g := &GitHubActionsConfigProvider{} - g.client.SetOptions(piperHttp.ClientOptions{ - UseDefaultTransport: true, // need to use default transport for http mock - MaxRetries: -1, - }) + g.InitOrchestratorProvider(&OrchestratorSettings{}) + g.client = github.NewClient(http.DefaultClient) + // setup http mock httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -177,12 +182,6 @@ func TestGitHubActionsConfigProvider_fetchRunData(t *testing.T) { return httpmock.NewJsonResponse(200, respJson) }, ) - // setup env vars - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("GITHUB_API_URL", "https://api.github.com") - _ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library") - _ = os.Setenv("GITHUB_RUN_ID", "11111") // run g.fetchRunData() @@ -219,12 +218,18 @@ func TestGitHubActionsConfigProvider_fetchJobs(t *testing.T) { HtmlURL: "https://github.com/SAP/jenkins-library/actions/runs/11111/jobs/333", }} + // setup env vars + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("GITHUB_API_URL", "https://api.github.com") + _ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library") + _ = os.Setenv("GITHUB_RUN_ID", "11111") + // setup provider g := &GitHubActionsConfigProvider{} - g.client.SetOptions(piperHttp.ClientOptions{ - UseDefaultTransport: true, // need to use default transport for http mock - MaxRetries: -1, - }) + g.InitOrchestratorProvider(&OrchestratorSettings{}) + g.client = github.NewClient(http.DefaultClient) + // setup http mock httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -235,12 +240,6 @@ func TestGitHubActionsConfigProvider_fetchJobs(t *testing.T) { return httpmock.NewJsonResponse(200, respJson) }, ) - // setup env vars - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("GITHUB_API_URL", "https://api.github.com") - _ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library") - _ = os.Setenv("GITHUB_RUN_ID", "11111") // run err := g.fetchJobs() @@ -262,16 +261,20 @@ func TestGitHubActionsConfigProvider_GetLog(t *testing.T) { {ID: 111}, {ID: 222}, {ID: 333}, {ID: 444}, {ID: 555}, } + // setup env vars + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("GITHUB_API_URL", "https://api.github.com") + _ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library") + // setup provider g := &GitHubActionsConfigProvider{ - client: piperHttp.Client{}, jobs: jobs, jobsFetched: true, } - g.client.SetOptions(piperHttp.ClientOptions{ - UseDefaultTransport: true, // need to use default transport for http mock - MaxRetries: -1, - }) + g.InitOrchestratorProvider(&OrchestratorSettings{}) + g.client = github.NewClient(http.DefaultClient) + // setup http mock rand.Seed(time.Now().UnixNano()) latencyMin, latencyMax := 15, 500 // milliseconds @@ -282,6 +285,18 @@ func TestGitHubActionsConfigProvider_GetLog(t *testing.T) { httpmock.RegisterResponder( http.MethodGet, fmt.Sprintf("https://api.github.com/repos/SAP/jenkins-library/actions/jobs/%d/logs", j.ID), + func(jobId int64) func(req *http.Request) (*http.Response, error) { + return func(req *http.Request) (*http.Response, error) { + resp := httpmock.NewStringResponse(http.StatusFound, respLogs[idx]) + logsDownloadUrl := fmt.Sprintf("https://api.github.com/repos/SAP/jenkins-library/actions/jobs/%d/logs/download", jobId) + resp.Header.Set("Location", logsDownloadUrl) + return resp, nil + } + }(j.ID), + ) + httpmock.RegisterResponder( + http.MethodGet, + fmt.Sprintf("https://api.github.com/repos/SAP/jenkins-library/actions/jobs/%d/logs/download", j.ID), func(req *http.Request) (*http.Response, error) { // simulate response delay latency := rand.Intn(latencyMax-latencyMin) + latencyMin @@ -290,12 +305,6 @@ func TestGitHubActionsConfigProvider_GetLog(t *testing.T) { }, ) } - // setup env vars - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("GITHUB_API_URL", "https://api.github.com") - _ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library") - // run logs, err := g.GetLog() assert.NoError(t, err) diff --git a/pkg/orchestrator/orchestrator.go b/pkg/orchestrator/orchestrator.go index 345645cc5b..20fcd51da7 100644 --- a/pkg/orchestrator/orchestrator.go +++ b/pkg/orchestrator/orchestrator.go @@ -78,7 +78,12 @@ func NewOrchestratorSpecificConfigProvider() (OrchestratorSpecificConfigProvidin case AzureDevOps: return &AzureDevOpsConfigProvider{}, nil case GitHubActions: - return &GitHubActionsConfigProvider{}, nil + ghProvider := &GitHubActionsConfigProvider{} + // Temporary workaround: The orchestrator provider is not always initialized after being created, + // which causes a panic in some places for GitHub Actions provider, as it needs to initialize + // github sdk client. + ghProvider.InitOrchestratorProvider(&OrchestratorSettings{}) + return ghProvider, nil case Jenkins: return &JenkinsConfigProvider{}, nil default: diff --git a/pkg/piperutils/pointer.go b/pkg/piperutils/pointer.go new file mode 100644 index 0000000000..1e1bacc85e --- /dev/null +++ b/pkg/piperutils/pointer.go @@ -0,0 +1,10 @@ +package piperutils + +func SafeDereference[T any](p *T) T { + if p == nil { + var zeroValue T + return zeroValue + } + + return *p +} diff --git a/pkg/piperutils/pointer_test.go b/pkg/piperutils/pointer_test.go new file mode 100644 index 0000000000..9e37627c1e --- /dev/null +++ b/pkg/piperutils/pointer_test.go @@ -0,0 +1,62 @@ +//go:build unit +// +build unit + +package piperutils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSafeDereferenceString(t *testing.T) { + type testCase[T any] struct { + name string + p *T + want T + } + str := "test" + tests := []testCase[string]{ + { + name: "nil", + p: nil, + want: "", + }, + { + name: "non-nil", + p: &str, + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, SafeDereference(tt.p), "SafeDereference(%v)", tt.p) + }) + } +} + +func TestSafeDereferenceInt64(t *testing.T) { + type testCase[T any] struct { + name string + p *T + want T + } + i64 := int64(111) + tests := []testCase[int64]{ + { + name: "nil", + p: nil, + want: 0, + }, + { + name: "non-nil", + p: &i64, + want: 111, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, SafeDereference(tt.p), "SafeDereference(%v)", tt.p) + }) + } +}