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