1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-02-11 13:53:53 +02:00

githubCreateIssue_fix (#4151)

* extend githubCreateIssue to handle long body

Co-authored-by: Jordi van Liempt <35920075+jliempt@users.noreply.github.com>
This commit is contained in:
raman-susla-epam 2022-12-15 18:20:01 +03:00 committed by GitHub
parent c16fba873e
commit d7cf8654f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 226 additions and 65 deletions

View File

@ -77,7 +77,8 @@ func (c *checkmarxExecuteScanUtilsBundle) Open(name string) (*os.File, error) {
}
func (c *checkmarxExecuteScanUtilsBundle) CreateIssue(ghCreateIssueOptions *piperGithub.CreateIssueOptions) error {
return piperGithub.CreateIssue(ghCreateIssueOptions)
_, err := piperGithub.CreateIssue(ghCreateIssueOptions)
return err
}
func (c *checkmarxExecuteScanUtilsBundle) GetIssueService() *github.IssuesService {

View File

@ -78,7 +78,8 @@ func (f *fortifyUtilsBundle) GetArtifact(buildTool, buildDescriptorFile string,
}
func (f *fortifyUtilsBundle) CreateIssue(ghCreateIssueOptions *piperGithub.CreateIssueOptions) error {
return piperGithub.CreateIssue(ghCreateIssueOptions)
_, err := piperGithub.CreateIssue(ghCreateIssueOptions)
return err
}
func (f *fortifyUtilsBundle) GetIssueService() *github.IssuesService {

View File

@ -2,34 +2,71 @@ package cmd
import (
"fmt"
"io/ioutil"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/pkg/errors"
piperGithub "github.com/SAP/jenkins-library/pkg/github"
github "github.com/google/go-github/v45/github"
)
type githubCreateIssueUtils interface {
FileRead(string) ([]byte, error)
}
func githubCreateIssue(config githubCreateIssueOptions, telemetryData *telemetry.CustomData) {
err := runGithubCreateIssue(&config, telemetryData)
fileUtils := &piperutils.Files{}
options := piperGithub.CreateIssueOptions{}
err := runGithubCreateIssue(&config, telemetryData, &options, fileUtils, piperGithub.CreateIssue)
if err != nil {
log.Entry().WithError(err).Fatal("Failed to comment on issue")
}
}
func runGithubCreateIssue(config *githubCreateIssueOptions, _ *telemetry.CustomData) error {
options := piperGithub.CreateIssueOptions{}
err := transformConfig(config, &options, ioutil.ReadFile)
func runGithubCreateIssue(config *githubCreateIssueOptions, _ *telemetry.CustomData, options *piperGithub.CreateIssueOptions, utils githubCreateIssueUtils, createIssue func(*piperGithub.CreateIssueOptions) (*github.Issue, error)) error {
chunks, err := getBody(config, utils.FileRead)
if err != nil {
return err
}
return piperGithub.CreateIssue(&options)
transformConfig(config, options, chunks[0])
issue, err := createIssue(options)
if err != nil {
return err
}
if len(chunks) > 1 {
for _, v := range chunks[1:] {
options.Body = []byte(v)
options.Issue = issue
options.UpdateExisting = true
_, err = createIssue(options)
if err != nil {
return err
}
}
}
return nil
}
func transformConfig(config *githubCreateIssueOptions, options *piperGithub.CreateIssueOptions, readFile func(string) ([]byte, error)) error {
func getBody(config *githubCreateIssueOptions, readFile func(string) ([]byte, error)) ([]string, error) {
var bodyString []rune
if len(config.Body)+len(config.BodyFilePath) == 0 {
return nil, fmt.Errorf("either parameter `body` or parameter `bodyFilePath` is required")
}
if len(config.Body) == 0 {
issueContent, err := readFile(config.BodyFilePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to read file '%v'", config.BodyFilePath)
}
bodyString = []rune(string(issueContent))
} else {
bodyString = []rune(config.Body)
}
return getChunks(bodyString, config.ChunkSize), nil
}
func transformConfig(config *githubCreateIssueOptions, options *piperGithub.CreateIssueOptions, body string) {
options.Token = config.Token
options.APIURL = config.APIURL
options.Owner = config.Owner
@ -38,16 +75,21 @@ func transformConfig(config *githubCreateIssueOptions, options *piperGithub.Crea
options.Body = []byte(config.Body)
options.Assignees = config.Assignees
options.UpdateExisting = config.UpdateExisting
if len(config.Body)+len(config.BodyFilePath) == 0 {
return fmt.Errorf("either parameter `body` or parameter `bodyFilePath` is required")
}
if len(config.Body) == 0 {
issueContent, err := readFile(config.BodyFilePath)
if err != nil {
return errors.Wrapf(err, "failed to read file '%v'", config.BodyFilePath)
}
options.Body = issueContent
}
return nil
options.Body = []byte(body)
}
func getChunks(value []rune, chunkSize int) []string {
chunks := []string{}
length := len(value)
if length == 0 {
return []string{""}
}
for i := 0; i < length; i += chunkSize {
to := length
if to > i+chunkSize {
to = i + chunkSize
}
chunks = append(chunks, string(value[i:to]))
}
return chunks
}

View File

@ -18,6 +18,7 @@ import (
type githubCreateIssueOptions struct {
APIURL string `json:"apiUrl,omitempty"`
Assignees []string `json:"assignees,omitempty"`
ChunkSize int `json:"chunkSize,omitempty"`
Body string `json:"body,omitempty"`
BodyFilePath string `json:"bodyFilePath,omitempty"`
Owner string `json:"owner,omitempty"`
@ -125,6 +126,7 @@ You will be able to use this step for example for regular jobs to report into yo
func addGithubCreateIssueFlags(cmd *cobra.Command, stepConfig *githubCreateIssueOptions) {
cmd.Flags().StringVar(&stepConfig.APIURL, "apiUrl", `https://api.github.com`, "Set the GitHub API url.")
cmd.Flags().StringSliceVar(&stepConfig.Assignees, "assignees", []string{``}, "Defines the assignees for the Issue.")
cmd.Flags().IntVar(&stepConfig.ChunkSize, "chunkSize", 65500, "Defines size of the chunk. If content exceed chunk size it'll be sliced into chunks and stored in comments")
cmd.Flags().StringVar(&stepConfig.Body, "body", os.Getenv("PIPER_body"), "Defines the content of the issue, e.g. using markdown syntax.")
cmd.Flags().StringVar(&stepConfig.BodyFilePath, "bodyFilePath", os.Getenv("PIPER_bodyFilePath"), "Defines the path to a file containing the markdown content for the issue. This can be used instead of [`body`](#body)")
cmd.Flags().StringVar(&stepConfig.Owner, "owner", os.Getenv("PIPER_owner"), "Name of the GitHub organization.")
@ -172,6 +174,15 @@ func githubCreateIssueMetadata() config.StepData {
Aliases: []config.Alias{},
Default: []string{``},
},
{
Name: "chunkSize",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "int",
Mandatory: false,
Aliases: []config.Alias{},
Default: 65500,
},
{
Name: "body",
ResourceRef: []config.ResourceReference{},

View File

@ -3,13 +3,57 @@ package cmd
import (
"testing"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
piperGithub "github.com/SAP/jenkins-library/pkg/github"
"github.com/SAP/jenkins-library/pkg/mock"
github "github.com/google/go-github/v45/github"
"github.com/stretchr/testify/assert"
)
func TestGetChunk(t *testing.T) {
tests := []struct {
name string
chunkSize int
largeString string
expectedChunks []string
}{
{
name: "large string",
largeString: `The quick
brown fox jumps
over
the lazy dog
`,
chunkSize: 12,
expectedChunks: []string{"The quick\nbr", "own fox jump", "s\nover\nthe l", "azy dog\n"},
},
{
name: "small string",
largeString: `small`,
chunkSize: 12,
expectedChunks: []string{"small"},
},
{
name: "exact size",
largeString: `exact size12`,
chunkSize: 12,
expectedChunks: []string{"exact size12"},
},
{
name: "empty string",
largeString: ``,
chunkSize: 12,
expectedChunks: []string{""},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
chunks := getChunks([]rune(test.largeString), test.chunkSize)
assert.ElementsMatch(t, test.expectedChunks, chunks)
})
}
}
func TestTransformConfig(t *testing.T) {
t.Parallel()
@ -22,11 +66,17 @@ func TestTransformConfig(t *testing.T) {
Body: "This is my test body",
Title: "This is my title",
Assignees: []string{"userIdOne", "userIdTwo"},
ChunkSize: 100,
}
options := piperGithub.CreateIssueOptions{}
resultChunks := []string{}
createIssue := func(options *piperGithub.CreateIssueOptions) (*github.Issue, error) {
resultChunks = append(resultChunks, string(options.Body))
return nil, nil
}
// test
err := transformConfig(&config, &options, filesMock.FileRead)
err := runGithubCreateIssue(&config, nil, &options, &filesMock, createIssue)
// assert
assert.NoError(t, err)
@ -34,10 +84,10 @@ func TestTransformConfig(t *testing.T) {
assert.Equal(t, config.APIURL, options.APIURL)
assert.Equal(t, config.Owner, options.Owner)
assert.Equal(t, config.Repository, options.Repository)
assert.Equal(t, []byte(config.Body), options.Body)
assert.Equal(t, config.Title, options.Title)
assert.Equal(t, config.Assignees, options.Assignees)
assert.Equal(t, config.UpdateExisting, options.UpdateExisting)
assert.ElementsMatch(t, resultChunks, []string{string(config.Body)})
})
t.Run("Success bodyFilePath", func(t *testing.T) {
@ -50,11 +100,16 @@ func TestTransformConfig(t *testing.T) {
BodyFilePath: "test.md",
Title: "This is my title",
Assignees: []string{"userIdOne", "userIdTwo"},
ChunkSize: 100,
}
options := piperGithub.CreateIssueOptions{}
resultChunks := []string{}
createIssue := func(options *piperGithub.CreateIssueOptions) (*github.Issue, error) {
resultChunks = append(resultChunks, string(options.Body))
return nil, nil
}
// test
err := transformConfig(&config, &options, filesMock.FileRead)
err := runGithubCreateIssue(&config, nil, &options, &filesMock, createIssue)
// assert
assert.NoError(t, err)
@ -62,20 +117,24 @@ func TestTransformConfig(t *testing.T) {
assert.Equal(t, config.APIURL, options.APIURL)
assert.Equal(t, config.Owner, options.Owner)
assert.Equal(t, config.Repository, options.Repository)
assert.Equal(t, []byte("Test markdown"), options.Body)
assert.Equal(t, config.Title, options.Title)
assert.Equal(t, config.Assignees, options.Assignees)
assert.Equal(t, config.UpdateExisting, options.UpdateExisting)
assert.ElementsMatch(t, resultChunks, []string{"Test markdown"})
})
t.Run("Error - missing issue body", func(t *testing.T) {
// init
filesMock := mock.FilesMock{}
config := githubCreateIssueOptions{}
config := githubCreateIssueOptions{ChunkSize: 100}
options := piperGithub.CreateIssueOptions{}
resultChunks := []string{}
createIssue := func(options *piperGithub.CreateIssueOptions) (*github.Issue, error) {
resultChunks = append(resultChunks, string(options.Body))
return nil, nil
}
// test
err := transformConfig(&config, &options, filesMock.FileRead)
err := runGithubCreateIssue(&config, nil, &options, &filesMock, createIssue)
// assert
assert.EqualError(t, err, "either parameter `body` or parameter `bodyFilePath` is required")

View File

@ -27,15 +27,16 @@ type githubCreateCommentService interface {
// 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"`
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"`
}
// NewClient creates a new GitHub client using an OAuth token for authentication
@ -74,15 +75,15 @@ func NewClient(token, apiURL, uploadURL string, trustedCerts []string) (context.
return ctx, client, nil
}
func CreateIssue(ghCreateIssueOptions *CreateIssueOptions) error {
func CreateIssue(ghCreateIssueOptions *CreateIssueOptions) (*github.Issue, error) {
ctx, client, err := NewClient(ghCreateIssueOptions.Token, ghCreateIssueOptions.APIURL, "", ghCreateIssueOptions.TrustedCerts)
if err != nil {
return errors.Wrap(err, "failed to get GitHub client")
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) error {
func createIssueLocal(ctx context.Context, ghCreateIssueOptions *CreateIssueOptions, ghCreateIssueService githubCreateIssueService, ghSearchIssuesService githubSearchIssuesService, ghCreateCommentService githubCreateCommentService) (*github.Issue, error) {
issue := github.IssueRequest{
Title: &ghCreateIssueOptions.Title,
}
@ -102,17 +103,20 @@ func createIssueLocal(ctx context.Context, ghCreateIssueOptions *CreateIssueOpti
var existingIssue *github.Issue = nil
if ghCreateIssueOptions.UpdateExisting {
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 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
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
}
}
}
}
@ -124,7 +128,7 @@ func createIssueLocal(ctx context.Context, ghCreateIssueOptions *CreateIssueOpti
if resp != nil {
log.Entry().Errorf("GitHub create comment returned response code %v", resp.Status)
}
return errors.Wrap(err, "error occurred when adding comment to existing issue")
return nil, errors.Wrap(err, "error occurred when adding comment to existing issue")
}
}
}
@ -135,10 +139,11 @@ func createIssueLocal(ctx context.Context, ghCreateIssueOptions *CreateIssueOpti
if resp != nil {
log.Entry().Errorf("GitHub create issue returned response code %v", resp.Status)
}
return errors.Wrap(err, "error occurred when creating issue")
return nil, errors.Wrap(err, "error occurred when creating issue")
}
log.Entry().Debugf("New issue created: %v", newIssue)
existingIssue = newIssue
}
return nil
return existingIssue, nil
}

View File

@ -80,11 +80,13 @@ func (g *ghSearchIssuesMock) Issues(ctx context.Context, query string, opts *git
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"
@ -114,7 +116,7 @@ func TestRunGithubCreateIssue(t *testing.T) {
}
// test
err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock)
_, err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock)
// assert
assert.NoError(t, err)
@ -143,7 +145,7 @@ func TestRunGithubCreateIssue(t *testing.T) {
}
// test
err := createIssueLocal(ctx, &config, nil, &ghSearchIssuesMock, &ghCreateCommentMock)
_, err := createIssueLocal(ctx, &config, nil, &ghSearchIssuesMock, &ghCreateCommentMock)
// assert
assert.NoError(t, err)
@ -154,6 +156,38 @@ func TestRunGithubCreateIssue(t *testing.T) {
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{
@ -173,7 +207,7 @@ func TestRunGithubCreateIssue(t *testing.T) {
}
// test
err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock)
_, err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock)
// assert
assert.NoError(t, err)
@ -194,7 +228,7 @@ func TestRunGithubCreateIssue(t *testing.T) {
}
// test
err := createIssueLocal(ctx, &config, &ghCreateIssueService, nil, nil)
_, err := createIssueLocal(ctx, &config, &ghCreateIssueService, nil, nil)
// assert
assert.EqualError(t, err, "error occurred when creating issue: error creating issue")

View File

@ -33,6 +33,14 @@ spec:
type: "[]string"
default: []
mandatory: false
- name: chunkSize
description: Defines size of the chunk. If content exceed chunk size it'll be sliced into chunks and stored in comments
scope:
- PARAMETERS
- STAGES
- STEPS
type: int
default: 65500
- name: body
description: Defines the content of the issue, e.g. using markdown syntax.
scope: