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