github.com/ahmet2mir/goreleaser@v0.180.3-0.20210927151101-8e5ee5a9b8c5/internal/client/gitlab.go (about) 1 package client 2 3 import ( 4 "crypto/tls" 5 "fmt" 6 "net/http" 7 "os" 8 9 "github.com/apex/log" 10 "github.com/goreleaser/goreleaser/internal/artifact" 11 "github.com/goreleaser/goreleaser/internal/tmpl" 12 "github.com/goreleaser/goreleaser/pkg/config" 13 "github.com/goreleaser/goreleaser/pkg/context" 14 "github.com/xanzy/go-gitlab" 15 ) 16 17 const DefaultGitLabDownloadURL = "https://gitlab.com" 18 19 type gitlabClient struct { 20 client *gitlab.Client 21 } 22 23 // NewGitLab returns a gitlab client implementation. 24 func NewGitLab(ctx *context.Context, token string) (Client, error) { 25 transport := &http.Transport{ 26 Proxy: http.ProxyFromEnvironment, 27 TLSClientConfig: &tls.Config{ 28 // nolint: gosec 29 InsecureSkipVerify: ctx.Config.GitLabURLs.SkipTLSVerify, 30 }, 31 } 32 options := []gitlab.ClientOptionFunc{ 33 gitlab.WithHTTPClient(&http.Client{ 34 Transport: transport, 35 }), 36 } 37 if ctx.Config.GitLabURLs.API != "" { 38 apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.API) 39 if err != nil { 40 return nil, fmt.Errorf("templating GitLab API URL: %w", err) 41 } 42 43 options = append(options, gitlab.WithBaseURL(apiURL)) 44 } 45 client, err := gitlab.NewClient(token, options...) 46 if err != nil { 47 return &gitlabClient{}, err 48 } 49 return &gitlabClient{client: client}, nil 50 } 51 52 // CloseMilestone closes a given milestone. 53 func (c *gitlabClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error { 54 milestone, err := c.getMilestoneByTitle(repo, title) 55 if err != nil { 56 return err 57 } 58 59 if milestone == nil { 60 return ErrNoMilestoneFound{Title: title} 61 } 62 63 closeStateEvent := "close" 64 65 opts := &gitlab.UpdateMilestoneOptions{ 66 Description: &milestone.Description, 67 DueDate: milestone.DueDate, 68 StartDate: milestone.StartDate, 69 StateEvent: &closeStateEvent, 70 Title: &milestone.Title, 71 } 72 73 _, _, err = c.client.Milestones.UpdateMilestone( 74 repo.String(), 75 milestone.ID, 76 opts, 77 ) 78 79 return err 80 } 81 82 // CreateFile gets a file in the repository at a given path 83 // and updates if it exists or creates it for later pipes in the pipeline. 84 func (c *gitlabClient) CreateFile( 85 ctx *context.Context, 86 commitAuthor config.CommitAuthor, 87 repo Repo, 88 content []byte, // the content of the formula.rb 89 path, // the path to the formula.rb 90 message string, // the commit msg 91 ) error { 92 fileName := path 93 // we assume having the formula in the master branch only 94 ref := "master" 95 branch := "master" 96 opts := &gitlab.GetFileOptions{Ref: &ref} 97 castedContent := string(content) 98 projectID := repo.Owner + "/" + repo.Name 99 100 log.WithFields(log.Fields{ 101 "owner": repo.Owner, 102 "name": repo.Name, 103 }).Debug("projectID at brew") 104 105 _, res, err := c.client.RepositoryFiles.GetFile(projectID, fileName, opts) 106 if err != nil && (res == nil || res.StatusCode != 404) { 107 log.WithFields(log.Fields{ 108 "fileName": fileName, 109 "ref": ref, 110 "projectID": projectID, 111 "statusCode": res.StatusCode, 112 "err": err.Error(), 113 }).Error("error getting file for brew formula") 114 return err 115 } 116 117 log.WithFields(log.Fields{ 118 "fileName": fileName, 119 "branch": branch, 120 "projectID": projectID, 121 }).Debug("found already existing brew formula file") 122 123 if res.StatusCode == 404 { 124 log.WithFields(log.Fields{ 125 "fileName": fileName, 126 "ref": ref, 127 "projectID": projectID, 128 }).Debug("creating brew formula") 129 createOpts := &gitlab.CreateFileOptions{ 130 AuthorName: &commitAuthor.Name, 131 AuthorEmail: &commitAuthor.Email, 132 Content: &castedContent, 133 Branch: &branch, 134 CommitMessage: &message, 135 } 136 fileInfo, res, err := c.client.RepositoryFiles.CreateFile(projectID, fileName, createOpts) 137 if err != nil { 138 log.WithFields(log.Fields{ 139 "fileName": fileName, 140 "branch": branch, 141 "projectID": projectID, 142 "statusCode": res.StatusCode, 143 "err": err.Error(), 144 }).Error("error creating brew formula file") 145 return err 146 } 147 148 log.WithFields(log.Fields{ 149 "fileName": fileName, 150 "branch": branch, 151 "projectID": projectID, 152 "filePath": fileInfo.FilePath, 153 }).Debug("created brew formula file") 154 return nil 155 } 156 157 log.WithFields(log.Fields{ 158 "fileName": fileName, 159 "ref": ref, 160 "projectID": projectID, 161 }).Debug("updating brew formula") 162 updateOpts := &gitlab.UpdateFileOptions{ 163 AuthorName: &commitAuthor.Name, 164 AuthorEmail: &commitAuthor.Email, 165 Content: &castedContent, 166 Branch: &branch, 167 CommitMessage: &message, 168 } 169 170 updateFileInfo, res, err := c.client.RepositoryFiles.UpdateFile(projectID, fileName, updateOpts) 171 if err != nil { 172 log.WithFields(log.Fields{ 173 "fileName": fileName, 174 "branch": branch, 175 "projectID": projectID, 176 "statusCode": res.StatusCode, 177 "err": err.Error(), 178 }).Error("error updating brew formula file") 179 return err 180 } 181 182 log.WithFields(log.Fields{ 183 "fileName": fileName, 184 "branch": branch, 185 "projectID": projectID, 186 "filePath": updateFileInfo.FilePath, 187 "statusCode": res.StatusCode, 188 }).Debug("updated brew formula file") 189 return nil 190 } 191 192 // CreateRelease creates a new release or updates it by keeping 193 // the release notes if it exists. 194 func (c *gitlabClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) { 195 title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate) 196 if err != nil { 197 return "", err 198 } 199 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 200 if err != nil { 201 return "", err 202 } 203 projectID := gitlabName 204 if ctx.Config.Release.GitLab.Owner != "" { 205 projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID 206 } 207 log.WithFields(log.Fields{ 208 "owner": ctx.Config.Release.GitLab.Owner, 209 "name": gitlabName, 210 "projectID": projectID, 211 }).Debug("projectID") 212 213 name := title 214 tagName := ctx.Git.CurrentTag 215 release, resp, err := c.client.Releases.GetRelease(projectID, tagName) 216 if err != nil && (resp == nil || (resp.StatusCode != 403 && resp.StatusCode != 404)) { 217 return "", err 218 } 219 220 if resp.StatusCode == 403 || resp.StatusCode == 404 { 221 log.WithFields(log.Fields{ 222 "err": err.Error(), 223 }).Debug("get release") 224 225 description := body 226 ref := ctx.Git.Commit 227 gitURL := ctx.Git.URL 228 229 log.WithFields(log.Fields{ 230 "name": name, 231 "description": description, 232 "ref": ref, 233 "url": gitURL, 234 }).Debug("creating release") 235 release, _, err = c.client.Releases.CreateRelease(projectID, &gitlab.CreateReleaseOptions{ 236 Name: &name, 237 Description: &description, 238 Ref: &ref, 239 TagName: &tagName, 240 }) 241 242 if err != nil { 243 log.WithFields(log.Fields{ 244 "err": err.Error(), 245 }).Debug("error create release") 246 return "", err 247 } 248 log.WithField("name", release.Name).Info("release created") 249 } else { 250 desc := body 251 if release != nil && release.DescriptionHTML != "" { 252 desc = release.DescriptionHTML 253 } 254 255 release, _, err = c.client.Releases.UpdateRelease(projectID, tagName, &gitlab.UpdateReleaseOptions{ 256 Name: &name, 257 Description: &desc, 258 }) 259 if err != nil { 260 log.WithFields(log.Fields{ 261 "err": err.Error(), 262 }).Debug("error update release") 263 return "", err 264 } 265 266 log.WithField("name", release.Name).Info("release updated") 267 } 268 269 return tagName, err // gitlab references a tag in a repo by its name 270 } 271 272 func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error) { 273 var urlTemplate string 274 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 275 if err != nil { 276 return "", err 277 } 278 downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download) 279 if err != nil { 280 return "", err 281 } 282 283 if ctx.Config.Release.GitLab.Owner != "" { 284 urlTemplate = fmt.Sprintf( 285 "%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", 286 downloadURL, 287 ctx.Config.Release.GitLab.Owner, 288 gitlabName, 289 ) 290 } else { 291 urlTemplate = fmt.Sprintf( 292 "%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", 293 downloadURL, 294 gitlabName, 295 ) 296 } 297 return urlTemplate, nil 298 } 299 300 // Upload uploads a file into a release repository. 301 func (c *gitlabClient) Upload( 302 ctx *context.Context, 303 releaseID string, 304 artifact *artifact.Artifact, 305 file *os.File, 306 ) error { 307 // create new template and apply name field 308 gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name) 309 if err != nil { 310 return err 311 } 312 projectID := gitlabName 313 // check if owner is empty 314 if ctx.Config.Release.GitLab.Owner != "" { 315 projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID 316 } 317 318 log.WithField("file", file.Name()).Debug("uploading file") 319 projectFile, _, err := c.client.Projects.UploadFile( 320 projectID, 321 file.Name(), 322 nil, 323 ) 324 if err != nil { 325 return err 326 } 327 328 log.WithFields(log.Fields{ 329 "file": file.Name(), 330 "url": projectFile.URL, 331 }).Debug("uploaded file") 332 333 // search for project details based on projectID 334 projectDetails, _, err := c.client.Projects.GetProject(projectID, nil) 335 if err != nil { 336 return err 337 } 338 339 gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download) 340 if err != nil { 341 return fmt.Errorf("templating GitLab Download URL: %w", err) 342 } 343 344 linkURL := gitlabBaseURL + "/" + projectDetails.PathWithNamespace + projectFile.URL 345 name := artifact.Name 346 filename := "/" + name 347 releaseLink, _, err := c.client.ReleaseLinks.CreateReleaseLink( 348 projectID, 349 releaseID, 350 &gitlab.CreateReleaseLinkOptions{ 351 Name: &name, 352 URL: &linkURL, 353 FilePath: &filename, 354 }) 355 if err != nil { 356 return RetriableError{err} 357 } 358 359 log.WithFields(log.Fields{ 360 "id": releaseLink.ID, 361 "url": releaseLink.DirectAssetURL, 362 }).Debug("created release link") 363 364 // for checksums.txt the field is nil, so we initialize it 365 if artifact.Extra == nil { 366 artifact.Extra = make(map[string]interface{}) 367 } 368 369 return nil 370 } 371 372 // getMilestoneByTitle returns a milestone by title. 373 func (c *gitlabClient) getMilestoneByTitle(repo Repo, title string) (*gitlab.Milestone, error) { 374 opts := &gitlab.ListMilestonesOptions{ 375 Title: &title, 376 } 377 378 for { 379 milestones, resp, err := c.client.Milestones.ListMilestones(repo.String(), opts) 380 if err != nil { 381 return nil, err 382 } 383 384 for _, milestone := range milestones { 385 if milestone != nil && milestone.Title == title { 386 return milestone, nil 387 } 388 } 389 390 if resp.NextPage == 0 { 391 break 392 } 393 394 opts.Page = resp.NextPage 395 } 396 397 return nil, nil 398 }