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