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