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  }