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