github.com/jaylevin/jenkins-library@v1.230.4/cmd/githubPublishRelease.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "fmt" 6 "mime" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/SAP/jenkins-library/pkg/log" 12 "github.com/SAP/jenkins-library/pkg/telemetry" 13 "github.com/google/go-github/v32/github" 14 "github.com/pkg/errors" 15 16 piperGithub "github.com/SAP/jenkins-library/pkg/github" 17 ) 18 19 // mock generated with: mockery --name GithubRepoClient --dir cmd --output cmd/mocks 20 type GithubRepoClient interface { 21 CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error) 22 DeleteReleaseAsset(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) 23 GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error) 24 ListReleaseAssets(ctx context.Context, owner string, repo string, id int64, opt *github.ListOptions) ([]*github.ReleaseAsset, *github.Response, error) 25 UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error) 26 } 27 28 type githubIssueClient interface { 29 ListByRepo(ctx context.Context, owner string, repo string, opt *github.IssueListByRepoOptions) ([]*github.Issue, *github.Response, error) 30 } 31 32 func githubPublishRelease(config githubPublishReleaseOptions, telemetryData *telemetry.CustomData) { 33 //TODO provide parameter for trusted certs 34 ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, config.UploadURL, []string{}) 35 if err != nil { 36 log.Entry().WithError(err).Fatal("Failed to get GitHub client.") 37 } 38 39 err = runGithubPublishRelease(ctx, &config, client.Repositories, client.Issues) 40 if err != nil { 41 log.Entry().WithError(err).Fatal("Failed to publish GitHub release.") 42 } 43 } 44 45 func runGithubPublishRelease(ctx context.Context, config *githubPublishReleaseOptions, ghRepoClient GithubRepoClient, ghIssueClient githubIssueClient) error { 46 47 var publishedAt github.Timestamp 48 49 lastRelease, resp, err := ghRepoClient.GetLatestRelease(ctx, config.Owner, config.Repository) 50 if err != nil { 51 if resp != nil && resp.StatusCode == 404 { 52 //no previous release found -> first release 53 config.AddDeltaToLastRelease = false 54 log.Entry().Debug("This is the first release.") 55 } else { 56 return errors.Wrapf(err, "Error occurred when retrieving latest GitHub release (%v/%v)", config.Owner, config.Repository) 57 } 58 } 59 publishedAt = lastRelease.GetPublishedAt() 60 log.Entry().Debugf("Previous GitHub release published: '%v'", publishedAt) 61 62 //updating assets only supported on latest release 63 if len(config.AssetPath) > 0 && config.Version == "latest" { 64 return uploadReleaseAsset(ctx, lastRelease.GetID(), config, ghRepoClient) 65 } 66 67 releaseBody := "" 68 69 if len(config.ReleaseBodyHeader) > 0 { 70 releaseBody += config.ReleaseBodyHeader + "\n" 71 } 72 73 if config.AddClosedIssues { 74 releaseBody += getClosedIssuesText(ctx, publishedAt, config, ghIssueClient) 75 } 76 77 if config.AddDeltaToLastRelease { 78 releaseBody += getReleaseDeltaText(config, lastRelease) 79 } 80 81 prefixedTagName := config.TagPrefix + config.Version 82 83 release := github.RepositoryRelease{ 84 TagName: &prefixedTagName, 85 TargetCommitish: &config.Commitish, 86 Name: &config.Version, 87 Body: &releaseBody, 88 Prerelease: &config.PreRelease, 89 } 90 91 createdRelease, _, err := ghRepoClient.CreateRelease(ctx, config.Owner, config.Repository, &release) 92 if err != nil { 93 return errors.Wrapf(err, "Creation of release '%v' failed", *release.TagName) 94 } 95 log.Entry().Infof("Release %v created on %v/%v", *createdRelease.TagName, config.Owner, config.Repository) 96 97 if len(config.AssetPath) > 0 { 98 return uploadReleaseAsset(ctx, createdRelease.GetID(), config, ghRepoClient) 99 } else if len(config.AssetPathList) > 0 { 100 return uploadReleaseAssetList(ctx, createdRelease.GetID(), config, ghRepoClient) 101 } 102 103 return nil 104 } 105 106 func getClosedIssuesText(ctx context.Context, publishedAt github.Timestamp, config *githubPublishReleaseOptions, ghIssueClient githubIssueClient) string { 107 closedIssuesText := "" 108 109 options := github.IssueListByRepoOptions{ 110 State: "closed", 111 Direction: "asc", 112 Since: publishedAt.Time, 113 } 114 if len(config.Labels) > 0 { 115 options.Labels = config.Labels 116 } 117 ghIssues, _, err := ghIssueClient.ListByRepo(ctx, config.Owner, config.Repository, &options) 118 if err != nil { 119 log.Entry().WithError(err).Error("Failed to get GitHub issues.") 120 } 121 122 prTexts := []string{"**List of closed pull-requests since last release**"} 123 issueTexts := []string{"**List of closed issues since last release**"} 124 125 for _, issue := range ghIssues { 126 if issue.IsPullRequest() && !isExcluded(issue, config.ExcludeLabels) { 127 prTexts = append(prTexts, fmt.Sprintf("[#%v](%v): %v", issue.GetNumber(), issue.GetHTMLURL(), issue.GetTitle())) 128 log.Entry().Debugf("Added PR #%v to release", issue.GetNumber()) 129 } else if !issue.IsPullRequest() && !isExcluded(issue, config.ExcludeLabels) { 130 issueTexts = append(issueTexts, fmt.Sprintf("[#%v](%v): %v", issue.GetNumber(), issue.GetHTMLURL(), issue.GetTitle())) 131 log.Entry().Debugf("Added Issue #%v to release", issue.GetNumber()) 132 } 133 } 134 135 if len(prTexts) > 1 { 136 closedIssuesText += "\n" + strings.Join(prTexts, "\n") + "\n" 137 } 138 139 if len(issueTexts) > 1 { 140 closedIssuesText += "\n" + strings.Join(issueTexts, "\n") + "\n" 141 } 142 return closedIssuesText 143 } 144 145 func getReleaseDeltaText(config *githubPublishReleaseOptions, lastRelease *github.RepositoryRelease) string { 146 releaseDeltaText := "" 147 148 //add delta link to previous release 149 releaseDeltaText += "\n**Changes**\n" 150 releaseDeltaText += fmt.Sprintf( 151 "[%v...%v](%v/%v/%v/compare/%v...%v)\n", 152 lastRelease.GetTagName(), 153 config.Version, 154 config.ServerURL, 155 config.Owner, 156 config.Repository, 157 lastRelease.GetTagName(), config.Version, 158 ) 159 160 return releaseDeltaText 161 } 162 163 func uploadReleaseAssetList(ctx context.Context, releaseID int64, config *githubPublishReleaseOptions, ghRepoClient GithubRepoClient) error { 164 for _, asset := range config.AssetPathList { 165 config.AssetPath = asset 166 err := uploadReleaseAsset(ctx, releaseID, config, ghRepoClient) 167 if err != nil { 168 return fmt.Errorf("failed to upload release asset: %w", err) 169 } 170 } 171 return nil 172 } 173 174 func uploadReleaseAsset(ctx context.Context, releaseID int64, config *githubPublishReleaseOptions, ghRepoClient GithubRepoClient) error { 175 176 assets, _, err := ghRepoClient.ListReleaseAssets(ctx, config.Owner, config.Repository, releaseID, &github.ListOptions{}) 177 if err != nil { 178 return errors.Wrap(err, "Failed to get list of release assets.") 179 } 180 var assetID int64 181 for _, a := range assets { 182 if a.GetName() == filepath.Base(config.AssetPath) { 183 assetID = a.GetID() 184 break 185 } 186 } 187 if assetID != 0 { 188 //asset needs to be deleted first since API does not allow for replacement 189 _, err := ghRepoClient.DeleteReleaseAsset(ctx, config.Owner, config.Repository, assetID) 190 if err != nil { 191 return errors.Wrap(err, "Failed to delete release asset.") 192 } 193 } 194 195 mediaType := mime.TypeByExtension(filepath.Ext(config.AssetPath)) 196 if mediaType == "" { 197 mediaType = "application/octet-stream" 198 } 199 log.Entry().Debugf("Using mediaType '%v'", mediaType) 200 201 name := filepath.Base(config.AssetPath) 202 log.Entry().Debugf("Using file name '%v'", name) 203 204 opts := github.UploadOptions{ 205 Name: name, 206 MediaType: mediaType, 207 } 208 file, err := os.Open(config.AssetPath) 209 defer file.Close() 210 if err != nil { 211 return errors.Wrapf(err, "Failed to load release asset '%v'", config.AssetPath) 212 } 213 214 log.Entry().Info("Starting to upload release asset.") 215 asset, _, err := ghRepoClient.UploadReleaseAsset(ctx, config.Owner, config.Repository, releaseID, &opts, file) 216 if err != nil { 217 return errors.Wrap(err, "Failed to upload release asset.") 218 } 219 log.Entry().Infof("Done uploading asset '%v'.", asset.GetURL()) 220 221 return nil 222 } 223 224 func isExcluded(issue *github.Issue, excludeLabels []string) bool { 225 //issue.Labels[0].GetName() 226 for _, ex := range excludeLabels { 227 for _, l := range issue.Labels { 228 if ex == l.GetName() { 229 return true 230 } 231 } 232 } 233 return false 234 }