github.com/windmeup/goreleaser@v1.21.95/internal/client/gitlab.go (about) 1 package client 2 3 import ( 4 "crypto/tls" 5 "fmt" 6 "net/http" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/caarlos0/log" 12 "github.com/windmeup/goreleaser/internal/artifact" 13 "github.com/windmeup/goreleaser/internal/tmpl" 14 "github.com/windmeup/goreleaser/pkg/config" 15 "github.com/windmeup/goreleaser/pkg/context" 16 "github.com/xanzy/go-gitlab" 17 ) 18 19 const DefaultGitLabDownloadURL = "https://gitlab.com" 20 21 type gitlabClient struct { 22 client *gitlab.Client 23 } 24 25 var _ Client = &gitlabClient{} 26 27 // newGitLab returns a gitlab client implementation. 28 func newGitLab(ctx *context.Context, token string) (*gitlabClient, error) { 29 transport := &http.Transport{ 30 Proxy: http.ProxyFromEnvironment, 31 TLSClientConfig: &tls.Config{ 32 // nolint: gosec 33 InsecureSkipVerify: ctx.Config.GitLabURLs.SkipTLSVerify, 34 }, 35 } 36 options := []gitlab.ClientOptionFunc{ 37 gitlab.WithHTTPClient(&http.Client{ 38 Transport: transport, 39 }), 40 } 41 if ctx.Config.GitLabURLs.API != "" { 42 apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.API) 43 if err != nil { 44 return nil, fmt.Errorf("templating GitLab API URL: %w", err) 45 } 46 47 options = append(options, gitlab.WithBaseURL(apiURL)) 48 } 49 50 var client *gitlab.Client 51 var err error 52 if checkUseJobToken(*ctx, token) { 53 client, err = gitlab.NewJobClient(token, options...) 54 } else { 55 client, err = gitlab.NewClient(token, options...) 56 } 57 if err != nil { 58 return &gitlabClient{}, err 59 } 60 return &gitlabClient{client: client}, nil 61 } 62 63 func (c *gitlabClient) Changelog(_ *context.Context, repo Repo, prev, current string) (string, error) { 64 cmpOpts := &gitlab.CompareOptions{ 65 From: &prev, 66 To: ¤t, 67 } 68 result, _, err := c.client.Repositories.Compare(repo.String(), cmpOpts) 69 var log []string 70 if err != nil { 71 return "", err 72 } 73 74 for _, commit := range result.Commits { 75 log = append(log, fmt.Sprintf( 76 "%s: %s (%s <%s>)", 77 commit.ShortID, 78 strings.Split(commit.Message, "\n")[0], 79 commit.AuthorName, 80 commit.AuthorEmail, 81 )) 82 } 83 return strings.Join(log, "\n"), nil 84 } 85 86 // getDefaultBranch get the default branch 87 func (c *gitlabClient) getDefaultBranch(_ *context.Context, repo Repo) (string, error) { 88 projectID := repo.String() 89 p, res, err := c.client.Projects.GetProject(projectID, nil) 90 if err != nil { 91 log. 92 WithField("projectID", projectID). 93 WithField("statusCode", res.StatusCode). 94 WithError(err). 95 Warn("error checking for default branch") 96 return "", err 97 } 98 return p.DefaultBranch, nil 99 } 100 101 // CloseMilestone closes a given milestone. 102 func (c *gitlabClient) CloseMilestone(_ *context.Context, repo Repo, title string) error { 103 milestone, err := c.getMilestoneByTitle(repo, title) 104 if err != nil { 105 return err 106 } 107 108 if milestone == nil { 109 return ErrNoMilestoneFound{Title: title} 110 } 111 112 closeStateEvent := "close" 113 114 opts := &gitlab.UpdateMilestoneOptions{ 115 Description: &milestone.Description, 116 DueDate: milestone.DueDate, 117 StartDate: milestone.StartDate, 118 StateEvent: &closeStateEvent, 119 Title: &milestone.Title, 120 } 121 122 _, _, err = c.client.Milestones.UpdateMilestone( 123 repo.String(), 124 milestone.ID, 125 opts, 126 ) 127 128 return err 129 } 130 131 // CreateFile gets a file in the repository at a given path 132 // and updates if it exists or creates it for later pipes in the pipeline. 133 func (c *gitlabClient) CreateFile( 134 ctx *context.Context, 135 commitAuthor config.CommitAuthor, 136 repo Repo, 137 content []byte, // the content of the formula.rb 138 path, // the path to the formula.rb 139 message string, // the commit msg 140 ) error { 141 fileName := path 142 projectID := repo.String() 143 144 // Use the project default branch if we can get it...otherwise, just use 145 // 'master' 146 var branch, ref string 147 var err error 148 // Use the branch if given one 149 if repo.Branch != "" { 150 branch = repo.Branch 151 } else { 152 // Try to get the default branch from the Git provider 153 branch, err = c.getDefaultBranch(ctx, repo) 154 if err != nil { 155 // Fall back to 'master' 😠156 log. 157 WithField("fileName", fileName). 158 WithField("projectID", repo.String()). 159 WithField("requestedBranch", branch). 160 WithError(err). 161 Warn("error checking for default branch, using master") 162 ref = "master" 163 branch = "master" 164 } 165 } 166 ref = branch 167 opts := &gitlab.GetFileOptions{Ref: &ref} 168 castedContent := string(content) 169 170 log. 171 WithField("owner", repo.Owner). 172 WithField("name", repo.Name). 173 WithField("ref", ref). 174 WithField("branch", branch). 175 Debug("projectID at brew") 176 177 log. 178 WithField("repository", repo.String()). 179 WithField("name", repo.Name). 180 WithField("name", repo.Name). 181 Info("pushing") 182 183 _, res, err := c.client.RepositoryFiles.GetFile(repo.String(), fileName, opts) 184 if err != nil && (res == nil || res.StatusCode != 404) { 185 log. 186 WithField("fileName", fileName). 187 WithField("ref", ref). 188 WithField("projectID", projectID). 189 WithField("statusCode", res.StatusCode). 190 WithError(err). 191 Error("error getting file for brew formula") 192 return err 193 } 194 195 log. 196 WithField("fileName", fileName). 197 WithField("branch", branch). 198 WithField("projectID", projectID). 199 Debug("found already existing brew formula file") 200 201 if res.StatusCode == 404 { 202 log. 203 WithField("fileName", fileName). 204 WithField("ref", ref). 205 WithField("projectID", projectID). 206 Debug("creating brew formula") 207 createOpts := &gitlab.CreateFileOptions{ 208 AuthorName: &commitAuthor.Name, 209 AuthorEmail: &commitAuthor.Email, 210 Content: &castedContent, 211 Branch: &branch, 212 CommitMessage: &message, 213 } 214 fileInfo, res, err := c.client.RepositoryFiles.CreateFile(projectID, fileName, createOpts) 215 if err != nil { 216 log. 217 WithField("fileName", fileName). 218 WithField("branch", branch). 219 WithField("projectID", projectID). 220 WithField("statusCode", res.StatusCode). 221 WithError(err). 222 Error("error creating brew formula file") 223 return err 224 } 225 226 log. 227 WithField("fileName", fileName). 228 WithField("branch", branch). 229 WithField("projectID", projectID). 230 WithField("filePath", fileInfo.FilePath). 231 Debug("created brew formula file") 232 return nil 233 } 234 235 log. 236 WithField("fileName", fileName). 237 WithField("ref", ref). 238 WithField("projectID", projectID). 239 Debug("updating brew formula") 240 updateOpts := &gitlab.UpdateFileOptions{ 241 AuthorName: &commitAuthor.Name, 242 AuthorEmail: &commitAuthor.Email, 243 Content: &castedContent, 244 Branch: &branch, 245 CommitMessage: &message, 246 } 247 248 updateFileInfo, res, err := c.client.RepositoryFiles.UpdateFile(projectID, fileName, updateOpts) 249 if err != nil { 250 log. 251 WithField("fileName", fileName). 252 WithField("branch", branch). 253 WithField("projectID", projectID). 254 WithField("statusCode", res.StatusCode). 255 WithError(err). 256 Error("error updating brew formula file") 257 return err 258 } 259 260 log. 261 WithField("fileName", fileName). 262 WithField("branch", branch). 263 WithField("projectID", projectID). 264 WithField("filePath", updateFileInfo.FilePath). 265 WithField("statusCode", res.StatusCode). 266 Debug("updated brew formula file") 267 return nil 268 } 269 270 // CreateRelease creates a new release or updates it by keeping 271 // the release notes if it exists. 272 func (c *gitlabClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) { 273 title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate) 274 if err != nil { 275 return "", err 276 } 277 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 278 if err != nil { 279 return "", err 280 } 281 projectID := gitlabName 282 if ctx.Config.Release.GitLab.Owner != "" { 283 projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID 284 } 285 log. 286 WithField("owner", ctx.Config.Release.GitLab.Owner). 287 WithField("name", gitlabName). 288 WithField("projectID", projectID). 289 Debug("projectID") 290 291 name := title 292 tagName := ctx.Git.CurrentTag 293 release, resp, err := c.client.Releases.GetRelease(projectID, tagName) 294 if err != nil && (resp == nil || (resp.StatusCode != 403 && resp.StatusCode != 404)) { 295 return "", err 296 } 297 298 if resp.StatusCode == 403 || resp.StatusCode == 404 { 299 log.WithError(err).Debug("get release") 300 301 description := body 302 ref := ctx.Git.Commit 303 gitURL := ctx.Git.URL 304 305 log. 306 WithField("name", name). 307 WithField("description", description). 308 WithField("ref", ref). 309 WithField("url", gitURL). 310 Debug("creating release") 311 release, _, err = c.client.Releases.CreateRelease(projectID, &gitlab.CreateReleaseOptions{ 312 Name: &name, 313 Description: &description, 314 Ref: &ref, 315 TagName: &tagName, 316 }) 317 318 if err != nil { 319 log.WithError(err).Debug("error creating release") 320 return "", err 321 } 322 log.WithField("name", release.Name).Info("release created") 323 } else { 324 desc := body 325 if release != nil { 326 desc = getReleaseNotes(release.Description, body, ctx.Config.Release.ReleaseNotesMode) 327 } 328 329 release, _, err = c.client.Releases.UpdateRelease(projectID, tagName, &gitlab.UpdateReleaseOptions{ 330 Name: &name, 331 Description: &desc, 332 }) 333 if err != nil { 334 log.WithError(err).Debug("error updating release") 335 return "", err 336 } 337 338 log.WithField("name", release.Name).Info("release updated") 339 } 340 341 return tagName, err // gitlab references a tag in a repo by its name 342 } 343 344 func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error) { 345 var urlTemplate string 346 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 347 if err != nil { 348 return "", err 349 } 350 downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download) 351 if err != nil { 352 return "", err 353 } 354 355 if ctx.Config.Release.GitLab.Owner != "" { 356 urlTemplate = fmt.Sprintf( 357 "%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", 358 downloadURL, 359 ctx.Config.Release.GitLab.Owner, 360 gitlabName, 361 ) 362 } else { 363 urlTemplate = fmt.Sprintf( 364 "%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", 365 downloadURL, 366 gitlabName, 367 ) 368 } 369 return urlTemplate, nil 370 } 371 372 // Upload uploads a file into a release repository. 373 func (c *gitlabClient) Upload( 374 ctx *context.Context, 375 releaseID string, 376 artifact *artifact.Artifact, 377 file *os.File, 378 ) error { 379 // create new template and apply name field 380 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 381 if err != nil { 382 return err 383 } 384 projectID := gitlabName 385 // check if owner is empty 386 if ctx.Config.Release.GitLab.Owner != "" { 387 projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID 388 } 389 390 var baseLinkURL string 391 var linkURL string 392 if ctx.Config.GitLabURLs.UsePackageRegistry { 393 log.WithField("file", file.Name()).Debug("uploading file as generic package") 394 if _, _, err := c.client.GenericPackages.PublishPackageFile( 395 projectID, 396 ctx.Config.ProjectName, 397 ctx.Version, 398 artifact.Name, 399 file, 400 nil, 401 ); err != nil { 402 return err 403 } 404 405 baseLinkURL, err = c.client.GenericPackages.FormatPackageURL( 406 projectID, 407 ctx.Config.ProjectName, 408 ctx.Version, 409 artifact.Name, 410 ) 411 if err != nil { 412 return err 413 } 414 linkURL = c.client.BaseURL().String() + baseLinkURL 415 } else { 416 log.WithField("file", file.Name()).Debug("uploading file as attachment") 417 projectFile, _, err := c.client.Projects.UploadFile( 418 projectID, 419 file, 420 filepath.Base(file.Name()), 421 nil, 422 ) 423 if err != nil { 424 return err 425 } 426 427 baseLinkURL = projectFile.URL 428 gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download) 429 if err != nil { 430 return fmt.Errorf("templating GitLab Download URL: %w", err) 431 } 432 433 // search for project details based on projectID 434 projectDetails, _, err := c.client.Projects.GetProject(projectID, nil) 435 if err != nil { 436 return err 437 } 438 linkURL = gitlabBaseURL + "/" + projectDetails.PathWithNamespace + baseLinkURL 439 } 440 441 log.WithField("file", file.Name()). 442 WithField("url", baseLinkURL). 443 Debug("uploaded file") 444 445 name := artifact.Name 446 filename := "/" + name 447 releaseLink, _, err := c.client.ReleaseLinks.CreateReleaseLink( 448 projectID, 449 releaseID, 450 &gitlab.CreateReleaseLinkOptions{ 451 Name: &name, 452 URL: &linkURL, 453 FilePath: &filename, 454 }) 455 if err != nil { 456 return RetriableError{err} 457 } 458 459 log.WithField("id", releaseLink.ID). 460 WithField("url", releaseLink.DirectAssetURL). 461 Debug("created release link") 462 463 // for checksums.txt the field is nil, so we initialize it 464 if artifact.Extra == nil { 465 artifact.Extra = make(map[string]interface{}) 466 } 467 468 return nil 469 } 470 471 // getMilestoneByTitle returns a milestone by title. 472 func (c *gitlabClient) getMilestoneByTitle(repo Repo, title string) (*gitlab.Milestone, error) { 473 opts := &gitlab.ListMilestonesOptions{ 474 Title: &title, 475 } 476 477 for { 478 milestones, resp, err := c.client.Milestones.ListMilestones(repo.String(), opts) 479 if err != nil { 480 return nil, err 481 } 482 483 for _, milestone := range milestones { 484 if milestone != nil && milestone.Title == title { 485 return milestone, nil 486 } 487 } 488 489 if resp.NextPage == 0 { 490 break 491 } 492 493 opts.Page = resp.NextPage 494 } 495 496 return nil, nil 497 } 498 499 // checkUseJobToken examines the context and given token, and determines if We should use NewJobClient vs NewClient 500 func checkUseJobToken(ctx context.Context, token string) bool { 501 // The CI_JOB_TOKEN env var is set automatically in all GitLab runners. 502 // If this comes back as empty, we aren't in a functional GitLab runner 503 ciToken := os.Getenv("CI_JOB_TOKEN") 504 if ciToken == "" { 505 return false 506 } 507 508 // We only want to use the JobToken client if we have specified 509 // UseJobToken. Older versions of GitLab don't work with this, so we 510 // want to be specific 511 if ctx.Config.GitLabURLs.UseJobToken { 512 // We may be creating a new client with a non-CI_JOB_TOKEN, for 513 // things like Homebrew publishing. We can't use the 514 // CI_JOB_TOKEN there 515 return token == ciToken 516 } 517 return false 518 }