1
0
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:
Googlom 2023-09-20 14:38:45 +05:00 committed by GitHub
parent 1e993263e6
commit 3744787348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 669 additions and 424 deletions

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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 {

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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.")
}

View File

@ -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")
}

View File

@ -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

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
View 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
}

View 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")
})
}

View File

@ -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
}

View File

@ -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")
})
}

View File

@ -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]
}

View File

@ -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)

View File

@ -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
View File

@ -0,0 +1,10 @@
package piperutils
func SafeDereference[T any](p *T) T {
if p == nil {
var zeroValue T
return zeroValue
}
return *p
}

View 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)
})
}
}