diff --git a/cmd/githubPublishRelease.go b/cmd/githubPublishRelease.go new file mode 100644 index 000000000..a7f70e471 --- /dev/null +++ b/cmd/githubPublishRelease.go @@ -0,0 +1,217 @@ +package cmd + +import ( + "context" + "fmt" + "mime" + "os" + "path/filepath" + "strings" + + "github.com/SAP/jenkins-library/pkg/log" + "github.com/google/go-github/v28/github" + "github.com/pkg/errors" + + piperGithub "github.com/SAP/jenkins-library/pkg/github" +) + +type githubRepoClient interface { + CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error) + DeleteReleaseAsset(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) + GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) + ListReleaseAssets(ctx context.Context, owner string, repo string, id int64, opt *github.ListOptions) ([]*github.ReleaseAsset, *github.Response, error) + UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error) +} + +type githubIssueClient interface { + ListByRepo(ctx context.Context, owner string, repo string, opt *github.IssueListByRepoOptions) ([]*github.Issue, *github.Response, error) +} + +func githubPublishRelease(myGithubPublishReleaseOptions githubPublishReleaseOptions) error { + ctx, client, err := piperGithub.NewClient(myGithubPublishReleaseOptions.Token, myGithubPublishReleaseOptions.APIURL, myGithubPublishReleaseOptions.UploadURL) + if err != nil { + log.Entry().WithError(err).Fatal("Failed to get GitHub client.") + } + + err = runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, client.Repositories, client.Issues) + if err != nil { + log.Entry().WithError(err).Fatal("Failed to publish GitHub release.") + } + + return nil +} + +func runGithubPublishRelease(ctx context.Context, myGithubPublishReleaseOptions *githubPublishReleaseOptions, ghRepoClient githubRepoClient, ghIssueClient githubIssueClient) error { + + var publishedAt github.Timestamp + + lastRelease, resp, err := ghRepoClient.GetLatestRelease(ctx, myGithubPublishReleaseOptions.Owner, myGithubPublishReleaseOptions.Repository) + if err != nil { + if resp.StatusCode == 404 { + //no previous release found -> first release + myGithubPublishReleaseOptions.AddDeltaToLastRelease = false + log.Entry().Debug("This is the first release.") + } else { + return errors.Wrap(err, "Error occured when retrieving latest GitHub release.") + } + } + publishedAt = lastRelease.GetPublishedAt() + log.Entry().Debugf("Previous GitHub release published: '%v'", publishedAt) + + //updating assets only supported on latest release + if myGithubPublishReleaseOptions.UpdateAsset && myGithubPublishReleaseOptions.Version == "latest" { + return uploadReleaseAsset(ctx, lastRelease.GetID(), myGithubPublishReleaseOptions, ghRepoClient) + } + + releaseBody := "" + + if len(myGithubPublishReleaseOptions.ReleaseBodyHeader) > 0 { + releaseBody += myGithubPublishReleaseOptions.ReleaseBodyHeader + "\n" + } + + if myGithubPublishReleaseOptions.AddClosedIssues { + releaseBody += getClosedIssuesText(ctx, publishedAt, myGithubPublishReleaseOptions, ghIssueClient) + } + + if myGithubPublishReleaseOptions.AddDeltaToLastRelease { + releaseBody += getReleaseDeltaText(myGithubPublishReleaseOptions, lastRelease) + } + + release := github.RepositoryRelease{ + TagName: &myGithubPublishReleaseOptions.Version, + TargetCommitish: &myGithubPublishReleaseOptions.Commitish, + Name: &myGithubPublishReleaseOptions.Version, + Body: &releaseBody, + } + + createdRelease, _, err := ghRepoClient.CreateRelease(ctx, myGithubPublishReleaseOptions.Owner, myGithubPublishReleaseOptions.Repository, &release) + if err != nil { + return errors.Wrapf(err, "Creation of release '%v' failed", *release.TagName) + } + log.Entry().Infof("Release %v created on %v/%v", *createdRelease.TagName, myGithubPublishReleaseOptions.Owner, myGithubPublishReleaseOptions.Repository) + + if len(myGithubPublishReleaseOptions.AssetPath) > 0 { + return uploadReleaseAsset(ctx, createdRelease.GetID(), myGithubPublishReleaseOptions, ghRepoClient) + } + + return nil +} + +func getClosedIssuesText(ctx context.Context, publishedAt github.Timestamp, myGithubPublishReleaseOptions *githubPublishReleaseOptions, ghIssueClient githubIssueClient) string { + closedIssuesText := "" + + options := github.IssueListByRepoOptions{ + State: "closed", + Direction: "asc", + Since: publishedAt.Time, + } + if len(myGithubPublishReleaseOptions.Labels) > 0 { + options.Labels = myGithubPublishReleaseOptions.Labels + } + ghIssues, _, err := ghIssueClient.ListByRepo(ctx, myGithubPublishReleaseOptions.Owner, myGithubPublishReleaseOptions.Repository, &options) + if err != nil { + log.Entry().WithError(err).Error("Failed to get GitHub issues.") + } + + prTexts := []string{"**List of closed pull-requests since last release**"} + issueTexts := []string{"**List of closed issues since last release**"} + + for _, issue := range ghIssues { + if issue.IsPullRequest() && !isExcluded(issue, myGithubPublishReleaseOptions.ExcludeLabels) { + prTexts = append(prTexts, fmt.Sprintf("[#%v](%v): %v", issue.GetNumber(), issue.GetHTMLURL(), issue.GetTitle())) + log.Entry().Debugf("Added PR #%v to release", issue.GetNumber()) + } else if !issue.IsPullRequest() && !isExcluded(issue, myGithubPublishReleaseOptions.ExcludeLabels) { + issueTexts = append(issueTexts, fmt.Sprintf("[#%v](%v): %v", issue.GetNumber(), issue.GetHTMLURL(), issue.GetTitle())) + log.Entry().Debugf("Added Issue #%v to release", issue.GetNumber()) + } + } + + if len(prTexts) > 1 { + closedIssuesText += "\n" + strings.Join(prTexts, "\n") + "\n" + } + + if len(issueTexts) > 1 { + closedIssuesText += "\n" + strings.Join(issueTexts, "\n") + "\n" + } + return closedIssuesText +} + +func getReleaseDeltaText(myGithubPublishReleaseOptions *githubPublishReleaseOptions, lastRelease *github.RepositoryRelease) string { + releaseDeltaText := "" + + //add delta link to previous release + releaseDeltaText += "\n**Changes**\n" + releaseDeltaText += fmt.Sprintf( + "[%v...%v](%v/%v/%v/compare/%v...%v)\n", + lastRelease.GetTagName(), + myGithubPublishReleaseOptions.Version, + myGithubPublishReleaseOptions.ServerURL, + myGithubPublishReleaseOptions.Owner, + myGithubPublishReleaseOptions.Repository, + lastRelease.GetTagName(), myGithubPublishReleaseOptions.Version, + ) + + return releaseDeltaText +} + +func uploadReleaseAsset(ctx context.Context, releaseID int64, myGithubPublishReleaseOptions *githubPublishReleaseOptions, ghRepoClient githubRepoClient) error { + + assets, _, err := ghRepoClient.ListReleaseAssets(ctx, myGithubPublishReleaseOptions.Owner, myGithubPublishReleaseOptions.Repository, releaseID, &github.ListOptions{}) + if err != nil { + return errors.Wrap(err, "Failed to get list of release assets.") + } + var assetID int64 + for _, a := range assets { + if a.GetName() == filepath.Base(myGithubPublishReleaseOptions.AssetPath) { + assetID = a.GetID() + break + } + } + if assetID != 0 { + //asset needs to be deleted first since API does not allow for replacement + _, err := ghRepoClient.DeleteReleaseAsset(ctx, myGithubPublishReleaseOptions.Owner, myGithubPublishReleaseOptions.Repository, assetID) + if err != nil { + return errors.Wrap(err, "Failed to delete release asset.") + } + } + + mediaType := mime.TypeByExtension(filepath.Ext(myGithubPublishReleaseOptions.AssetPath)) + if mediaType == "" { + mediaType = "application/octet-stream" + } + log.Entry().Debugf("Using mediaType '%v'", mediaType) + + name := filepath.Base(myGithubPublishReleaseOptions.AssetPath) + log.Entry().Debugf("Using file name '%v'", name) + + opts := github.UploadOptions{ + Name: name, + MediaType: mediaType, + } + file, err := os.Open(myGithubPublishReleaseOptions.AssetPath) + defer file.Close() + if err != nil { + return errors.Wrapf(err, "Failed to load release asset '%v'", myGithubPublishReleaseOptions.AssetPath) + } + + log.Entry().Info("Starting to upload release asset.") + asset, _, err := ghRepoClient.UploadReleaseAsset(ctx, myGithubPublishReleaseOptions.Owner, myGithubPublishReleaseOptions.Repository, releaseID, &opts, file) + if err != nil { + return errors.Wrap(err, "Failed to upload release asset.") + } + log.Entry().Infof("Done uploading asset '%v'.", asset.GetURL()) + + return nil +} + +func isExcluded(issue *github.Issue, excludeLabels []string) bool { + //issue.Labels[0].GetName() + for _, ex := range excludeLabels { + for _, l := range issue.Labels { + if ex == l.GetName() { + return true + } + } + } + return false +} diff --git a/cmd/githubPublishRelease_generated.go b/cmd/githubPublishRelease_generated.go new file mode 100644 index 000000000..faddfaf2e --- /dev/null +++ b/cmd/githubPublishRelease_generated.go @@ -0,0 +1,189 @@ +package cmd + +import ( + "os" + + "github.com/SAP/jenkins-library/pkg/config" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/spf13/cobra" +) + +type githubPublishReleaseOptions struct { + AddClosedIssues bool `json:"addClosedIssues,omitempty"` + AddDeltaToLastRelease bool `json:"addDeltaToLastRelease,omitempty"` + AssetPath string `json:"assetPath,omitempty"` + Commitish string `json:"commitish,omitempty"` + ExcludeLabels []string `json:"excludeLabels,omitempty"` + APIURL string `json:"apiUrl,omitempty"` + Owner string `json:"owner,omitempty"` + Repository string `json:"repository,omitempty"` + ServerURL string `json:"serverUrl,omitempty"` + Token string `json:"token,omitempty"` + UploadURL string `json:"uploadUrl,omitempty"` + Labels []string `json:"labels,omitempty"` + ReleaseBodyHeader string `json:"releaseBodyHeader,omitempty"` + UpdateAsset bool `json:"updateAsset,omitempty"` + Version string `json:"version,omitempty"` +} + +var myGithubPublishReleaseOptions githubPublishReleaseOptions +var githubPublishReleaseStepConfigJSON string + +// GithubPublishReleaseCommand Publish a release in GitHub +func GithubPublishReleaseCommand() *cobra.Command { + metadata := githubPublishReleaseMetadata() + var createGithubPublishReleaseCmd = &cobra.Command{ + Use: "githubPublishRelease", + Short: "Publish a release in GitHub", + Long: `This step creates a tag in your GitHub repository together with a release. +The release can be filled with text plus additional information like: + +* Closed pull request since last release +* Closed issues since last release +* Link to delta information showing all commits since last release + +The result looks like + +![Example release](../images/githubRelease.png)`, + PreRunE: func(cmd *cobra.Command, args []string) error { + log.SetStepName("githubPublishRelease") + log.SetVerbose(generalConfig.verbose) + return PrepareConfig(cmd, &metadata, "githubPublishRelease", &myGithubPublishReleaseOptions, openPiperFile) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return githubPublishRelease(myGithubPublishReleaseOptions) + }, + } + + addGithubPublishReleaseFlags(createGithubPublishReleaseCmd) + return createGithubPublishReleaseCmd +} + +func addGithubPublishReleaseFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&myGithubPublishReleaseOptions.AddClosedIssues, "addClosedIssues", false, "If set to `true`, closed issues and merged pull-requests since the last release will added below the `releaseBodyHeader`") + cmd.Flags().BoolVar(&myGithubPublishReleaseOptions.AddDeltaToLastRelease, "addDeltaToLastRelease", false, "If set to `true`, a link will be added to the relese information that brings up all commits since the last release.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.AssetPath, "assetPath", os.Getenv("PIPER_assetPath"), "Path to a release asset which should be uploaded to the list of release assets.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.Commitish, "commitish", "master", "Target git commitish for the release") + cmd.Flags().StringSliceVar(&myGithubPublishReleaseOptions.ExcludeLabels, "excludeLabels", []string{}, "Allows to exclude issues with dedicated list of labels.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.APIURL, "apiUrl", "https://api.github.com", "Set the GitHub API url.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.Owner, "owner", os.Getenv("PIPER_owner"), "Set the GitHub organization.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.Repository, "repository", os.Getenv("PIPER_repository"), "Set the GitHub repository.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.ServerURL, "serverUrl", "https://github.com", "GitHub server url for end-user access.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.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.Flags().StringVar(&myGithubPublishReleaseOptions.UploadURL, "uploadUrl", "https://uploads.github.com", "Set the GitHub API url.") + cmd.Flags().StringSliceVar(&myGithubPublishReleaseOptions.Labels, "labels", []string{}, "Labels to include in issue search.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.ReleaseBodyHeader, "releaseBodyHeader", os.Getenv("PIPER_releaseBodyHeader"), "Content which will appear for the release.") + cmd.Flags().BoolVar(&myGithubPublishReleaseOptions.UpdateAsset, "updateAsset", false, "Specify if a release asset should be updated only.") + cmd.Flags().StringVar(&myGithubPublishReleaseOptions.Version, "version", os.Getenv("PIPER_version"), "Define the version number which will be written as tag as well as release name.") + + cmd.MarkFlagRequired("apiUrl") + cmd.MarkFlagRequired("owner") + cmd.MarkFlagRequired("repository") + cmd.MarkFlagRequired("serverUrl") + cmd.MarkFlagRequired("token") + cmd.MarkFlagRequired("uploadUrl") + cmd.MarkFlagRequired("version") +} + +// retrieve step metadata +func githubPublishReleaseMetadata() config.StepData { + var theMetaData = config.StepData{ + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Parameters: []config.StepParameters{ + { + Name: "addClosedIssues", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + }, + { + Name: "addDeltaToLastRelease", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + }, + { + Name: "assetPath", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + }, + { + Name: "commitish", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + }, + { + Name: "excludeLabels", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "[]string", + Mandatory: false, + }, + { + Name: "apiUrl", + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + }, + { + Name: "owner", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + }, + { + Name: "repository", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + }, + { + Name: "serverUrl", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + }, + { + Name: "token", + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + }, + { + Name: "uploadUrl", + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + }, + { + Name: "labels", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "[]string", + Mandatory: false, + }, + { + Name: "releaseBodyHeader", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + }, + { + Name: "updateAsset", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + }, + { + Name: "version", + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + }, + }, + }, + }, + } + return theMetaData +} diff --git a/cmd/githubPublishRelease_generated_test.go b/cmd/githubPublishRelease_generated_test.go new file mode 100644 index 000000000..a17000345 --- /dev/null +++ b/cmd/githubPublishRelease_generated_test.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGithubPublishReleaseCommand(t *testing.T) { + + testCmd := GithubPublishReleaseCommand() + + // only high level testing performed - details are tested in step generation procudure + assert.Equal(t, "githubPublishRelease", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/githubPublishRelease_test.go b/cmd/githubPublishRelease_test.go new file mode 100644 index 000000000..95aafc493 --- /dev/null +++ b/cmd/githubPublishRelease_test.go @@ -0,0 +1,383 @@ +package cmd + +import ( + "context" + "fmt" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/go-github/v28/github" + "github.com/stretchr/testify/assert" +) + +type ghRCMock struct { + createErr error + latestRelease *github.RepositoryRelease + release *github.RepositoryRelease + delErr error + delID int64 + delOwner string + delRepo string + listErr error + listID int64 + listOwner string + listReleaseAssets []*github.ReleaseAsset + listRepo string + listOpts *github.ListOptions + latestStatusCode int + latestErr error + uploadID int64 + uploadOpts *github.UploadOptions + uploadOwner string + uploadRepo string +} + +func (g *ghRCMock) CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error) { + g.release = release + return release, nil, g.createErr +} + +func (g *ghRCMock) DeleteReleaseAsset(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) { + g.delOwner = owner + g.delRepo = repo + g.delID = id + return nil, g.delErr +} + +func (g *ghRCMock) GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) { + hc := http.Response{StatusCode: 200} + if g.latestStatusCode != 0 { + hc.StatusCode = g.latestStatusCode + } + ghResp := github.Response{Response: &hc} + return g.latestRelease, &ghResp, g.latestErr +} + +func (g *ghRCMock) ListReleaseAssets(ctx context.Context, owner string, repo string, id int64, opt *github.ListOptions) ([]*github.ReleaseAsset, *github.Response, error) { + g.listID = id + g.listOwner = owner + g.listRepo = repo + g.listOpts = opt + return g.listReleaseAssets, nil, g.listErr +} + +func (g *ghRCMock) UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error) { + g.uploadID = id + g.uploadOwner = owner + g.uploadRepo = repo + g.uploadOpts = opt + return nil, nil, nil +} + +type ghICMock struct { + issues []*github.Issue + lastPublished time.Time + owner string + repo string + options *github.IssueListByRepoOptions +} + +func (g *ghICMock) ListByRepo(ctx context.Context, owner string, repo string, opt *github.IssueListByRepoOptions) ([]*github.Issue, *github.Response, error) { + g.owner = owner + g.repo = repo + g.options = opt + g.lastPublished = opt.Since + return g.issues, nil, nil +} + +func TestRunGithubPublishRelease(t *testing.T) { + ctx := context.Background() + + t.Run("Success - first release & no body", func(t *testing.T) { + ghIssueClient := ghICMock{} + ghRepoClient := ghRCMock{ + latestStatusCode: 404, + latestErr: fmt.Errorf("not found"), + } + + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + AddDeltaToLastRelease: true, + Commitish: "master", + Owner: "TEST", + Repository: "test", + ServerURL: "https://github.com", + ReleaseBodyHeader: "Header", + Version: "1.0", + } + err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient) + assert.NoError(t, err, "Error occured but none expected.") + + assert.Equal(t, "Header\n", ghRepoClient.release.GetBody()) + }) + + t.Run("Success - subsequent releases & with body", func(t *testing.T) { + lastTag := "1.0" + lastPublishedAt := github.Timestamp{Time: time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC)} + ghRepoClient := ghRCMock{ + createErr: nil, + latestRelease: &github.RepositoryRelease{ + TagName: &lastTag, + PublishedAt: &lastPublishedAt, + }, + } + prHTMLURL := "https://github.com/TEST/test/pull/1" + prTitle := "Pull" + prNo := 1 + + issHTMLURL := "https://github.com/TEST/test/issues/2" + issTitle := "Issue" + issNo := 2 + + ghIssueClient := ghICMock{ + issues: []*github.Issue{ + {Number: &prNo, Title: &prTitle, HTMLURL: &prHTMLURL, PullRequestLinks: &github.PullRequestLinks{URL: &prHTMLURL}}, + {Number: &issNo, Title: &issTitle, HTMLURL: &issHTMLURL}, + }, + } + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + AddClosedIssues: true, + AddDeltaToLastRelease: true, + Commitish: "master", + Owner: "TEST", + Repository: "test", + ServerURL: "https://github.com", + ReleaseBodyHeader: "Header", + Version: "1.1", + } + err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient) + + assert.NoError(t, err, "Error occured but none expected.") + + assert.Equal(t, "Header\n\n**List of closed pull-requests since last release**\n[#1](https://github.com/TEST/test/pull/1): Pull\n\n**List of closed issues since last release**\n[#2](https://github.com/TEST/test/issues/2): Issue\n\n**Changes**\n[1.0...1.1](https://github.com/TEST/test/compare/1.0...1.1)\n", ghRepoClient.release.GetBody()) + assert.Equal(t, "1.1", ghRepoClient.release.GetName()) + assert.Equal(t, "1.1", ghRepoClient.release.GetTagName()) + assert.Equal(t, "master", ghRepoClient.release.GetTargetCommitish()) + + assert.Equal(t, lastPublishedAt.Time, ghIssueClient.lastPublished) + }) + + t.Run("Success - update asset", func(t *testing.T) { + var releaseID int64 = 1 + ghIssueClient := ghICMock{} + ghRepoClient := ghRCMock{ + latestRelease: &github.RepositoryRelease{ + ID: &releaseID, + }, + } + + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + UpdateAsset: true, + AssetPath: filepath.Join("testdata", t.Name()+"_test.txt"), + Version: "latest", + } + + err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient) + + assert.NoError(t, err, "Error occured but none expected.") + + assert.Nil(t, ghRepoClient.release) + + assert.Equal(t, releaseID, ghRepoClient.listID) + assert.Equal(t, releaseID, ghRepoClient.uploadID) + }) + + t.Run("Error - get release", func(t *testing.T) { + ghIssueClient := ghICMock{} + ghRepoClient := ghRCMock{ + latestErr: fmt.Errorf("Latest release error"), + } + myGithubPublishReleaseOptions := githubPublishReleaseOptions{} + err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient) + + assert.Equal(t, "Error occured when retrieving latest GitHub release.: Latest release error", fmt.Sprint(err)) + }) + + t.Run("Error - create release", func(t *testing.T) { + ghIssueClient := ghICMock{} + ghRepoClient := ghRCMock{ + createErr: fmt.Errorf("Create release error"), + } + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + Version: "1.0", + } + err := runGithubPublishRelease(ctx, &myGithubPublishReleaseOptions, &ghRepoClient, &ghIssueClient) + + assert.Equal(t, "Creation of release '1.0' failed: Create release error", fmt.Sprint(err)) + }) +} + +func TestGetClosedIssuesText(t *testing.T) { + ctx := context.Background() + publishedAt := github.Timestamp{Time: time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC)} + + t.Run("No issues", func(t *testing.T) { + ghIssueClient := ghICMock{} + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + Version: "1.0", + } + + res := getClosedIssuesText(ctx, publishedAt, &myGithubPublishReleaseOptions, &ghIssueClient) + + assert.Equal(t, "", res) + }) + + t.Run("All issues", func(t *testing.T) { + ctx := context.Background() + publishedAt := github.Timestamp{Time: time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC)} + + prHTMLURL := []string{"https://github.com/TEST/test/pull/1", "https://github.com/TEST/test/pull/2"} + prTitle := []string{"Pull1", "Pull2"} + prNo := []int{1, 2} + + issHTMLURL := []string{"https://github.com/TEST/test/issues/3", "https://github.com/TEST/test/issues/4"} + issTitle := []string{"Issue3", "Issue4"} + issNo := []int{3, 4} + + ghIssueClient := ghICMock{ + issues: []*github.Issue{ + {Number: &prNo[0], Title: &prTitle[0], HTMLURL: &prHTMLURL[0], PullRequestLinks: &github.PullRequestLinks{URL: &prHTMLURL[0]}}, + {Number: &prNo[1], Title: &prTitle[1], HTMLURL: &prHTMLURL[1], PullRequestLinks: &github.PullRequestLinks{URL: &prHTMLURL[1]}}, + {Number: &issNo[0], Title: &issTitle[0], HTMLURL: &issHTMLURL[0]}, + {Number: &issNo[1], Title: &issTitle[1], HTMLURL: &issHTMLURL[1]}, + }, + } + + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + Owner: "TEST", + Repository: "test", + } + + res := getClosedIssuesText(ctx, publishedAt, &myGithubPublishReleaseOptions, &ghIssueClient) + + assert.Equal(t, "\n**List of closed pull-requests since last release**\n[#1](https://github.com/TEST/test/pull/1): Pull1\n[#2](https://github.com/TEST/test/pull/2): Pull2\n\n**List of closed issues since last release**\n[#3](https://github.com/TEST/test/issues/3): Issue3\n[#4](https://github.com/TEST/test/issues/4): Issue4\n", res) + assert.Equal(t, "TEST", ghIssueClient.owner, "Owner not properly passed") + assert.Equal(t, "test", ghIssueClient.repo, "Repo not properly passed") + assert.Equal(t, "closed", ghIssueClient.options.State, "Issue state not properly passed") + assert.Equal(t, "asc", ghIssueClient.options.Direction, "Sort direction not properly passed") + assert.Equal(t, publishedAt.Time, ghIssueClient.options.Since, "PublishedAt not properly passed") + }) + +} + +func TestGetReleaseDeltaText(t *testing.T) { + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + Owner: "TEST", + Repository: "test", + ServerURL: "https://github.com", + Version: "1.1", + } + lastTag := "1.0" + lastRelease := github.RepositoryRelease{ + TagName: &lastTag, + } + + res := getReleaseDeltaText(&myGithubPublishReleaseOptions, &lastRelease) + + assert.Equal(t, "\n**Changes**\n[1.0...1.1](https://github.com/TEST/test/compare/1.0...1.1)\n", res) +} + +func TestUploadReleaseAsset(t *testing.T) { + ctx := context.Background() + + t.Run("Success - existing asset", func(t *testing.T) { + var releaseID int64 = 1 + assetName := "Success_-_existing_asset_test.txt" + var assetID int64 = 11 + ghRepoClient := ghRCMock{ + latestRelease: &github.RepositoryRelease{ + ID: &releaseID, + }, + listReleaseAssets: []*github.ReleaseAsset{ + {Name: &assetName, ID: &assetID}, + }, + } + + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + Owner: "TEST", + Repository: "test", + AssetPath: filepath.Join("testdata", t.Name()+"_test.txt"), + } + + err := uploadReleaseAsset(ctx, releaseID, &myGithubPublishReleaseOptions, &ghRepoClient) + + assert.NoError(t, err, "Error occured but none expected.") + + assert.Equal(t, "TEST", ghRepoClient.listOwner, "Owner not properly passed - list") + assert.Equal(t, "test", ghRepoClient.listRepo, "Repo not properly passed - list") + assert.Equal(t, releaseID, ghRepoClient.listID, "Relase ID not properly passed - list") + + assert.Equal(t, "TEST", ghRepoClient.delOwner, "Owner not properly passed - del") + assert.Equal(t, "test", ghRepoClient.delRepo, "Repo not properly passed - del") + assert.Equal(t, assetID, ghRepoClient.delID, "Relase ID not properly passed - del") + + assert.Equal(t, "TEST", ghRepoClient.uploadOwner, "Owner not properly passed - upload") + assert.Equal(t, "test", ghRepoClient.uploadRepo, "Repo not properly passed - upload") + assert.Equal(t, releaseID, ghRepoClient.uploadID, "Relase ID not properly passed - upload") + assert.Equal(t, "text/plain; charset=utf-8", ghRepoClient.uploadOpts.MediaType, "Wrong MediaType passed - upload") + }) + + t.Run("Success - no asset", func(t *testing.T) { + var releaseID int64 = 1 + assetName := "notFound" + var assetID int64 = 11 + ghRepoClient := ghRCMock{ + latestRelease: &github.RepositoryRelease{ + ID: &releaseID, + }, + listReleaseAssets: []*github.ReleaseAsset{ + {Name: &assetName, ID: &assetID}, + }, + } + + myGithubPublishReleaseOptions := githubPublishReleaseOptions{ + Owner: "TEST", + Repository: "test", + AssetPath: filepath.Join("testdata", t.Name()+"_test.txt"), + } + + err := uploadReleaseAsset(ctx, releaseID, &myGithubPublishReleaseOptions, &ghRepoClient) + + assert.NoError(t, err, "Error occured but none expected.") + + assert.Equal(t, int64(0), ghRepoClient.delID, "Relase ID should not be populated") + }) + + t.Run("Error - List Assets", func(t *testing.T) { + var releaseID int64 = 1 + ghRepoClient := ghRCMock{ + listErr: fmt.Errorf("List Asset Error"), + } + myGithubPublishReleaseOptions := githubPublishReleaseOptions{} + + err := uploadReleaseAsset(ctx, releaseID, &myGithubPublishReleaseOptions, &ghRepoClient) + assert.Equal(t, "Failed to get list of release assets.: List Asset Error", fmt.Sprint(err), "Wrong error received") + }) +} + +func TestIsExcluded(t *testing.T) { + + l1 := "label1" + l2 := "label2" + + tt := []struct { + issue *github.Issue + excludeLabels []string + expected bool + }{ + {issue: nil, excludeLabels: nil, expected: false}, + {issue: &github.Issue{}, excludeLabels: nil, expected: false}, + {issue: &github.Issue{Labels: []github.Label{{Name: &l1}}}, excludeLabels: nil, expected: false}, + {issue: &github.Issue{Labels: []github.Label{{Name: &l1}}}, excludeLabels: []string{"label0"}, expected: false}, + {issue: &github.Issue{Labels: []github.Label{{Name: &l1}}}, excludeLabels: []string{"label1"}, expected: true}, + {issue: &github.Issue{Labels: []github.Label{{Name: &l1}, {Name: &l2}}}, excludeLabels: []string{}, expected: false}, + {issue: &github.Issue{Labels: []github.Label{{Name: &l1}, {Name: &l2}}}, excludeLabels: []string{"label1"}, expected: true}, + } + + for k, v := range tt { + assert.Equal(t, v.expected, isExcluded(v.issue, v.excludeLabels), fmt.Sprintf("Run %v failed", k)) + } + +} diff --git a/cmd/karmaExecuteTests_generated.go b/cmd/karmaExecuteTests_generated.go index 3d6b58925..db6e8fe23 100644 --- a/cmd/karmaExecuteTests_generated.go +++ b/cmd/karmaExecuteTests_generated.go @@ -1,8 +1,6 @@ package cmd import ( - //"os" - "github.com/SAP/jenkins-library/pkg/config" "github.com/SAP/jenkins-library/pkg/log" "github.com/spf13/cobra" diff --git a/cmd/piper.go b/cmd/piper.go index 3813bb54b..7994ee8f6 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -41,6 +41,7 @@ func Execute() { rootCmd.AddCommand(ConfigCommand()) rootCmd.AddCommand(VersionCommand()) rootCmd.AddCommand(KarmaExecuteTestsCommand()) + rootCmd.AddCommand(GithubPublishReleaseCommand()) addRootFlags(rootCmd) if err := rootCmd.Execute(); err != nil { diff --git a/cmd/testdata/TestRunGithubPublishRelease/Success_-_update_asset_test.txt b/cmd/testdata/TestRunGithubPublishRelease/Success_-_update_asset_test.txt new file mode 100644 index 000000000..3b1246497 --- /dev/null +++ b/cmd/testdata/TestRunGithubPublishRelease/Success_-_update_asset_test.txt @@ -0,0 +1 @@ +TEST \ No newline at end of file diff --git a/cmd/testdata/TestUploadReleaseAsset/Success_-_existing_asset_test.txt b/cmd/testdata/TestUploadReleaseAsset/Success_-_existing_asset_test.txt new file mode 100644 index 000000000..3b1246497 --- /dev/null +++ b/cmd/testdata/TestUploadReleaseAsset/Success_-_existing_asset_test.txt @@ -0,0 +1 @@ +TEST \ No newline at end of file diff --git a/cmd/testdata/TestUploadReleaseAsset/Success_-_no_asset_test.txt b/cmd/testdata/TestUploadReleaseAsset/Success_-_no_asset_test.txt new file mode 100644 index 000000000..3b1246497 --- /dev/null +++ b/cmd/testdata/TestUploadReleaseAsset/Success_-_no_asset_test.txt @@ -0,0 +1 @@ +TEST \ No newline at end of file diff --git a/cmd/version_generated.go b/cmd/version_generated.go index 2b4802299..d0d8c2152 100644 --- a/cmd/version_generated.go +++ b/cmd/version_generated.go @@ -1,8 +1,6 @@ package cmd import ( - //"os" - "github.com/SAP/jenkins-library/pkg/config" "github.com/SAP/jenkins-library/pkg/log" "github.com/spf13/cobra" diff --git a/go.mod b/go.mod index 2c137bf93..c678f17e6 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.13 require ( github.com/ghodss/yaml v1.0.0 + github.com/google/go-github/v28 v28.1.1 github.com/google/go-cmp v0.3.1 github.com/pkg/errors v0.8.1 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.2.2 + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 ) diff --git a/go.sum b/go.sum index dcd04561d..5617bdbb1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -9,6 +10,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= +github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -42,10 +49,24 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/generator/step-metadata.go b/pkg/generator/step-metadata.go index 4809a58d6..fabf35c74 100644 --- a/pkg/generator/step-metadata.go +++ b/pkg/generator/step-metadata.go @@ -30,7 +30,7 @@ type stepInfo struct { const stepGoTemplate = `package cmd import ( - //"os" + {{if .OSImport}}"os"{{end}} "github.com/SAP/jenkins-library/pkg/config" "github.com/SAP/jenkins-library/pkg/log" @@ -148,10 +148,11 @@ func processMetaFiles(metadataFiles []string, openFile func(s string) (io.ReadCl fmt.Printf("Step name: %v\n", stepData.Metadata.Name) - err = setDefaultParameters(&stepData) + osImport := false + osImport, err = setDefaultParameters(&stepData) checkError(err) - myStepInfo := getStepInfo(&stepData) + myStepInfo := getStepInfo(&stepData, osImport) step := stepTemplate(myStepInfo) err = writeFile(fmt.Sprintf("cmd/%v_generated.go", stepData.Metadata.Name), step, 0644) @@ -172,14 +173,16 @@ func fileWriter(filename string, data []byte, perm os.FileMode) error { return ioutil.WriteFile(filename, data, perm) } -func setDefaultParameters(stepData *config.StepData) error { +func setDefaultParameters(stepData *config.StepData) (bool, error) { //ToDo: custom function for default handling, support all relevant parameter types + osImportRequired := false for k, param := range stepData.Spec.Inputs.Parameters { if param.Default == nil { switch param.Type { case "string": param.Default = fmt.Sprintf("os.Getenv(\"PIPER_%v\")", param.Name) + osImportRequired = true case "bool": // ToDo: Check if default should be read from env param.Default = "false" @@ -187,7 +190,7 @@ func setDefaultParameters(stepData *config.StepData) error { // ToDo: Check if default should be read from env param.Default = "[]string{}" default: - return fmt.Errorf("Meta data type not set or not known: '%v'", param.Type) + return false, fmt.Errorf("Meta data type not set or not known: '%v'", param.Type) } } else { switch param.Type { @@ -202,16 +205,16 @@ func setDefaultParameters(stepData *config.StepData) error { case "[]string": param.Default = fmt.Sprintf("[]string{\"%v\"}", strings.Join(param.Default.([]string), "\", \"")) default: - return fmt.Errorf("Meta data type not set or not known: '%v'", param.Type) + return false, fmt.Errorf("Meta data type not set or not known: '%v'", param.Type) } } stepData.Spec.Inputs.Parameters[k] = param } - return nil + return osImportRequired, nil } -func getStepInfo(stepData *config.StepData) stepInfo { +func getStepInfo(stepData *config.StepData, osImport bool) stepInfo { return stepInfo{ StepName: stepData.Metadata.Name, CobraCmdFuncName: fmt.Sprintf("%vCommand", strings.Title(stepData.Metadata.Name)), @@ -220,6 +223,7 @@ func getStepInfo(stepData *config.StepData) stepInfo { Long: stepData.Metadata.LongDescription, Metadata: stepData.Spec.Inputs.Parameters, FlagsFunc: fmt.Sprintf("add%vFlags", strings.Title(stepData.Metadata.Name)), + OSImport: osImport, } } @@ -291,6 +295,7 @@ func longName(long string) string { func golangName(name string) string { properName := strings.Replace(name, "Api", "API", -1) + properName = strings.Replace(properName, "api", "API", -1) properName = strings.Replace(properName, "Url", "URL", -1) properName = strings.Replace(properName, "Id", "ID", -1) properName = strings.Replace(properName, "Json", "JSON", -1) diff --git a/pkg/generator/step-metadata_test.go b/pkg/generator/step-metadata_test.go index 538c772f0..0285b0360 100644 --- a/pkg/generator/step-metadata_test.go +++ b/pkg/generator/step-metadata_test.go @@ -112,10 +112,12 @@ func TestSetDefaultParameters(t *testing.T) { "[]string{}", } - err := setDefaultParameters(&stepData) + osImport, err := setDefaultParameters(&stepData) assert.NoError(t, err, "error occured but none expected") + assert.Equal(t, true, osImport, "import of os package required") + for k, v := range expected { assert.Equal(t, v, stepData.Spec.Inputs.Parameters[k].Default, fmt.Sprintf("default not correct for parameter %v", k)) } @@ -145,7 +147,7 @@ func TestSetDefaultParameters(t *testing.T) { } for k, v := range stepData { - err := setDefaultParameters(&v) + _, err := setDefaultParameters(&v) assert.Error(t, err, fmt.Sprintf("error expected but none occured for parameter %v", k)) } }) @@ -168,7 +170,7 @@ func TestGetStepInfo(t *testing.T) { }, } - myStepInfo := getStepInfo(&stepData) + myStepInfo := getStepInfo(&stepData, true) assert.Equal(t, "testStep", myStepInfo.StepName, "StepName incorrect") assert.Equal(t, "TestStepCommand", myStepInfo.CobraCmdFuncName, "CobraCmdFuncName incorrect") @@ -177,6 +179,7 @@ func TestGetStepInfo(t *testing.T) { assert.Equal(t, "Long Test description", myStepInfo.Long, "Long incorrect") assert.Equal(t, stepData.Spec.Inputs.Parameters, myStepInfo.Metadata, "Metadata incorrect") assert.Equal(t, "addTestStepFlags", myStepInfo.FlagsFunc, "FlagsFunc incorrect") + assert.Equal(t, "addTestStepFlags", myStepInfo.FlagsFunc, "FlagsFunc incorrect") } @@ -200,6 +203,7 @@ func TestGolangName(t *testing.T) { expected string }{ {input: "testApi", expected: "TestAPI"}, + {input: "apiTest", expected: "APITest"}, {input: "testUrl", expected: "TestURL"}, {input: "testId", expected: "TestID"}, {input: "testJson", expected: "TestJSON"}, diff --git a/pkg/generator/testdata/TestProcessMetaFiles/step_code_generated.golden b/pkg/generator/testdata/TestProcessMetaFiles/step_code_generated.golden index defa93201..7a244bbae 100644 --- a/pkg/generator/testdata/TestProcessMetaFiles/step_code_generated.golden +++ b/pkg/generator/testdata/TestProcessMetaFiles/step_code_generated.golden @@ -1,7 +1,7 @@ package cmd import ( - //"os" + "os" "github.com/SAP/jenkins-library/pkg/config" "github.com/SAP/jenkins-library/pkg/log" diff --git a/pkg/github/github.go b/pkg/github/github.go new file mode 100644 index 000000000..5897a999c --- /dev/null +++ b/pkg/github/github.go @@ -0,0 +1,23 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v28/github" + "golang.org/x/oauth2" +) + +//NewClient creates a new GitHub client using an OAuth token for authentication +func NewClient(token, apiURL, uploadURL string) (context.Context, *github.Client, error) { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + + client, err := github.NewEnterpriseClient(apiURL, uploadURL, tc) + if err != nil { + return ctx, nil, err + } + return ctx, client, nil +} diff --git a/resources/metadata/githubrelease.yaml b/resources/metadata/githubrelease.yaml new file mode 100644 index 000000000..6e8cefa55 --- /dev/null +++ b/resources/metadata/githubrelease.yaml @@ -0,0 +1,154 @@ +metadata: + name: githubPublishRelease + description: Publish a release in GitHub + longDescription: | + This step creates a tag in your GitHub repository together with a release. + The release can be filled with text plus additional information like: + + * Closed pull request since last release + * Closed issues since last release + * Link to delta information showing all commits since last release + + The result looks like + + ![Example release](../images/githubRelease.png) +spec: + inputs: + secrets: + - name: githubTokenCredentialsId + description: Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub. + type: jenkins + params: + - name: addClosedIssues + description: 'If set to `true`, closed issues and merged pull-requests since the last release will added below the `releaseBodyHeader`' + scope: + - PARAMETERS + - STAGES + - STEPS + type: bool + default: false + - name: addDeltaToLastRelease + description: 'If set to `true`, a link will be added to the relese information that brings up all commits since the last release.' + scope: + - PARAMETERS + - STAGES + - STEPS + type: bool + default: false + - name: assetPath + description: Path to a release asset which should be uploaded to the list of release assets. + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + - name: commitish + description: 'Target git commitish for the release' + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + default: "master" + - name: excludeLabels + description: 'Allows to exclude issues with dedicated list of labels.' + scope: + - PARAMETERS + - STAGES + - STEPS + type: '[]string' + - 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: owner + aliases: + - name: githubOrg + description: 'Set the GitHub organization.' + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: repository + aliases: + - name: githubRepo + description: 'Set the GitHub repository.' + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true + - name: serverUrl + aliases: + - name: githubServerUrl + description: 'GitHub server url for end-user access.' + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + default: https://github.com + mandatory: true + - name: token + aliases: + - name: githubToken + 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 + - name: uploadUrl + aliases: + - name: githubUploadUrl + description: Set the GitHub API url. + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + default: https://uploads.github.com + mandatory: true + - name: labels + description: 'Labels to include in issue search.' + scope: + - PARAMETERS + - STAGES + - STEPS + type: '[]string' + - name: releaseBodyHeader + description: Content which will appear for the release. + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + - name: updateAsset + description: Specify if a release asset should be updated only. + scope: + - PARAMETERS + - STAGES + - STEPS + type: bool + - name: version + description: 'Define the version number which will be written as tag as well as release name.' + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + mandatory: true