diff --git a/cmd/githubCreateIssue.go b/cmd/githubCreateIssue.go new file mode 100644 index 000000000..db70f1095 --- /dev/null +++ b/cmd/githubCreateIssue.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "context" + + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/google/go-github/v32/github" + "github.com/pkg/errors" + + piperGithub "github.com/SAP/jenkins-library/pkg/github" +) + +type githubCreateIssueService interface { + Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error) +} + +func githubCreateIssue(config githubCreateIssueOptions, telemetryData *telemetry.CustomData) { + ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, "") + if err != nil { + log.Entry().WithError(err).Fatal("Failed to get GitHub client") + } + err = runGithubCreateIssue(ctx, &config, telemetryData, client.Issues) + if err != nil { + log.Entry().WithError(err).Fatal("Failed to comment on issue") + } +} + +func runGithubCreateIssue(ctx context.Context, config *githubCreateIssueOptions, _ *telemetry.CustomData, ghCreateIssueService githubCreateIssueService) error { + issue := github.IssueRequest{ + Body: &config.Body, + Title: &config.Title, + } + + newIssue, resp, err := ghCreateIssueService.Create(ctx, config.Owner, config.Repository, &issue) + if err != nil { + log.Entry().Errorf("GitHub response code %v", resp.Status) + return errors.Wrap(err, "Error occurred when creating issue") + } + log.Entry().Debugf("New issue created: %v", newIssue) + + return nil +} diff --git a/cmd/githubCreateIssue_generated.go b/cmd/githubCreateIssue_generated.go new file mode 100644 index 000000000..26ad5b606 --- /dev/null +++ b/cmd/githubCreateIssue_generated.go @@ -0,0 +1,185 @@ +// Code generated by piper's step-generator. DO NOT EDIT. + +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/SAP/jenkins-library/pkg/config" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/spf13/cobra" +) + +type githubCreateIssueOptions struct { + APIURL string `json:"apiUrl,omitempty"` + Body string `json:"body,omitempty"` + Owner string `json:"owner,omitempty"` + Repository string `json:"repository,omitempty"` + Title string `json:"title,omitempty"` + Token string `json:"token,omitempty"` +} + +// GithubCreateIssueCommand Create a new GitHub issue. +func GithubCreateIssueCommand() *cobra.Command { + const STEP_NAME = "githubCreateIssue" + + metadata := githubCreateIssueMetadata() + var stepConfig githubCreateIssueOptions + var startTime time.Time + + var createGithubCreateIssueCmd = &cobra.Command{ + Use: STEP_NAME, + Short: "Create a new GitHub issue.", + Long: `This step allows you to create a new GitHub issue. + +You will be able to use this step for example for regular jobs to report into your repository in case of new security findings.`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + startTime = time.Now() + log.SetStepName(STEP_NAME) + log.SetVerbose(GeneralConfig.Verbose) + + path, _ := os.Getwd() + fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} + log.RegisterHook(fatalHook) + + err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + log.RegisterSecret(stepConfig.Token) + + if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { + sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) + log.RegisterHook(&sentryHook) + } + + return nil + }, + Run: func(_ *cobra.Command, _ []string) { + telemetryData := telemetry.CustomData{} + telemetryData.ErrorCode = "1" + handler := func() { + config.RemoveVaultSecretFiles() + telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) + telemetryData.ErrorCategory = log.GetErrorCategory().String() + telemetry.Send(&telemetryData) + } + log.DeferExitHandler(handler) + defer handler() + telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) + githubCreateIssue(stepConfig, &telemetryData) + telemetryData.ErrorCode = "0" + log.Entry().Info("SUCCESS") + }, + } + + addGithubCreateIssueFlags(createGithubCreateIssueCmd, &stepConfig) + return createGithubCreateIssueCmd +} + +func addGithubCreateIssueFlags(cmd *cobra.Command, stepConfig *githubCreateIssueOptions) { + cmd.Flags().StringVar(&stepConfig.APIURL, "apiUrl", `https://api.github.com`, "Set the GitHub API url.") + 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.Owner, "owner", os.Getenv("PIPER_owner"), "Name of the GitHub organization.") + cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "Name of the GitHub repository.") + cmd.Flags().StringVar(&stepConfig.Title, "title", os.Getenv("PIPER_title"), "Defines the title for the Issue.") + cmd.Flags().StringVar(&stepConfig.Token, "token", os.Getenv("PIPER_token"), "GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line.") + + cmd.MarkFlagRequired("apiUrl") + cmd.MarkFlagRequired("body") + cmd.MarkFlagRequired("owner") + cmd.MarkFlagRequired("repository") + cmd.MarkFlagRequired("title") + cmd.MarkFlagRequired("token") +} + +// retrieve step metadata +func githubCreateIssueMetadata() config.StepData { + var theMetaData = config.StepData{ + Metadata: config.StepMetadata{ + Name: "githubCreateIssue", + Aliases: []config.Alias{}, + Description: "Create a new GitHub issue.", + }, + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Parameters: []config.StepParameters{ + { + Name: "apiUrl", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubApiUrl"}}, + }, + { + Name: "body", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "owner", + ResourceRef: []config.ResourceReference{ + { + Name: "commonPipelineEnvironment", + Param: "github/owner", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubOrg"}}, + }, + { + Name: "repository", + ResourceRef: []config.ResourceReference{ + { + Name: "commonPipelineEnvironment", + Param: "github/repository", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubRepo"}}, + }, + { + Name: "title", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "token", + ResourceRef: []config.ResourceReference{ + { + Name: "githubTokenCredentialsId", + Type: "secret", + }, + + { + Name: "", + Paths: []string{"$(vaultPath)/github", "$(vaultBasePath)/$(vaultPipelineName)/github", "$(vaultBasePath)/GROUP-SECRETS/github"}, + Type: "vaultSecret", + }, + }, + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "githubToken"}, {Name: "access_token"}}, + }, + }, + }, + }, + } + return theMetaData +} diff --git a/cmd/githubCreateIssue_generated_test.go b/cmd/githubCreateIssue_generated_test.go new file mode 100644 index 000000000..6b5388db5 --- /dev/null +++ b/cmd/githubCreateIssue_generated_test.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGithubCreateIssueCommand(t *testing.T) { + + testCmd := GithubCreateIssueCommand() + + // only high level testing performed - details are tested in step generation procedure + assert.Equal(t, "githubCreateIssue", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/githubCreateIssue_test.go b/cmd/githubCreateIssue_test.go new file mode 100644 index 000000000..e924bbf00 --- /dev/null +++ b/cmd/githubCreateIssue_test.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/google/go-github/v32/github" + "github.com/stretchr/testify/assert" +) + +type ghCreateIssueMock struct { + issue *github.IssueRequest + issueID int64 + issueError error + owner string + repo string + number int +} + +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 + + 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 +} + +func TestRunGithubCreateIssue(t *testing.T) { + ctx := context.Background() + t.Parallel() + + t.Run("Success", func(t *testing.T) { + // init + ghCreateIssueService := ghCreateIssueMock{ + issueID: 1, + } + config := githubCreateIssueOptions{ + Owner: "TEST", + Repository: "test", + Body: "This is my test body", + Title: "This is my title", + } + + // test + err := runGithubCreateIssue(ctx, &config, nil, &ghCreateIssueService) + + // assert + assert.NoError(t, err) + assert.Equal(t, config.Owner, ghCreateIssueService.owner) + assert.Equal(t, config.Repository, ghCreateIssueService.repo) + assert.Equal(t, config.Body, ghCreateIssueService.issue.GetBody()) + assert.Equal(t, config.Title, ghCreateIssueService.issue.GetTitle()) + }) + + t.Run("Error", func(t *testing.T) { + // init + ghCreateIssueService := ghCreateIssueMock{ + issueError: fmt.Errorf("error creating issue"), + } + config := githubCreateIssueOptions{} + + // test + err := runGithubCreateIssue(ctx, &config, nil, &ghCreateIssueService) + + // assert + assert.EqualError(t, err, "Error occurred when creating issue: error creating issue") + }) +} diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go index 841560435..60de5b4da 100644 --- a/cmd/metadata_generated.go +++ b/cmd/metadata_generated.go @@ -36,6 +36,7 @@ func GetAllStepMetadata() map[string]config.StepData { "gctsRollback": gctsRollbackMetadata(), "githubCheckBranchProtection": githubCheckBranchProtectionMetadata(), "githubCommentIssue": githubCommentIssueMetadata(), + "githubCreateIssue": githubCreateIssueMetadata(), "githubCreatePullRequest": githubCreatePullRequestMetadata(), "githubPublishRelease": githubPublishReleaseMetadata(), "githubSetCommitStatus": githubSetCommitStatusMetadata(), diff --git a/cmd/piper.go b/cmd/piper.go index 222b8d7bc..1d1d13fc6 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -75,6 +75,7 @@ func Execute() { rootCmd.AddCommand(XsDeployCommand()) rootCmd.AddCommand(GithubCheckBranchProtectionCommand()) rootCmd.AddCommand(GithubCommentIssueCommand()) + rootCmd.AddCommand(GithubCreateIssueCommand()) rootCmd.AddCommand(GithubCreatePullRequestCommand()) rootCmd.AddCommand(GithubPublishReleaseCommand()) rootCmd.AddCommand(GithubSetCommitStatusCommand()) diff --git a/documentation/docs/steps/githubCreateIssue.md b/documentation/docs/steps/githubCreateIssue.md new file mode 100644 index 000000000..1e548d23b --- /dev/null +++ b/documentation/docs/steps/githubCreateIssue.md @@ -0,0 +1,13 @@ +# ${docGenStepName} + +## Prerequisites + +You need to create a personal access token within GitHub and add this to the Jenkins credentials store. + +Please see [GitHub documentation for details about creating the personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/). + +## ${docGenParameters} + +## ${docGenConfiguration} + +## ${docGenDescription} diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index c4979df98..0e29e3d66 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -92,6 +92,7 @@ nav: - gctsRollback: steps/gctsRollback.md - githubCheckBranchProtection: steps/githubCheckBranchProtection.md - githubCommentIssue: steps/githubCommentIssue.md + - githubCreateIssue: steps/githubCreateIssue.md - githubCreatePullRequest: steps/githubCreatePullRequest.md - githubPublishRelease: steps/githubPublishRelease.md - githubSetCommitStatus: steps/githubSetCommitStatus.md diff --git a/resources/metadata/githubcreateissue.yaml b/resources/metadata/githubcreateissue.yaml new file mode 100644 index 000000000..7a8e17288 --- /dev/null +++ b/resources/metadata/githubcreateissue.yaml @@ -0,0 +1,89 @@ +metadata: + name: githubCreateIssue + description: Create a new GitHub issue. + longDescription: | + This step allows you to create a new GitHub issue. + + You will be able to use this step for example for regular jobs to report into your repository in case of new security findings. +spec: + inputs: + secrets: + - name: githubTokenCredentialsId + description: Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub. + type: jenkins + params: + - name: apiUrl + aliases: + - name: githubApiUrl + description: Set the GitHub API url. + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + default: https://api.github.com + mandatory: true + - name: body + description: Defines the content of the issue, e.g. using markdown syntax. + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: owner + aliases: + - name: githubOrg + description: Name of the GitHub organization. + resourceRef: + - name: commonPipelineEnvironment + param: github/owner + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: repository + aliases: + - name: githubRepo + description: Name of the GitHub repository. + resourceRef: + - name: commonPipelineEnvironment + param: github/repository + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: title + description: Defines the title for the Issue. + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: token + aliases: + - name: githubToken + - name: access_token + description: GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line. + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + secret: true + resourceRef: + - name: githubTokenCredentialsId + type: secret + - type: vaultSecret + paths: + - $(vaultPath)/github + - $(vaultBasePath)/$(vaultPipelineName)/github + - $(vaultBasePath)/GROUP-SECRETS/github diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy index 19b27355e..28d3c7b31 100644 --- a/test/groovy/CommonStepsTest.groovy +++ b/test/groovy/CommonStepsTest.groovy @@ -139,6 +139,7 @@ public class CommonStepsTest extends BasePiperTest{ 'buildSetResult', 'runClosures', 'checkmarxExecuteScan', //implementing new golang pattern without fields + 'githubCreateIssue', //implementing new golang pattern without fields 'githubPublishRelease', //implementing new golang pattern without fields 'githubCheckBranchProtection', //implementing new golang pattern without fields 'githubCommentIssue', //implementing new golang pattern without fields diff --git a/vars/githubCreateIssue.groovy b/vars/githubCreateIssue.groovy new file mode 100644 index 000000000..d0add2106 --- /dev/null +++ b/vars/githubCreateIssue.groovy @@ -0,0 +1,11 @@ +import groovy.transform.Field + +@Field String STEP_NAME = getClass().getName() +@Field String METADATA_FILE = 'metadata/githubcreateissue.yaml' + +void call(Map parameters = [:]) { + List credentials = [ + [type: 'token', id: 'githubTokenCredentialsId', env: ['PIPER_token']] + ] + piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials) +}