github.com/goreleaser/goreleaser@v1.25.1/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/goreleaser/goreleaser/internal/artifact" 13 "github.com/goreleaser/goreleaser/internal/tmpl" 14 "github.com/goreleaser/goreleaser/pkg/config" 15 "github.com/goreleaser/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 := log.WithField("projectID", projectID) 92 if res != nil { 93 log = log.WithField("statusCode", res.StatusCode) 94 } 95 log.WithError(err).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 143 projectID := repo.Name 144 if repo.Owner != "" { 145 projectID = repo.Owner + "/" + projectID 146 } 147 148 // Use the project default branch if we can get it...otherwise, just use 149 // 'master' 150 var branch, ref string 151 var err error 152 // Use the branch if given one 153 if repo.Branch != "" { 154 branch = repo.Branch 155 } else { 156 // Try to get the default branch from the Git provider 157 branch, err = c.getDefaultBranch(ctx, repo) 158 if err != nil { 159 // Fall back to 'master' 😠160 log. 161 WithField("fileName", fileName). 162 WithField("projectID", projectID). 163 WithField("requestedBranch", branch). 164 WithError(err). 165 Warn("error checking for default branch, using master") 166 ref = "master" 167 branch = "master" 168 } 169 } 170 ref = branch 171 opts := &gitlab.GetFileOptions{Ref: &ref} 172 castedContent := string(content) 173 174 log. 175 WithField("projectID", projectID). 176 WithField("ref", ref). 177 WithField("branch", branch). 178 Debug("projectID at brew") 179 180 log. 181 WithField("projectID", projectID). 182 Info("pushing") 183 184 _, res, err := c.client.RepositoryFiles.GetFile(projectID, fileName, opts) 185 if err != nil && (res == nil || res.StatusCode != 404) { 186 log := log. 187 WithField("fileName", fileName). 188 WithField("ref", ref). 189 WithField("projectID", projectID) 190 if res != nil { 191 log = log.WithField("statusCode", res.StatusCode) 192 } 193 log.WithError(err). 194 Error("error getting file for brew formula") 195 return err 196 } 197 198 log. 199 WithField("fileName", fileName). 200 WithField("branch", branch). 201 WithField("projectID", projectID). 202 Debug("found already existing brew formula file") 203 204 if res.StatusCode == 404 { 205 log. 206 WithField("fileName", fileName). 207 WithField("ref", ref). 208 WithField("projectID", projectID). 209 Debug("creating brew formula") 210 createOpts := &gitlab.CreateFileOptions{ 211 AuthorName: &commitAuthor.Name, 212 AuthorEmail: &commitAuthor.Email, 213 Content: &castedContent, 214 Branch: &branch, 215 CommitMessage: &message, 216 } 217 fileInfo, res, err := c.client.RepositoryFiles.CreateFile(projectID, fileName, createOpts) 218 if err != nil { 219 log := log. 220 WithField("fileName", fileName). 221 WithField("branch", branch). 222 WithField("projectID", projectID) 223 if res != nil { 224 log = log.WithField("statusCode", res.StatusCode) 225 } 226 log.WithError(err). 227 Error("error creating brew formula file") 228 return err 229 } 230 231 log. 232 WithField("fileName", fileName). 233 WithField("branch", branch). 234 WithField("projectID", projectID). 235 WithField("filePath", fileInfo.FilePath). 236 Debug("created brew formula file") 237 return nil 238 } 239 240 log. 241 WithField("fileName", fileName). 242 WithField("ref", ref). 243 WithField("projectID", projectID). 244 Debug("updating brew formula") 245 updateOpts := &gitlab.UpdateFileOptions{ 246 AuthorName: &commitAuthor.Name, 247 AuthorEmail: &commitAuthor.Email, 248 Content: &castedContent, 249 Branch: &branch, 250 CommitMessage: &message, 251 } 252 253 updateFileInfo, res, err := c.client.RepositoryFiles.UpdateFile(projectID, fileName, updateOpts) 254 if err != nil { 255 log := log. 256 WithField("fileName", fileName). 257 WithField("branch", branch). 258 WithField("projectID", projectID) 259 if res != nil { 260 log = log.WithField("statusCode", res.StatusCode) 261 } 262 log.WithError(err). 263 Error("error updating brew formula file") 264 return err 265 } 266 267 log := log. 268 WithField("fileName", fileName). 269 WithField("branch", branch). 270 WithField("projectID", projectID). 271 WithField("filePath", updateFileInfo.FilePath) 272 if res != nil { 273 log = log.WithField("statusCode", res.StatusCode) 274 } 275 log.Debug("updated brew formula file") 276 return nil 277 } 278 279 // CreateRelease creates a new release or updates it by keeping 280 // the release notes if it exists. 281 func (c *gitlabClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) { 282 title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate) 283 if err != nil { 284 return "", err 285 } 286 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 287 if err != nil { 288 return "", err 289 } 290 projectID := gitlabName 291 if ctx.Config.Release.GitLab.Owner != "" { 292 projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID 293 } 294 log. 295 WithField("owner", ctx.Config.Release.GitLab.Owner). 296 WithField("name", gitlabName). 297 WithField("projectID", projectID). 298 Debug("projectID") 299 300 name := title 301 tagName := ctx.Git.CurrentTag 302 release, resp, err := c.client.Releases.GetRelease(projectID, tagName) 303 if err != nil && (resp == nil || (resp.StatusCode != 403 && resp.StatusCode != 404)) { 304 return "", err 305 } 306 307 if resp.StatusCode == 403 || resp.StatusCode == 404 { 308 log.WithError(err).Debug("get release") 309 310 description := body 311 ref := ctx.Git.Commit 312 gitURL := ctx.Git.URL 313 314 log. 315 WithField("name", name). 316 WithField("description", description). 317 WithField("ref", ref). 318 WithField("url", gitURL). 319 Debug("creating release") 320 release, _, err = c.client.Releases.CreateRelease(projectID, &gitlab.CreateReleaseOptions{ 321 Name: &name, 322 Description: &description, 323 Ref: &ref, 324 TagName: &tagName, 325 }) 326 if err != nil { 327 log.WithError(err).Debug("error creating release") 328 return "", err 329 } 330 log.WithField("name", release.Name).Info("release created") 331 } else { 332 desc := body 333 if release != nil { 334 desc = getReleaseNotes(release.Description, body, ctx.Config.Release.ReleaseNotesMode) 335 } 336 337 release, _, err = c.client.Releases.UpdateRelease(projectID, tagName, &gitlab.UpdateReleaseOptions{ 338 Name: &name, 339 Description: &desc, 340 }) 341 if err != nil { 342 log.WithError(err).Debug("error updating release") 343 return "", err 344 } 345 346 log.WithField("name", release.Name).Info("release updated") 347 } 348 349 return tagName, err // gitlab references a tag in a repo by its name 350 } 351 352 func (c *gitlabClient) PublishRelease(_ *context.Context, _ string /* releaseID */) (err error) { 353 // GitLab doesn't support draft releases. So a created release is already published. 354 return nil 355 } 356 357 func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error) { 358 var urlTemplate string 359 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 360 if err != nil { 361 return "", err 362 } 363 downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download) 364 if err != nil { 365 return "", err 366 } 367 368 if ctx.Config.Release.GitLab.Owner != "" { 369 urlTemplate = fmt.Sprintf( 370 "%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", 371 downloadURL, 372 ctx.Config.Release.GitLab.Owner, 373 gitlabName, 374 ) 375 } else { 376 urlTemplate = fmt.Sprintf( 377 "%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", 378 downloadURL, 379 gitlabName, 380 ) 381 } 382 return urlTemplate, nil 383 } 384 385 // Upload uploads a file into a release repository. 386 func (c *gitlabClient) Upload( 387 ctx *context.Context, 388 releaseID string, 389 artifact *artifact.Artifact, 390 file *os.File, 391 ) error { 392 // create new template and apply name field 393 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 394 if err != nil { 395 return err 396 } 397 projectID := gitlabName 398 // check if owner is empty 399 if ctx.Config.Release.GitLab.Owner != "" { 400 projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID 401 } 402 403 var baseLinkURL string 404 var linkURL string 405 if ctx.Config.GitLabURLs.UsePackageRegistry { 406 log.WithField("file", file.Name()).Debug("uploading file as generic package") 407 if _, _, err := c.client.GenericPackages.PublishPackageFile( 408 projectID, 409 ctx.Config.ProjectName, 410 ctx.Version, 411 artifact.Name, 412 file, 413 nil, 414 ); err != nil { 415 return err 416 } 417 418 baseLinkURL, err = c.client.GenericPackages.FormatPackageURL( 419 projectID, 420 ctx.Config.ProjectName, 421 ctx.Version, 422 artifact.Name, 423 ) 424 if err != nil { 425 return err 426 } 427 linkURL = c.client.BaseURL().String() + baseLinkURL 428 } else { 429 log.WithField("file", file.Name()).Debug("uploading file as attachment") 430 projectFile, _, err := c.client.Projects.UploadFile( 431 projectID, 432 file, 433 filepath.Base(file.Name()), 434 nil, 435 ) 436 if err != nil { 437 return err 438 } 439 440 baseLinkURL = projectFile.URL 441 gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download) 442 if err != nil { 443 return fmt.Errorf("templating GitLab Download URL: %w", err) 444 } 445 446 // search for project details based on projectID 447 projectDetails, _, err := c.client.Projects.GetProject(projectID, nil) 448 if err != nil { 449 return err 450 } 451 linkURL = gitlabBaseURL + "/" + projectDetails.PathWithNamespace + baseLinkURL 452 } 453 454 log.WithField("file", file.Name()). 455 WithField("url", baseLinkURL). 456 Debug("uploaded file") 457 458 name := artifact.Name 459 filename := "/" + name 460 releaseLink, _, err := c.client.ReleaseLinks.CreateReleaseLink( 461 projectID, 462 releaseID, 463 &gitlab.CreateReleaseLinkOptions{ 464 Name: &name, 465 URL: &linkURL, 466 FilePath: &filename, 467 }) 468 if err != nil { 469 return RetriableError{err} 470 } 471 472 log.WithField("id", releaseLink.ID). 473 WithField("url", releaseLink.DirectAssetURL). 474 Debug("created release link") 475 476 // for checksums.txt the field is nil, so we initialize it 477 if artifact.Extra == nil { 478 artifact.Extra = make(map[string]interface{}) 479 } 480 481 return nil 482 } 483 484 // getMilestoneByTitle returns a milestone by title. 485 func (c *gitlabClient) getMilestoneByTitle(repo Repo, title string) (*gitlab.Milestone, error) { 486 opts := &gitlab.ListMilestonesOptions{ 487 Title: &title, 488 } 489 490 for { 491 milestones, resp, err := c.client.Milestones.ListMilestones(repo.String(), opts) 492 if err != nil { 493 return nil, err 494 } 495 496 for _, milestone := range milestones { 497 if milestone != nil && milestone.Title == title { 498 return milestone, nil 499 } 500 } 501 502 if resp.NextPage == 0 { 503 break 504 } 505 506 opts.Page = resp.NextPage 507 } 508 509 return nil, nil 510 } 511 512 // checkUseJobToken examines the context and given token, and determines if We should use NewJobClient vs NewClient 513 func checkUseJobToken(ctx context.Context, token string) bool { 514 // The CI_JOB_TOKEN env var is set automatically in all GitLab runners. 515 // If this comes back as empty, we aren't in a functional GitLab runner 516 ciToken := os.Getenv("CI_JOB_TOKEN") 517 if ciToken == "" { 518 return false 519 } 520 521 // We only want to use the JobToken client if we have specified 522 // UseJobToken. Older versions of GitLab don't work with this, so we 523 // want to be specific 524 if ctx.Config.GitLabURLs.UseJobToken { 525 // We may be creating a new client with a non-CI_JOB_TOKEN, for 526 // things like Homebrew publishing. We can't use the 527 // CI_JOB_TOKEN there 528 return token == ciToken 529 } 530 return false 531 }