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:   &current,
    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  }