github.com/windmeup/goreleaser@v1.21.95/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/windmeup/goreleaser/internal/artifact"
    13  	"github.com/windmeup/goreleaser/internal/tmpl"
    14  	"github.com/windmeup/goreleaser/pkg/config"
    15  	"github.com/windmeup/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.
    92  			WithField("projectID", projectID).
    93  			WithField("statusCode", res.StatusCode).
    94  			WithError(err).
    95  			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  	projectID := repo.String()
   143  
   144  	// Use the project default branch if we can get it...otherwise, just use
   145  	// 'master'
   146  	var branch, ref string
   147  	var err error
   148  	// Use the branch if given one
   149  	if repo.Branch != "" {
   150  		branch = repo.Branch
   151  	} else {
   152  		// Try to get the default branch from the Git provider
   153  		branch, err = c.getDefaultBranch(ctx, repo)
   154  		if err != nil {
   155  			// Fall back to 'master' 😭
   156  			log.
   157  				WithField("fileName", fileName).
   158  				WithField("projectID", repo.String()).
   159  				WithField("requestedBranch", branch).
   160  				WithError(err).
   161  				Warn("error checking for default branch, using master")
   162  			ref = "master"
   163  			branch = "master"
   164  		}
   165  	}
   166  	ref = branch
   167  	opts := &gitlab.GetFileOptions{Ref: &ref}
   168  	castedContent := string(content)
   169  
   170  	log.
   171  		WithField("owner", repo.Owner).
   172  		WithField("name", repo.Name).
   173  		WithField("ref", ref).
   174  		WithField("branch", branch).
   175  		Debug("projectID at brew")
   176  
   177  	log.
   178  		WithField("repository", repo.String()).
   179  		WithField("name", repo.Name).
   180  		WithField("name", repo.Name).
   181  		Info("pushing")
   182  
   183  	_, res, err := c.client.RepositoryFiles.GetFile(repo.String(), fileName, opts)
   184  	if err != nil && (res == nil || res.StatusCode != 404) {
   185  		log.
   186  			WithField("fileName", fileName).
   187  			WithField("ref", ref).
   188  			WithField("projectID", projectID).
   189  			WithField("statusCode", res.StatusCode).
   190  			WithError(err).
   191  			Error("error getting file for brew formula")
   192  		return err
   193  	}
   194  
   195  	log.
   196  		WithField("fileName", fileName).
   197  		WithField("branch", branch).
   198  		WithField("projectID", projectID).
   199  		Debug("found already existing brew formula file")
   200  
   201  	if res.StatusCode == 404 {
   202  		log.
   203  			WithField("fileName", fileName).
   204  			WithField("ref", ref).
   205  			WithField("projectID", projectID).
   206  			Debug("creating brew formula")
   207  		createOpts := &gitlab.CreateFileOptions{
   208  			AuthorName:    &commitAuthor.Name,
   209  			AuthorEmail:   &commitAuthor.Email,
   210  			Content:       &castedContent,
   211  			Branch:        &branch,
   212  			CommitMessage: &message,
   213  		}
   214  		fileInfo, res, err := c.client.RepositoryFiles.CreateFile(projectID, fileName, createOpts)
   215  		if err != nil {
   216  			log.
   217  				WithField("fileName", fileName).
   218  				WithField("branch", branch).
   219  				WithField("projectID", projectID).
   220  				WithField("statusCode", res.StatusCode).
   221  				WithError(err).
   222  				Error("error creating brew formula file")
   223  			return err
   224  		}
   225  
   226  		log.
   227  			WithField("fileName", fileName).
   228  			WithField("branch", branch).
   229  			WithField("projectID", projectID).
   230  			WithField("filePath", fileInfo.FilePath).
   231  			Debug("created brew formula file")
   232  		return nil
   233  	}
   234  
   235  	log.
   236  		WithField("fileName", fileName).
   237  		WithField("ref", ref).
   238  		WithField("projectID", projectID).
   239  		Debug("updating brew formula")
   240  	updateOpts := &gitlab.UpdateFileOptions{
   241  		AuthorName:    &commitAuthor.Name,
   242  		AuthorEmail:   &commitAuthor.Email,
   243  		Content:       &castedContent,
   244  		Branch:        &branch,
   245  		CommitMessage: &message,
   246  	}
   247  
   248  	updateFileInfo, res, err := c.client.RepositoryFiles.UpdateFile(projectID, fileName, updateOpts)
   249  	if err != nil {
   250  		log.
   251  			WithField("fileName", fileName).
   252  			WithField("branch", branch).
   253  			WithField("projectID", projectID).
   254  			WithField("statusCode", res.StatusCode).
   255  			WithError(err).
   256  			Error("error updating brew formula file")
   257  		return err
   258  	}
   259  
   260  	log.
   261  		WithField("fileName", fileName).
   262  		WithField("branch", branch).
   263  		WithField("projectID", projectID).
   264  		WithField("filePath", updateFileInfo.FilePath).
   265  		WithField("statusCode", res.StatusCode).
   266  		Debug("updated brew formula file")
   267  	return nil
   268  }
   269  
   270  // CreateRelease creates a new release or updates it by keeping
   271  // the release notes if it exists.
   272  func (c *gitlabClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) {
   273  	title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate)
   274  	if err != nil {
   275  		return "", err
   276  	}
   277  	gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
   278  	if err != nil {
   279  		return "", err
   280  	}
   281  	projectID := gitlabName
   282  	if ctx.Config.Release.GitLab.Owner != "" {
   283  		projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID
   284  	}
   285  	log.
   286  		WithField("owner", ctx.Config.Release.GitLab.Owner).
   287  		WithField("name", gitlabName).
   288  		WithField("projectID", projectID).
   289  		Debug("projectID")
   290  
   291  	name := title
   292  	tagName := ctx.Git.CurrentTag
   293  	release, resp, err := c.client.Releases.GetRelease(projectID, tagName)
   294  	if err != nil && (resp == nil || (resp.StatusCode != 403 && resp.StatusCode != 404)) {
   295  		return "", err
   296  	}
   297  
   298  	if resp.StatusCode == 403 || resp.StatusCode == 404 {
   299  		log.WithError(err).Debug("get release")
   300  
   301  		description := body
   302  		ref := ctx.Git.Commit
   303  		gitURL := ctx.Git.URL
   304  
   305  		log.
   306  			WithField("name", name).
   307  			WithField("description", description).
   308  			WithField("ref", ref).
   309  			WithField("url", gitURL).
   310  			Debug("creating release")
   311  		release, _, err = c.client.Releases.CreateRelease(projectID, &gitlab.CreateReleaseOptions{
   312  			Name:        &name,
   313  			Description: &description,
   314  			Ref:         &ref,
   315  			TagName:     &tagName,
   316  		})
   317  
   318  		if err != nil {
   319  			log.WithError(err).Debug("error creating release")
   320  			return "", err
   321  		}
   322  		log.WithField("name", release.Name).Info("release created")
   323  	} else {
   324  		desc := body
   325  		if release != nil {
   326  			desc = getReleaseNotes(release.Description, body, ctx.Config.Release.ReleaseNotesMode)
   327  		}
   328  
   329  		release, _, err = c.client.Releases.UpdateRelease(projectID, tagName, &gitlab.UpdateReleaseOptions{
   330  			Name:        &name,
   331  			Description: &desc,
   332  		})
   333  		if err != nil {
   334  			log.WithError(err).Debug("error updating release")
   335  			return "", err
   336  		}
   337  
   338  		log.WithField("name", release.Name).Info("release updated")
   339  	}
   340  
   341  	return tagName, err // gitlab references a tag in a repo by its name
   342  }
   343  
   344  func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
   345  	var urlTemplate string
   346  	gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
   347  	if err != nil {
   348  		return "", err
   349  	}
   350  	downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
   351  	if err != nil {
   352  		return "", err
   353  	}
   354  
   355  	if ctx.Config.Release.GitLab.Owner != "" {
   356  		urlTemplate = fmt.Sprintf(
   357  			"%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
   358  			downloadURL,
   359  			ctx.Config.Release.GitLab.Owner,
   360  			gitlabName,
   361  		)
   362  	} else {
   363  		urlTemplate = fmt.Sprintf(
   364  			"%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
   365  			downloadURL,
   366  			gitlabName,
   367  		)
   368  	}
   369  	return urlTemplate, nil
   370  }
   371  
   372  // Upload uploads a file into a release repository.
   373  func (c *gitlabClient) Upload(
   374  	ctx *context.Context,
   375  	releaseID string,
   376  	artifact *artifact.Artifact,
   377  	file *os.File,
   378  ) error {
   379  	// create new template and apply name field
   380  	gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
   381  	if err != nil {
   382  		return err
   383  	}
   384  	projectID := gitlabName
   385  	// check if owner is empty
   386  	if ctx.Config.Release.GitLab.Owner != "" {
   387  		projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID
   388  	}
   389  
   390  	var baseLinkURL string
   391  	var linkURL string
   392  	if ctx.Config.GitLabURLs.UsePackageRegistry {
   393  		log.WithField("file", file.Name()).Debug("uploading file as generic package")
   394  		if _, _, err := c.client.GenericPackages.PublishPackageFile(
   395  			projectID,
   396  			ctx.Config.ProjectName,
   397  			ctx.Version,
   398  			artifact.Name,
   399  			file,
   400  			nil,
   401  		); err != nil {
   402  			return err
   403  		}
   404  
   405  		baseLinkURL, err = c.client.GenericPackages.FormatPackageURL(
   406  			projectID,
   407  			ctx.Config.ProjectName,
   408  			ctx.Version,
   409  			artifact.Name,
   410  		)
   411  		if err != nil {
   412  			return err
   413  		}
   414  		linkURL = c.client.BaseURL().String() + baseLinkURL
   415  	} else {
   416  		log.WithField("file", file.Name()).Debug("uploading file as attachment")
   417  		projectFile, _, err := c.client.Projects.UploadFile(
   418  			projectID,
   419  			file,
   420  			filepath.Base(file.Name()),
   421  			nil,
   422  		)
   423  		if err != nil {
   424  			return err
   425  		}
   426  
   427  		baseLinkURL = projectFile.URL
   428  		gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
   429  		if err != nil {
   430  			return fmt.Errorf("templating GitLab Download URL: %w", err)
   431  		}
   432  
   433  		// search for project details based on projectID
   434  		projectDetails, _, err := c.client.Projects.GetProject(projectID, nil)
   435  		if err != nil {
   436  			return err
   437  		}
   438  		linkURL = gitlabBaseURL + "/" + projectDetails.PathWithNamespace + baseLinkURL
   439  	}
   440  
   441  	log.WithField("file", file.Name()).
   442  		WithField("url", baseLinkURL).
   443  		Debug("uploaded file")
   444  
   445  	name := artifact.Name
   446  	filename := "/" + name
   447  	releaseLink, _, err := c.client.ReleaseLinks.CreateReleaseLink(
   448  		projectID,
   449  		releaseID,
   450  		&gitlab.CreateReleaseLinkOptions{
   451  			Name:     &name,
   452  			URL:      &linkURL,
   453  			FilePath: &filename,
   454  		})
   455  	if err != nil {
   456  		return RetriableError{err}
   457  	}
   458  
   459  	log.WithField("id", releaseLink.ID).
   460  		WithField("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  }