mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-11-28 08:49:44 +02:00
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 <gulomjon.alimov@sap.com> Co-authored-by: Jordi van Liempt <35920075+jliempt@users.noreply.github.com>
This commit is contained in:
parent
1e993263e6
commit
3744787348
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
103
pkg/github/create_issue.go
Normal file
103
pkg/github/create_issue.go
Normal file
@ -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
|
||||
}
|
239
pkg/github/create_issue_test.go
Normal file
239
pkg/github/create_issue_test.go
Normal file
@ -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<Title>(.*))`)
|
||||
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")
|
||||
})
|
||||
}
|
@ -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 += "/"
|
||||
}
|
||||
baseURL, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return ctx, nil, err
|
||||
func NewClientBuilder(token, baseURL string) *ClientBuilder {
|
||||
if !strings.HasSuffix(baseURL, "/") {
|
||||
baseURL += "/"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
b.uploadURL = uploadURL
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *ClientBuilder) WithTimeout(timeout time.Duration) *ClientBuilder {
|
||||
b.timeout = timeout
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *ClientBuilder) WithMaxRetries(maxRetries int) *ClientBuilder {
|
||||
b.maxRetries = maxRetries
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *ClientBuilder) Build() (context.Context, *github.Client, error) {
|
||||
baseURL, err := url.Parse(b.baseURL)
|
||||
if err != nil {
|
||||
return ctx, nil, err
|
||||
return nil, nil, errors.Wrap(err, "failed to parse baseURL")
|
||||
}
|
||||
|
||||
client := github.NewClient(tc)
|
||||
uploadURL, err := url.Parse(b.uploadURL)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to parse uploadURL")
|
||||
}
|
||||
|
||||
if b.timeout == 0 {
|
||||
b.timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
if b.maxRetries == 0 {
|
||||
b.maxRetries = 5
|
||||
}
|
||||
|
||||
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 = uploadTargetURL
|
||||
client.UploadURL = uploadURL
|
||||
return ctx, client, nil
|
||||
}
|
||||
|
||||
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 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 = ""
|
||||
}
|
||||
issue.Body = &bodyString
|
||||
if len(ghCreateIssueOptions.Assignees) > 0 {
|
||||
issue.Assignees = &ghCreateIssueOptions.Assignees
|
||||
} else {
|
||||
issue.Assignees = &[]string{}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
return existingIssue, nil
|
||||
}
|
||||
|
@ -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,
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
@ -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"`
|
||||
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")
|
||||
}
|
||||
err = piperHttp.ParseHTTPResponseBodyJSON(resp, &result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse JSON data: %w", err)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
10
pkg/piperutils/pointer.go
Normal file
10
pkg/piperutils/pointer.go
Normal file
@ -0,0 +1,10 @@
|
||||
package piperutils
|
||||
|
||||
func SafeDereference[T any](p *T) T {
|
||||
if p == nil {
|
||||
var zeroValue T
|
||||
return zeroValue
|
||||
}
|
||||
|
||||
return *p
|
||||
}
|
62
pkg/piperutils/pointer_test.go
Normal file
62
pkg/piperutils/pointer_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user