github.com/goreleaser/goreleaser@v1.25.1/internal/pipe/chocolatey/chocolatey.go (about)

     1  package chocolatey
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"text/template"
    11  
    12  	"github.com/caarlos0/log"
    13  	"github.com/goreleaser/goreleaser/internal/artifact"
    14  	"github.com/goreleaser/goreleaser/internal/client"
    15  	"github.com/goreleaser/goreleaser/internal/skips"
    16  	"github.com/goreleaser/goreleaser/internal/tmpl"
    17  	"github.com/goreleaser/goreleaser/pkg/config"
    18  	"github.com/goreleaser/goreleaser/pkg/context"
    19  )
    20  
    21  var errNoWindowsArchive = errors.New("chocolatey requires at least one windows archive")
    22  
    23  // nuget package extension.
    24  const nupkgFormat = "nupkg"
    25  
    26  // custom chocolatey config placed in artifact.
    27  const chocoConfigExtra = "ChocolateyConfig"
    28  
    29  // cmd represents a command executor.
    30  var cmd cmder = stdCmd{}
    31  
    32  // Pipe for chocolatey packaging.
    33  type Pipe struct{}
    34  
    35  func (Pipe) String() string        { return "chocolatey packages" }
    36  func (Pipe) ContinueOnError() bool { return true }
    37  func (Pipe) Skip(ctx *context.Context) bool {
    38  	return skips.Any(ctx, skips.Chocolatey) || len(ctx.Config.Chocolateys) == 0
    39  }
    40  func (Pipe) Dependencies(_ *context.Context) []string { return []string{"choco"} }
    41  
    42  // Default sets the pipe defaults.
    43  func (Pipe) Default(ctx *context.Context) error {
    44  	for i := range ctx.Config.Chocolateys {
    45  		choco := &ctx.Config.Chocolateys[i]
    46  
    47  		if choco.Name == "" {
    48  			choco.Name = ctx.Config.ProjectName
    49  		}
    50  
    51  		if choco.Title == "" {
    52  			choco.Title = ctx.Config.ProjectName
    53  		}
    54  
    55  		if choco.Goamd64 == "" {
    56  			choco.Goamd64 = "v1"
    57  		}
    58  
    59  		if choco.SourceRepo == "" {
    60  			choco.SourceRepo = "https://push.chocolatey.org/"
    61  		}
    62  	}
    63  
    64  	return nil
    65  }
    66  
    67  // Run the pipe.
    68  func (Pipe) Run(ctx *context.Context) error {
    69  	cli, err := client.NewReleaseClient(ctx)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	for _, choco := range ctx.Config.Chocolateys {
    75  		if err := doRun(ctx, cli, choco); err != nil {
    76  			return err
    77  		}
    78  	}
    79  
    80  	return nil
    81  }
    82  
    83  // Publish packages.
    84  func (Pipe) Publish(ctx *context.Context) error {
    85  	artifacts := ctx.Artifacts.Filter(
    86  		artifact.ByType(artifact.PublishableChocolatey),
    87  	).List()
    88  
    89  	for _, artifact := range artifacts {
    90  		if err := doPush(ctx, artifact); err != nil {
    91  			return err
    92  		}
    93  	}
    94  
    95  	return nil
    96  }
    97  
    98  func doRun(ctx *context.Context, cl client.ReleaseURLTemplater, choco config.Chocolatey) error {
    99  	filters := []artifact.Filter{
   100  		artifact.ByGoos("windows"),
   101  		artifact.ByType(artifact.UploadableArchive),
   102  		artifact.Or(
   103  			artifact.And(
   104  				artifact.ByGoarch("amd64"),
   105  				artifact.ByGoamd64(choco.Goamd64),
   106  			),
   107  			artifact.ByGoarch("386"),
   108  		),
   109  	}
   110  
   111  	if len(choco.IDs) > 0 {
   112  		filters = append(filters, artifact.ByIDs(choco.IDs...))
   113  	}
   114  
   115  	artifacts := ctx.Artifacts.
   116  		Filter(artifact.And(filters...)).
   117  		List()
   118  
   119  	if len(artifacts) == 0 {
   120  		return errNoWindowsArchive
   121  	}
   122  
   123  	// folderDir is the directory that then will be compressed to make the
   124  	// chocolatey package.
   125  	folderPath := filepath.Join(ctx.Config.Dist, choco.Name+".choco")
   126  	toolsPath := filepath.Join(folderPath, "tools")
   127  	if err := os.MkdirAll(toolsPath, 0o755); err != nil {
   128  		return err
   129  	}
   130  
   131  	nuspecFile := filepath.Join(folderPath, choco.Name+".nuspec")
   132  	nuspec, err := buildNuspec(ctx, choco)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	if err = os.WriteFile(nuspecFile, nuspec, 0o644); err != nil {
   138  		return err
   139  	}
   140  
   141  	data, err := dataFor(ctx, cl, choco, artifacts)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	script, err := buildTemplate(choco.Name, scriptTemplate, data)
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	scriptFile := filepath.Join(toolsPath, "chocolateyinstall.ps1")
   152  	log.WithField("file", scriptFile).Debug("creating")
   153  	if err = os.WriteFile(scriptFile, script, 0o644); err != nil {
   154  		return err
   155  	}
   156  
   157  	log.WithField("nuspec", nuspecFile).Info("packing")
   158  	out, err := cmd.Exec(ctx, "choco", "pack", nuspecFile, "--out", ctx.Config.Dist)
   159  	if err != nil {
   160  		return fmt.Errorf("failed to generate chocolatey package: %w: %s", err, string(out))
   161  	}
   162  
   163  	if choco.SkipPublish {
   164  		return nil
   165  	}
   166  
   167  	pkgFile := fmt.Sprintf("%s.%s.%s", choco.Name, ctx.Version, nupkgFormat)
   168  
   169  	ctx.Artifacts.Add(&artifact.Artifact{
   170  		Type: artifact.PublishableChocolatey,
   171  		Path: filepath.Join(ctx.Config.Dist, pkgFile),
   172  		Name: pkgFile,
   173  		Extra: map[string]interface{}{
   174  			artifact.ExtraFormat: nupkgFormat,
   175  			chocoConfigExtra:     choco,
   176  		},
   177  	})
   178  
   179  	return nil
   180  }
   181  
   182  func doPush(ctx *context.Context, art *artifact.Artifact) error {
   183  	choco, err := artifact.Extra[config.Chocolatey](*art, chocoConfigExtra)
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	key, err := tmpl.New(ctx).Apply(choco.APIKey)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	log := log.WithField("name", choco.Name)
   194  	if key == "" {
   195  		log.Warn("skip pushing: no api key")
   196  		return nil
   197  	}
   198  
   199  	log.Info("pushing package")
   200  
   201  	args := []string{
   202  		"push",
   203  		"--source",
   204  		choco.SourceRepo,
   205  		"--api-key",
   206  		key,
   207  		filepath.Clean(art.Path),
   208  	}
   209  
   210  	if out, err := cmd.Exec(ctx, "choco", args...); err != nil {
   211  		return fmt.Errorf("failed to push chocolatey package: %w: %s", err, string(out))
   212  	}
   213  
   214  	log.Info("package sent")
   215  
   216  	return nil
   217  }
   218  
   219  func buildNuspec(ctx *context.Context, choco config.Chocolatey) ([]byte, error) {
   220  	tpl := tmpl.New(ctx)
   221  
   222  	if err := tpl.ApplyAll(
   223  		&choco.Summary,
   224  		&choco.Description,
   225  		&choco.ReleaseNotes,
   226  	); err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	m := &Nuspec{
   231  		Xmlns: schema,
   232  		Metadata: Metadata{
   233  			ID:                       choco.Name,
   234  			Version:                  ctx.Version,
   235  			PackageSourceURL:         choco.PackageSourceURL,
   236  			Owners:                   choco.Owners,
   237  			Title:                    choco.Title,
   238  			Authors:                  choco.Authors,
   239  			ProjectURL:               choco.ProjectURL,
   240  			IconURL:                  choco.IconURL,
   241  			Copyright:                choco.Copyright,
   242  			LicenseURL:               choco.LicenseURL,
   243  			RequireLicenseAcceptance: choco.RequireLicenseAcceptance,
   244  			ProjectSourceURL:         choco.ProjectSourceURL,
   245  			DocsURL:                  choco.DocsURL,
   246  			BugTrackerURL:            choco.BugTrackerURL,
   247  			Tags:                     choco.Tags,
   248  			Summary:                  choco.Summary,
   249  			Description:              choco.Description,
   250  			ReleaseNotes:             choco.ReleaseNotes,
   251  		},
   252  		Files: Files{File: []File{
   253  			{Source: "tools\\**", Target: "tools"},
   254  		}},
   255  	}
   256  
   257  	deps := make([]Dependency, len(choco.Dependencies))
   258  	for i, dep := range choco.Dependencies {
   259  		deps[i] = Dependency{ID: dep.ID, Version: dep.Version}
   260  	}
   261  
   262  	if len(deps) > 0 {
   263  		m.Metadata.Dependencies = &Dependencies{Dependency: deps}
   264  	}
   265  
   266  	return m.Bytes()
   267  }
   268  
   269  func buildTemplate(name string, text string, data templateData) ([]byte, error) {
   270  	tp, err := template.New(name).Parse(text)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	var out bytes.Buffer
   276  	if err = tp.Execute(&out, data); err != nil {
   277  		return nil, err
   278  	}
   279  
   280  	return out.Bytes(), nil
   281  }
   282  
   283  func dataFor(ctx *context.Context, cl client.ReleaseURLTemplater, choco config.Chocolatey, artifacts []*artifact.Artifact) (templateData, error) {
   284  	result := templateData{}
   285  
   286  	if choco.URLTemplate == "" {
   287  		url, err := cl.ReleaseURLTemplate(ctx)
   288  		if err != nil {
   289  			return result, err
   290  		}
   291  
   292  		choco.URLTemplate = url
   293  	}
   294  
   295  	for _, artifact := range artifacts {
   296  		sum, err := artifact.Checksum("sha256")
   297  		if err != nil {
   298  			return result, err
   299  		}
   300  
   301  		url, err := tmpl.New(ctx).WithArtifact(artifact).Apply(choco.URLTemplate)
   302  		if err != nil {
   303  			return result, err
   304  		}
   305  
   306  		pkg := releasePackage{
   307  			DownloadURL: url,
   308  			Checksum:    sum,
   309  			Arch:        artifact.Goarch,
   310  		}
   311  
   312  		result.Packages = append(result.Packages, pkg)
   313  	}
   314  
   315  	return result, nil
   316  }
   317  
   318  // cmder is a special interface to execute external commands.
   319  //
   320  // The intention is to be used to wrap the standard exec and provide the
   321  // ability to create a fake one for testing.
   322  type cmder interface {
   323  	// Exec executes a command.
   324  	Exec(*context.Context, string, ...string) ([]byte, error)
   325  }
   326  
   327  // stdCmd uses the standard golang exec.
   328  type stdCmd struct{}
   329  
   330  var _ cmder = &stdCmd{}
   331  
   332  func (stdCmd) Exec(ctx *context.Context, name string, args ...string) ([]byte, error) {
   333  	log.WithField("cmd", name).
   334  		WithField("args", args).
   335  		Debug("running")
   336  	return exec.CommandContext(ctx, name, args...).CombinedOutput()
   337  }