diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index c917c13c228..b351339b389 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -66,6 +66,8 @@ const (
ScoopManifest
// SBOM is a Software Bill of Materials file.
SBOM
+ // PublishableChocolatey is a chocolatey package yet to be published.
+ PublishableChocolatey
// Header is a C header file, generated for CGo library builds.
Header
// CArchive is a C static library, generated via a CGo build with buildmode=c-archive.
@@ -110,6 +112,8 @@ func (t Type) String() string {
return "PKGBUILD"
case SrcInfo:
return "SRCINFO"
+ case PublishableChocolatey:
+ return "Chocolatey"
case Header:
return "C Header"
case CArchive:
diff --git a/internal/pipe/chocolatey/chocolatey.go b/internal/pipe/chocolatey/chocolatey.go
new file mode 100644
index 00000000000..bf0a47cce15
--- /dev/null
+++ b/internal/pipe/chocolatey/chocolatey.go
@@ -0,0 +1,340 @@
+package chocolatey
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "text/template"
+
+ "github.com/caarlos0/log"
+ "github.com/goreleaser/goreleaser/internal/artifact"
+ "github.com/goreleaser/goreleaser/internal/client"
+ "github.com/goreleaser/goreleaser/internal/pipe"
+ "github.com/goreleaser/goreleaser/internal/tmpl"
+ "github.com/goreleaser/goreleaser/pkg/config"
+ "github.com/goreleaser/goreleaser/pkg/context"
+)
+
+// nuget package extension.
+const nupkgFormat = "nupkg"
+
+// custom chocolatey config placed in artifact.
+const chocoConfigExtra = "ChocolateyConfig"
+
+// cmd represents a command executor.
+var cmd cmder = stdCmd{}
+
+// Pipe for chocolatey packaging.
+type Pipe struct{}
+
+func (Pipe) String() string { return "chocolatey packages" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Chocolateys) == 0 }
+
+// Default sets the pipe defaults.
+func (Pipe) Default(ctx *context.Context) error {
+ for i := range ctx.Config.Chocolateys {
+ choco := &ctx.Config.Chocolateys[i]
+
+ if choco.Name == "" {
+ choco.Name = ctx.Config.ProjectName
+ }
+
+ if choco.Title == "" {
+ choco.Title = ctx.Config.ProjectName
+ }
+
+ if choco.Goamd64 == "" {
+ choco.Goamd64 = "v1"
+ }
+
+ if choco.SourceRepo == "" {
+ choco.SourceRepo = "https://push.chocolatey.org/"
+ }
+ }
+
+ return nil
+}
+
+// Run the pipe.
+func (Pipe) Run(ctx *context.Context) error {
+ client, err := client.New(ctx)
+ if err != nil {
+ return err
+ }
+
+ for _, choco := range ctx.Config.Chocolateys {
+ if err := doRun(ctx, client, choco); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Publish packages.
+func (Pipe) Publish(ctx *context.Context) error {
+ if ctx.SkipPublish {
+ return pipe.ErrSkipPublishEnabled
+ }
+
+ artifacts := ctx.Artifacts.Filter(
+ artifact.ByType(artifact.PublishableChocolatey),
+ ).List()
+
+ for _, artifact := range artifacts {
+ if err := doPush(ctx, artifact); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func doRun(ctx *context.Context, cl client.Client, choco config.Chocolatey) error {
+ filters := []artifact.Filter{
+ artifact.ByGoos("windows"),
+ artifact.ByType(artifact.UploadableArchive),
+ artifact.Or(
+ artifact.And(
+ artifact.ByGoarch("amd64"),
+ artifact.ByGoamd64(choco.Goamd64),
+ ),
+ artifact.ByGoarch("386"),
+ ),
+ }
+
+ if len(choco.IDs) > 0 {
+ filters = append(filters, artifact.ByIDs(choco.IDs...))
+ }
+
+ artifacts := ctx.Artifacts.
+ Filter(artifact.And(filters...)).
+ List()
+
+ if len(artifacts) == 0 {
+ return errors.New("chocolatey requires a windows build and archive")
+ }
+
+ // folderDir is the directory that then will be compressed to make the
+ // chocolatey package.
+ folderPath := filepath.Join(ctx.Config.Dist, choco.Name+".choco")
+ toolsPath := filepath.Join(folderPath, "tools")
+ if err := os.MkdirAll(toolsPath, 0o755); err != nil {
+ return err
+ }
+
+ nuspecFile := filepath.Join(folderPath, choco.Name+".nuspec")
+ nuspec, err := buildNuspec(ctx, choco)
+ if err != nil {
+ return err
+ }
+
+ if err = os.WriteFile(nuspecFile, nuspec, 0o644); err != nil {
+ return err
+ }
+
+ data, err := dataFor(ctx, cl, choco, artifacts)
+ if err != nil {
+ return err
+ }
+
+ script, err := buildTemplate(choco.Name, scriptTemplate, data)
+ if err != nil {
+ return err
+ }
+
+ scriptFile := filepath.Join(toolsPath, "chocolateyinstall.ps1")
+ log.WithField("file", scriptFile).Debug("creating")
+ if err = os.WriteFile(scriptFile, script, 0o644); err != nil {
+ return err
+ }
+
+ log.WithField("nuspec", nuspecFile).Info("packing")
+ out, err := cmd.Exec(ctx, "choco", "pack", nuspecFile, "--out", ctx.Config.Dist)
+ if err != nil {
+ return fmt.Errorf("failed to generate chocolatey package: %w: %s", err, string(out))
+ }
+
+ if choco.SkipPublish {
+ return nil
+ }
+
+ pkgFile := fmt.Sprintf("%s.%s.%s", choco.Name, ctx.Version, nupkgFormat)
+
+ ctx.Artifacts.Add(&artifact.Artifact{
+ Type: artifact.PublishableChocolatey,
+ Path: filepath.Join(ctx.Config.Dist, pkgFile),
+ Name: pkgFile,
+ Extra: map[string]interface{}{
+ artifact.ExtraFormat: nupkgFormat,
+ chocoConfigExtra: choco,
+ },
+ })
+
+ return nil
+}
+
+func doPush(ctx *context.Context, art *artifact.Artifact) error {
+ choco, err := artifact.Extra[config.Chocolatey](*art, chocoConfigExtra)
+ if err != nil {
+ return err
+ }
+
+ key, err := tmpl.New(ctx).Apply(choco.APIKey)
+ if err != nil {
+ return err
+ }
+
+ log := log.WithField("name", choco.Name)
+ if key == "" {
+ log.Warn("skip pushing: no api key")
+ return nil
+ }
+
+ log.Info("pushing package")
+
+ args := []string{
+ "push",
+ "--source",
+ choco.SourceRepo,
+ "--api-key",
+ key,
+ art.Path,
+ }
+
+ if out, err := cmd.Exec(ctx, "choco", args...); err != nil {
+ return fmt.Errorf("failed to push chocolatey package: %w: %s", err, string(out))
+ }
+
+ log.Info("package sent")
+
+ return nil
+}
+
+func buildNuspec(ctx *context.Context, choco config.Chocolatey) ([]byte, error) {
+ tpl := tmpl.New(ctx)
+ summary, err := tpl.Apply(choco.Summary)
+ if err != nil {
+ return nil, err
+ }
+
+ description, err := tpl.Apply(choco.Description)
+ if err != nil {
+ return nil, err
+ }
+
+ releaseNotes, err := tpl.Apply(choco.ReleaseNotes)
+ if err != nil {
+ return nil, err
+ }
+
+ m := &Nuspec{
+ Xmlns: schema,
+ Metadata: Metadata{
+ ID: choco.Name,
+ Version: ctx.Version,
+ PackageSourceURL: choco.PackageSourceURL,
+ Owners: choco.Owners,
+ Title: choco.Title,
+ Authors: choco.Authors,
+ ProjectURL: choco.ProjectURL,
+ IconURL: choco.IconURL,
+ Copyright: choco.Copyright,
+ LicenseURL: choco.LicenseURL,
+ RequireLicenseAcceptance: choco.RequireLicenseAcceptance,
+ ProjectSourceURL: choco.ProjectSourceURL,
+ DocsURL: choco.DocsURL,
+ BugTrackerURL: choco.BugTrackerURL,
+ Tags: choco.Tags,
+ Summary: summary,
+ Description: description,
+ ReleaseNotes: releaseNotes,
+ },
+ Files: Files{File: []File{
+ {Source: "tools\\**", Target: "tools"},
+ }},
+ }
+
+ deps := make([]Dependency, len(choco.Dependencies))
+ for i, dep := range choco.Dependencies {
+ deps[i] = Dependency{ID: dep.ID, Version: dep.Version}
+ }
+
+ if len(deps) > 0 {
+ m.Metadata.Dependencies = &Dependencies{Dependency: deps}
+ }
+
+ return m.Bytes()
+}
+
+func buildTemplate(name string, text string, data templateData) ([]byte, error) {
+ tp, err := template.New(name).Parse(text)
+ if err != nil {
+ return nil, err
+ }
+
+ var out bytes.Buffer
+ if err = tp.Execute(&out, data); err != nil {
+ return nil, err
+ }
+
+ return out.Bytes(), nil
+}
+
+func dataFor(ctx *context.Context, cl client.Client, choco config.Chocolatey, artifacts []*artifact.Artifact) (templateData, error) {
+ result := templateData{}
+
+ if choco.URLTemplate == "" {
+ url, err := cl.ReleaseURLTemplate(ctx)
+ if err != nil {
+ return result, err
+ }
+
+ choco.URLTemplate = url
+ }
+
+ for _, artifact := range artifacts {
+ sum, err := artifact.Checksum("sha256")
+ if err != nil {
+ return result, err
+ }
+
+ url, err := tmpl.New(ctx).
+ WithArtifact(artifact, map[string]string{}).
+ Apply(choco.URLTemplate)
+ if err != nil {
+ return result, err
+ }
+
+ pkg := releasePackage{
+ DownloadURL: url,
+ Checksum: sum,
+ Arch: artifact.Goarch,
+ }
+
+ result.Packages = append(result.Packages, pkg)
+ }
+
+ return result, nil
+}
+
+// cmder is a special interface to execute external commands.
+//
+// The intention is to be used to wrap the standard exec and provide the
+// ability to create a fake one for testing.
+type cmder interface {
+ // Exec executes an command.
+ Exec(*context.Context, string, ...string) ([]byte, error)
+}
+
+// stdCmd uses the standard golang exec.
+type stdCmd struct{}
+
+var _ cmder = &stdCmd{}
+
+func (stdCmd) Exec(ctx *context.Context, name string, args ...string) ([]byte, error) {
+ return exec.CommandContext(ctx, name, args...).CombinedOutput()
+}
diff --git a/internal/pipe/chocolatey/chocolatey_test.go b/internal/pipe/chocolatey/chocolatey_test.go
new file mode 100644
index 00000000000..c89f0049757
--- /dev/null
+++ b/internal/pipe/chocolatey/chocolatey_test.go
@@ -0,0 +1,330 @@
+package chocolatey
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/goreleaser/goreleaser/internal/artifact"
+ "github.com/goreleaser/goreleaser/internal/client"
+ "github.com/goreleaser/goreleaser/internal/golden"
+ "github.com/goreleaser/goreleaser/internal/testlib"
+ "github.com/goreleaser/goreleaser/pkg/config"
+ "github.com/goreleaser/goreleaser/pkg/context"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDescription(t *testing.T) {
+ require.NotEmpty(t, Pipe{}.String())
+}
+
+func TestSkip(t *testing.T) {
+ ctx := context.New(config.Project{})
+ require.True(t, Pipe{}.Skip(ctx))
+}
+
+func TestDefault(t *testing.T) {
+ testlib.Mktmp(t)
+
+ ctx := &context.Context{
+ TokenType: context.TokenTypeGitHub,
+ Config: config.Project{
+ ProjectName: "myproject",
+ Chocolateys: []config.Chocolatey{
+ {},
+ },
+ },
+ }
+
+ require.NoError(t, Pipe{}.Default(ctx))
+ require.Equal(t, ctx.Config.ProjectName, ctx.Config.Chocolateys[0].Name)
+ require.Equal(t, ctx.Config.ProjectName, ctx.Config.Chocolateys[0].Title)
+ require.Equal(t, "v1", ctx.Config.Chocolateys[0].Goamd64)
+}
+
+func Test_doRun(t *testing.T) {
+ folder := t.TempDir()
+ file := filepath.Join(folder, "archive")
+ require.NoError(t, os.WriteFile(file, []byte("lorem ipsum"), 0o644))
+
+ tests := []struct {
+ name string
+ choco config.Chocolatey
+ exec func() ([]byte, error)
+ published int
+ err string
+ }{
+ {
+ name: "no artifacts",
+ choco: config.Chocolatey{
+ Name: "app",
+ IDs: []string{"no-app"},
+ Goamd64: "v1",
+ },
+ err: "chocolatey requires a windows build and archive",
+ },
+ {
+ name: "choco command not found",
+ choco: config.Chocolatey{
+ Name: "app",
+ Goamd64: "v1",
+ },
+ exec: func() ([]byte, error) {
+ return nil, errors.New(`exec: "choco.exe": executable file not found in $PATH`)
+ },
+ err: `failed to generate chocolatey package: exec: "choco.exe": executable file not found in $PATH: `,
+ },
+ {
+ name: "skip publish",
+ choco: config.Chocolatey{
+ Name: "app",
+ Goamd64: "v1",
+ SkipPublish: true,
+ },
+ exec: func() ([]byte, error) {
+ return []byte("success"), nil
+ },
+ },
+ {
+ name: "success",
+ choco: config.Chocolatey{
+ Name: "app",
+ Goamd64: "v1",
+ },
+ exec: func() ([]byte, error) {
+ return []byte("success"), nil
+ },
+ published: 1,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd = fakeCmd{execFn: tt.exec}
+ t.Cleanup(func() {
+ cmd = stdCmd{}
+ })
+
+ ctx := &context.Context{
+ Git: context.GitInfo{
+ CurrentTag: "v1.0.1",
+ },
+ Version: "1.0.1",
+ Artifacts: artifact.New(),
+ Config: config.Project{
+ Dist: folder,
+ ProjectName: "run-all",
+ },
+ }
+
+ ctx.Artifacts.Add(&artifact.Artifact{
+ Name: "app_1.0.1_windows_amd64.zip",
+ Path: file,
+ Goos: "windows",
+ Goarch: "amd64",
+ Goamd64: "v1",
+ Type: artifact.UploadableArchive,
+ Extra: map[string]interface{}{
+ artifact.ExtraID: "app",
+ artifact.ExtraFormat: "zip",
+ },
+ })
+
+ client := client.NewMock()
+ got := doRun(ctx, client, tt.choco)
+
+ var err string
+ if got != nil {
+ err = got.Error()
+ }
+ if tt.err != err {
+ t.Errorf("Unexpected error: %s (expected %s)", err, tt.err)
+ }
+
+ list := ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableChocolatey)).List()
+ require.Len(t, list, tt.published)
+ })
+ }
+}
+
+func Test_buildNuspec(t *testing.T) {
+ ctx := &context.Context{
+ Version: "1.12.3",
+ }
+ choco := config.Chocolatey{
+ Name: "goreleaser",
+ IDs: []string{},
+ Title: "GoReleaser",
+ Authors: "caarlos0",
+ ProjectURL: "https://goreleaser.com/",
+ Tags: "go docker homebrew golang package",
+ Summary: "Deliver Go binaries as fast and easily as possible",
+ Description: "GoReleaser builds Go binaries for several platforms, creates a GitHub release and then pushes a Homebrew formula to a tap repository. All that wrapped in your favorite CI.",
+ Dependencies: []config.ChocolateyDependency{
+ {ID: "nfpm"},
+ },
+ }
+
+ out, err := buildNuspec(ctx, choco)
+ require.NoError(t, err)
+
+ golden.RequireEqualExt(t, out, ".nuspec")
+}
+
+func Test_buildTemplate(t *testing.T) {
+ folder := t.TempDir()
+ file := filepath.Join(folder, "archive")
+ require.NoError(t, os.WriteFile(file, []byte("lorem ipsum"), 0o644))
+
+ ctx := &context.Context{
+ Version: "1.0.0",
+ Git: context.GitInfo{
+ CurrentTag: "v1.0.0",
+ },
+ }
+
+ artifacts := []*artifact.Artifact{
+ {
+ Name: "app_1.0.0_windows_386.zip",
+ Goos: "windows",
+ Goarch: "386",
+ Goamd64: "v1",
+ Path: file,
+ },
+ {
+ Name: "app_1.0.0_windows_amd64.zip",
+ Goos: "windows",
+ Goarch: "amd64",
+ Goamd64: "v1",
+ Path: file,
+ },
+ }
+
+ choco := config.Chocolatey{
+ Name: "app",
+ }
+
+ client := client.NewMock()
+
+ data, err := dataFor(ctx, client, choco, artifacts)
+ if err != nil {
+ t.Error(err)
+ }
+
+ out, err := buildTemplate(choco.Name, scriptTemplate, data)
+ require.NoError(t, err)
+
+ golden.RequireEqualExt(t, out, ".script.ps1")
+}
+
+func TestPublish(t *testing.T) {
+ folder := t.TempDir()
+ file := filepath.Join(folder, "archive")
+ require.NoError(t, os.WriteFile(file, []byte("lorem ipsum"), 0o644))
+
+ tests := []struct {
+ name string
+ artifacts []artifact.Artifact
+ exec func() ([]byte, error)
+ skip bool
+ err string
+ }{
+ {
+ name: "skip publish",
+ skip: true,
+ err: "publishing is disabled",
+ },
+ {
+ name: "no artifacts",
+ },
+ {
+ name: "no api key",
+ artifacts: []artifact.Artifact{
+ {
+ Type: artifact.PublishableChocolatey,
+ Name: "app.1.0.1.nupkg",
+ Extra: map[string]interface{}{
+ artifact.ExtraFormat: nupkgFormat,
+ chocoConfigExtra: config.Chocolatey{},
+ },
+ },
+ },
+ },
+ {
+ name: "push error",
+ artifacts: []artifact.Artifact{
+ {
+ Type: artifact.PublishableChocolatey,
+ Name: "app.1.0.1.nupkg",
+ Extra: map[string]interface{}{
+ artifact.ExtraFormat: nupkgFormat,
+ chocoConfigExtra: config.Chocolatey{
+ APIKey: "abcd",
+ },
+ },
+ },
+ },
+ exec: func() ([]byte, error) {
+ return nil, errors.New(`unable to push`)
+ },
+ err: "failed to push chocolatey package: unable to push: ",
+ },
+ {
+ name: "success",
+ artifacts: []artifact.Artifact{
+ {
+ Type: artifact.PublishableChocolatey,
+ Name: "app.1.0.1.nupkg",
+ Extra: map[string]interface{}{
+ artifact.ExtraFormat: nupkgFormat,
+ chocoConfigExtra: config.Chocolatey{
+ APIKey: "abcd",
+ },
+ },
+ },
+ },
+ exec: func() ([]byte, error) {
+ return []byte("success"), nil
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd = fakeCmd{execFn: tt.exec}
+ t.Cleanup(func() {
+ cmd = stdCmd{}
+ })
+
+ ctx := &context.Context{
+ SkipPublish: tt.skip,
+ Artifacts: artifact.New(),
+ }
+
+ for _, artifact := range tt.artifacts {
+ ctx.Artifacts.Add(&artifact)
+ }
+
+ got := Pipe{}.Publish(ctx)
+
+ var err string
+ if got != nil {
+ err = got.Error()
+ }
+ if tt.err != err {
+ t.Errorf("Unexpected error: %s (expected %s)", err, tt.err)
+ }
+ })
+ }
+}
+
+type fakeCmd struct {
+ execFn func() ([]byte, error)
+}
+
+var _ cmder = fakeCmd{}
+
+func (f fakeCmd) Exec(ctx *context.Context, name string, args ...string) ([]byte, error) {
+ return f.execFn()
+}
diff --git a/internal/pipe/chocolatey/nuspec.go b/internal/pipe/chocolatey/nuspec.go
new file mode 100644
index 00000000000..85d6637ac78
--- /dev/null
+++ b/internal/pipe/chocolatey/nuspec.go
@@ -0,0 +1,87 @@
+package chocolatey
+
+import (
+ "bytes"
+ "encoding/xml"
+ "strings"
+)
+
+const schema = "http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"
+
+// Nuspec represents a Nuget/Chocolatey Nuspec.
+// More info: https://learn.microsoft.com/en-us/nuget/reference/nuspec
+// https://docs.chocolatey.org/en-us/create/create-packages
+type Nuspec struct {
+ XMLName xml.Name `xml:"package"`
+ Xmlns string `xml:"xmlns,attr,omitempty"`
+ Metadata Metadata `xml:"metadata"`
+ Files Files `xml:"files,omitempty"`
+}
+
+// Metadata contains information about a single package.
+type Metadata struct {
+ ID string `xml:"id"`
+ Version string `xml:"version"`
+ PackageSourceURL string `xml:"packageSourceUrl,omitempty"`
+ Owners string `xml:"owners,omitempty"`
+ Title string `xml:"title,omitempty"`
+ Authors string `xml:"authors"`
+ ProjectURL string `xml:"projectUrl,omitempty"`
+ IconURL string `xml:"iconUrl,omitempty"`
+ Copyright string `xml:"copyright,omitempty"`
+ LicenseURL string `xml:"licenseUrl,omitempty"`
+ RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
+ ProjectSourceURL string `xml:"projectSourceUrl,omitempty"`
+ DocsURL string `xml:"docsUrl,omitempty"`
+ BugTrackerURL string `xml:"bugTrackerUrl,omitempty"`
+ Tags string `xml:"tags,omitempty"`
+ Summary string `xml:"summary,omitempty"`
+ Description string `xml:"description"`
+ ReleaseNotes string `xml:"releaseNotes,omitempty"`
+ Dependencies *Dependencies `xml:"dependencies,omitempty"`
+}
+
+// Dependency represents a dependency element.
+type Dependency struct {
+ ID string `xml:"id,attr"`
+ Version string `xml:"version,attr,omitempty"`
+}
+
+// Dependencies represents a collection zero or more dependency elements.
+type Dependencies struct {
+ Dependency []Dependency `xml:"dependency"`
+}
+
+// File represents a file to be copied.
+type File struct {
+ Source string `xml:"src,attr"`
+ Target string `xml:"target,attr,omitempty"`
+}
+
+// Files represents files that will be copied during packaging.
+type Files struct {
+ File []File `xml:"file"`
+}
+
+// Bytes marshals the Nuspec into XML format and return as []byte.
+func (m *Nuspec) Bytes() ([]byte, error) {
+ b := &bytes.Buffer{}
+ b.WriteString(strings.ToLower(xml.Header))
+
+ enc := xml.NewEncoder(b)
+ enc.Indent("", " ")
+
+ if err := enc.Encode(m); err != nil {
+ return nil, err
+ }
+
+ out := b.Bytes()
+
+ // Follows the nuget specification of self-closing xml tags.
+ tags := []string{"dependency", "file"}
+ for _, tag := range tags {
+ out = bytes.ReplaceAll(out, []byte(">"+tag+">"), []byte(" />"))
+ }
+
+ return out, nil
+}
diff --git a/internal/pipe/chocolatey/nuspec_test.go b/internal/pipe/chocolatey/nuspec_test.go
new file mode 100644
index 00000000000..3d51e0e1b57
--- /dev/null
+++ b/internal/pipe/chocolatey/nuspec_test.go
@@ -0,0 +1,45 @@
+package chocolatey
+
+import (
+ "testing"
+
+ "github.com/goreleaser/goreleaser/internal/golden"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNuspecBytes(t *testing.T) {
+ m := &Nuspec{
+ Xmlns: schema,
+ Metadata: Metadata{
+ ID: "goreleaser",
+ Version: "1.12.3",
+ PackageSourceURL: "https://github.com/goreleaser/goreleaser",
+ Owners: "caarlos0",
+ Title: "GoReleaser",
+ Authors: "caarlos0",
+ ProjectURL: "https://goreleaser.com/",
+ IconURL: "https://raw.githubusercontent.com/goreleaser/goreleaser/main/www/docs/static/avatar.png",
+ Copyright: "2016-2022 Carlos Alexandro Becker",
+ LicenseURL: "https://github.com/goreleaser/goreleaser/blob/main/LICENSE.md",
+ RequireLicenseAcceptance: true,
+ ProjectSourceURL: "https://github.com/goreleaser/goreleaser",
+ DocsURL: "https://github.com/goreleaser/goreleaser/blob/main/README.md",
+ BugTrackerURL: "https://github.com/goreleaser/goreleaser/issues",
+ Tags: "go docker homebrew golang package",
+ Summary: "Deliver Go binaries as fast and easily as possible",
+ Description: "GoReleaser builds Go binaries for several platforms, creates a GitHub release and then pushes a Homebrew formula to a tap repository. All that wrapped in your favorite CI.",
+ ReleaseNotes: "This tag is only to keep version parity with the pro version, which does have a couple of bugfixes.",
+ Dependencies: &Dependencies{Dependency: []Dependency{
+ {ID: "nfpm", Version: "2.20.0"},
+ }},
+ },
+ Files: Files{File: []File{
+ {Source: "tools\\**", Target: "tools"},
+ }},
+ }
+
+ out, err := m.Bytes()
+ require.NoError(t, err)
+
+ golden.RequireEqualExt(t, out, ".nuspec")
+}
diff --git a/internal/pipe/chocolatey/template.go b/internal/pipe/chocolatey/template.go
new file mode 100644
index 00000000000..252950f1cbc
--- /dev/null
+++ b/internal/pipe/chocolatey/template.go
@@ -0,0 +1,38 @@
+package chocolatey
+
+type templateData struct {
+ Packages []releasePackage
+}
+
+type releasePackage struct {
+ DownloadURL string
+ Checksum string
+ Arch string
+}
+
+const scriptTemplate = `# This file was generated by GoReleaser. DO NOT EDIT.
+$ErrorActionPreference = 'Stop';
+
+$version = $env:chocolateyPackageVersion
+$packageName = $env:chocolateyPackageName
+$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+
+$packageArgs = @{
+ packageName = $packageName
+ unzipLocation = $toolsDir
+ fileType = 'exe'
+ {{- range $release := .Packages }}
+ {{- if eq $release.Arch "amd64" }}
+ url64bit = '{{ $release.DownloadURL }}'
+ checksum64 = '{{ $release.Checksum }}'
+ checksumType64 = 'sha256'
+ {{- else }}
+ url = '{{ $release.DownloadURL }}'
+ checksum = '{{ $release.Checksum }}'
+ checksumType = 'sha256'
+ {{- end }}
+ {{- end }}
+}
+
+Install-ChocolateyZipPackage @packageArgs
+`
diff --git a/internal/pipe/chocolatey/testdata/TestNuspecBytes.nuspec.golden b/internal/pipe/chocolatey/testdata/TestNuspecBytes.nuspec.golden
new file mode 100644
index 00000000000..02968097568
--- /dev/null
+++ b/internal/pipe/chocolatey/testdata/TestNuspecBytes.nuspec.golden
@@ -0,0 +1,29 @@
+
+
+
+ goreleaser
+ 1.12.3
+ https://github.com/goreleaser/goreleaser
+ caarlos0
+ GoReleaser
+ caarlos0
+ https://goreleaser.com/
+ https://raw.githubusercontent.com/goreleaser/goreleaser/main/www/docs/static/avatar.png
+ 2016-2022 Carlos Alexandro Becker
+ https://github.com/goreleaser/goreleaser/blob/main/LICENSE.md
+ true
+ https://github.com/goreleaser/goreleaser
+ https://github.com/goreleaser/goreleaser/blob/main/README.md
+ https://github.com/goreleaser/goreleaser/issues
+ go docker homebrew golang package
+ Deliver Go binaries as fast and easily as possible
+ GoReleaser builds Go binaries for several platforms, creates a GitHub release and then pushes a Homebrew formula to a tap repository. All that wrapped in your favorite CI.
+ This tag is only to keep version parity with the pro version, which does have a couple of bugfixes.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/pipe/chocolatey/testdata/Test_buildNuspec.nuspec.golden b/internal/pipe/chocolatey/testdata/Test_buildNuspec.nuspec.golden
new file mode 100644
index 00000000000..7601fef7ef6
--- /dev/null
+++ b/internal/pipe/chocolatey/testdata/Test_buildNuspec.nuspec.golden
@@ -0,0 +1,20 @@
+
+
+
+ goreleaser
+ 1.12.3
+ GoReleaser
+ caarlos0
+ https://goreleaser.com/
+ false
+ go docker homebrew golang package
+ Deliver Go binaries as fast and easily as possible
+ GoReleaser builds Go binaries for several platforms, creates a GitHub release and then pushes a Homebrew formula to a tap repository. All that wrapped in your favorite CI.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/pipe/chocolatey/testdata/Test_buildTemplate.script.ps1.golden b/internal/pipe/chocolatey/testdata/Test_buildTemplate.script.ps1.golden
new file mode 100644
index 00000000000..779da005c22
--- /dev/null
+++ b/internal/pipe/chocolatey/testdata/Test_buildTemplate.script.ps1.golden
@@ -0,0 +1,20 @@
+# This file was generated by GoReleaser. DO NOT EDIT.
+$ErrorActionPreference = 'Stop';
+
+$version = $env:chocolateyPackageVersion
+$packageName = $env:chocolateyPackageName
+$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+
+$packageArgs = @{
+ packageName = $packageName
+ unzipLocation = $toolsDir
+ fileType = 'exe'
+ url = 'https://dummyhost/download/v1.0.0/app_1.0.0_windows_386.zip'
+ checksum = '5e2bf57d3f40c4b6df69daf1936cb766f832374b4fc0259a7cbff06e2f70f269'
+ checksumType = 'sha256'
+ url64bit = 'https://dummyhost/download/v1.0.0/app_1.0.0_windows_amd64.zip'
+ checksum64 = '5e2bf57d3f40c4b6df69daf1936cb766f832374b4fc0259a7cbff06e2f70f269'
+ checksumType64 = 'sha256'
+}
+
+Install-ChocolateyZipPackage @packageArgs
diff --git a/internal/pipe/publish/publish.go b/internal/pipe/publish/publish.go
index cb4c288cb10..cb4c0a9a0c4 100644
--- a/internal/pipe/publish/publish.go
+++ b/internal/pipe/publish/publish.go
@@ -11,6 +11,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/aur"
"github.com/goreleaser/goreleaser/internal/pipe/blob"
"github.com/goreleaser/goreleaser/internal/pipe/brew"
+ "github.com/goreleaser/goreleaser/internal/pipe/chocolatey"
"github.com/goreleaser/goreleaser/internal/pipe/custompublishers"
"github.com/goreleaser/goreleaser/internal/pipe/docker"
"github.com/goreleaser/goreleaser/internal/pipe/krew"
@@ -48,6 +49,7 @@ var publishers = []Publisher{
aur.Pipe{},
krew.Pipe{},
scoop.Pipe{},
+ chocolatey.Pipe{},
milestone.Pipe{},
}
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index 9d5dd684368..10f61ed4721 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -12,6 +12,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/build"
"github.com/goreleaser/goreleaser/internal/pipe/changelog"
"github.com/goreleaser/goreleaser/internal/pipe/checksums"
+ "github.com/goreleaser/goreleaser/internal/pipe/chocolatey"
"github.com/goreleaser/goreleaser/internal/pipe/defaults"
"github.com/goreleaser/goreleaser/internal/pipe/dist"
"github.com/goreleaser/goreleaser/internal/pipe/docker"
@@ -106,6 +107,8 @@ var Pipeline = append(
krew.Pipe{},
// create scoop buckets
scoop.Pipe{},
+ // create chocolatey pkg and publish
+ chocolatey.Pipe{},
// create and push docker images
docker.Pipe{},
// creates a metadata.json and an artifacts.json files in the dist folder
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 29a803b9f56..56895435a4d 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -930,6 +930,7 @@ type Project struct {
GoMod GoMod `yaml:"gomod,omitempty" json:"gomod,omitempty"`
Announce Announce `yaml:"announce,omitempty" json:"announce,omitempty"`
SBOMs []SBOM `yaml:"sboms,omitempty" json:"sboms,omitempty"`
+ Chocolateys []Chocolatey `yaml:"chocolateys,omitempty" json:"chocolatey,omitempty"`
UniversalBinaries []UniversalBinary `yaml:"universal_binaries,omitempty" json:"universal_binaries,omitempty"`
@@ -1116,3 +1117,37 @@ func (a *SlackAttachment) UnmarshalYAML(unmarshal func(interface{}) error) error
func (a SlackAttachment) MarshalJSON() ([]byte, error) {
return json.Marshal(a.Internal)
}
+
+// Chocolatey contains the chocolatey section.
+type Chocolatey struct {
+ Name string `yaml:"name,omitempty" json:"name,omitempty"`
+ IDs []string `yaml:"ids,omitempty" json:"ids,omitempty"`
+ PackageSourceURL string `yaml:"package_source_url,omitempty" json:"package_source_url,omitempty"`
+ Owners string `yaml:"owners,omitempty" json:"authoers,omitempty"`
+ Title string `yaml:"title,omitempty" json:"title,omitempty"`
+ Authors string `yaml:"authors,omitempty" json:"authors,omitempty"`
+ ProjectURL string `yaml:"project_url,omitempty" json:"project_url,omitempty"`
+ URLTemplate string `yaml:"url_template,omitempty" json:"url_template,omitempty"`
+ IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"`
+ Copyright string `yaml:"copyright,omitempty" json:"copyright,omitempty"`
+ LicenseURL string `yaml:"license_url,omitempty" json:"license_url,omitempty"`
+ RequireLicenseAcceptance bool `yaml:"require_license_acceptance,omitempty" json:"require_license_acceptance,omitempty"`
+ ProjectSourceURL string `yaml:"project_source_url,omitempty" json:"project_source_url,omitempty"`
+ DocsURL string `yaml:"docs_url,omitempty" json:"docs_url,omitempty"`
+ BugTrackerURL string `yaml:"bug_tracker_url,omitempty" json:"bug_tracker_url,omitempty"`
+ Tags string `yaml:"tags,omitempty" json:"tags,omitempty"`
+ Summary string `yaml:"summary,omitempty" json:"summary,omitempty"`
+ Description string `yaml:"description,omitempty" json:"description,omitempty"`
+ ReleaseNotes string `yaml:"release_notes,omitempty" json:"release_notes,omitempty"`
+ Dependencies []ChocolateyDependency `yaml:"dependencies,omitempty" json:"dependencies,omitempty"`
+ SkipPublish bool `yaml:"skip_publish,omitempty" json:"skip_publish,omitempty"`
+ APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"`
+ SourceRepo string `yaml:"source_repo,omitempty" json:"source_repo,omitempty"`
+ Goamd64 string `yaml:"goamd64,omitempty" json:"goamd64,omitempty"`
+}
+
+// ChcolateyDependency represents Chocolatey dependency.
+type ChocolateyDependency struct {
+ ID string `yaml:"id,omitempty" json:"id,omitempty"`
+ Version string `yaml:"version,omitempty" json:"version,omitempty"`
+}
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index bdb625e12ae..321f6420522 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -12,6 +12,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/brew"
"github.com/goreleaser/goreleaser/internal/pipe/build"
"github.com/goreleaser/goreleaser/internal/pipe/checksums"
+ "github.com/goreleaser/goreleaser/internal/pipe/chocolatey"
"github.com/goreleaser/goreleaser/internal/pipe/discord"
"github.com/goreleaser/goreleaser/internal/pipe/docker"
"github.com/goreleaser/goreleaser/internal/pipe/gomod"
@@ -84,4 +85,5 @@ var Defaulters = []Defaulter{
linkedin.Pipe{},
telegram.Pipe{},
webhook.Pipe{},
+ chocolatey.Pipe{},
}
diff --git a/www/docs/customization/chocolatey.md b/www/docs/customization/chocolatey.md
new file mode 100644
index 00000000000..5ea9ff95c2d
--- /dev/null
+++ b/www/docs/customization/chocolatey.md
@@ -0,0 +1,133 @@
+# Chocolatey Packages
+
+GoReleaser can also generate `nupkg` packages.
+[Chocolatey](http://chocolatey.org/) are packages based on `nupkg` format, that
+will let you publish your project directly to the Chocolatey Repository. From
+there it will be able to install locally or in Windows distributions.
+
+You can read more about it in the [chocolatey docs](https://docs.chocolatey.org/).
+
+Available options:
+
+```yaml
+# .goreleaser.yaml
+chocolateys:
+ -
+ # Your app's package name.
+ # The value may not contain spaces or character that are not valid for a URL.
+ # If you want a good separator for words, use '-', not '.'.
+ #
+ # Defaults to `ProjectName`.
+ name: foo
+
+ # IDs of the archives to use.
+ # Defaults to empty, which includes all artifacts.
+ ids:
+ - foo
+ - bar
+
+ # Your app's owner.
+ # It basically means your.
+ # Defaults empty.
+ owners: Drum Roll Inc
+
+ # The app's title.
+ # A human-friendly title of the package.
+ # Defaults to `ProjectName`.
+ title: Foo Bar
+
+ # Your app's authors (probably you).
+ # Defaults are shown below.
+ authors: Drummer
+
+ # Your app's project url.
+ # It is a required field.
+ project_url: https://example.com/
+
+ # Template for the url which is determined by the given Token (github,
+ # gitlab or gitea)
+ # Default depends on the client.
+ url_template: "https://github.com/foo/bar/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
+
+ # App's icon.
+ # Default is empty.
+ icon_url: 'https://rawcdn.githack.com/foo/bar/efbdc760-395b-43f1-bf69-ba25c374d473/icon.png'
+
+ # Your app's copyright details.
+ # Default is empty.
+ copyright: 2022 Drummer Roll Inc
+
+ # App's license information url.
+ license_url: https://github.com/foo/bar/blob/main/LICENSE
+
+ # Your apps's require license acceptance:
+ # Specify whether the client must prompt the consumer to accept the package
+ # license before installing.
+ # Default is false.
+ require_license_acceptance: false
+
+ # Your app's source url.
+ # Default is empty.
+ project_source_url: https://github.com/foo/bar
+
+ # Your app's documentation url.
+ # Default is empty.
+ docs_url: https://github.com/foo/bar/blob/main/README.md
+
+ # App's bugtracker url.
+ # Default is empty.
+ bug_tracker_url: https://github.com/foo/barr/issues
+
+ # Your app's tag list.
+ # Default is empty.
+ tags: "foo bar baz"
+
+ # Your app's summary:
+ summary: Software to create fast and easy drum rolls.
+
+ # This the description of your chocolatey package.
+ # Supports markdown.
+ description: |
+ {{ .ProjectName }} installer package.
+ Software to create fast and easy drum rolls.
+
+ # Your app's release notes.
+ # A description of the changes made in this release of the package.
+ # Supports markdown. To prevent the need to continually update this field,
+ # providing a URL to an external list of Release Notes is perfectly
+ # acceptable.
+ # Default is empty.
+ release_notes: "https://github.com/foo/bar/releases/tag/v{{ .Version }}"
+
+ # App's dependencies
+ # Default is empty. Version is not required.
+ dependencies:
+ - id: nfpm
+ version: 2.20.0
+
+ # The api key that should be used to push to the chocolatey repository.
+ #
+ # WARNING: do not expose your api key in the configuration file!
+ api_key: '{{ .Env.CHOCOLATEY_API_KEY }}'
+
+ # The source repository that will push the package to.
+ #
+ # Defaults are shown below.
+ source_repo: "https://push.chocolatey.org/"
+
+ # Setting this will prevent goreleaser to actually try to push the package
+ # to chocolatey repository, leaving the responsability of publishing it to
+ # the user.
+ skip_publish: false
+
+ # GOAMD64 to specify which amd64 version to use if there are multiple
+ # versions from the build section.
+ # Default is v1.
+ goamd64: v1
+```
+
+!!! tip
+ Learn more about the [name template engine](/customization/templates/).
+
+!!! note
+ GoReleaser will not install `chocolatey` nor any of its dependencies for you.
diff --git a/www/docs/static/schema.json b/www/docs/static/schema.json
index 277295cbaac..fac2bfe888f 100644
--- a/www/docs/static/schema.json
+++ b/www/docs/static/schema.json
@@ -496,6 +496,102 @@
"additionalProperties": false,
"type": "object"
},
+ "Chocolatey": {
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "ids": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "package_source_url": {
+ "type": "string"
+ },
+ "authoers": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "authors": {
+ "type": "string"
+ },
+ "project_url": {
+ "type": "string"
+ },
+ "url_template": {
+ "type": "string"
+ },
+ "icon_url": {
+ "type": "string"
+ },
+ "copyright": {
+ "type": "string"
+ },
+ "license_url": {
+ "type": "string"
+ },
+ "require_license_acceptance": {
+ "type": "boolean"
+ },
+ "project_source_url": {
+ "type": "string"
+ },
+ "docs_url": {
+ "type": "string"
+ },
+ "bug_tracker_url": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "string"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "release_notes": {
+ "type": "string"
+ },
+ "dependencies": {
+ "items": {
+ "$ref": "#/$defs/ChocolateyDependency"
+ },
+ "type": "array"
+ },
+ "skip_publish": {
+ "type": "boolean"
+ },
+ "api_key": {
+ "type": "string"
+ },
+ "source_repo": {
+ "type": "string"
+ },
+ "goamd64": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ },
+ "ChocolateyDependency": {
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ },
"CommitAuthor": {
"properties": {
"name": {
@@ -1747,6 +1843,12 @@
},
"type": "array"
},
+ "chocolatey": {
+ "items": {
+ "$ref": "#/$defs/Chocolatey"
+ },
+ "type": "array"
+ },
"universal_binaries": {
"items": {
"$ref": "#/$defs/UniversalBinary"
diff --git a/www/mkdocs.yml b/www/mkdocs.yml
index 8f67dce1c09..b036ef4081f 100644
--- a/www/mkdocs.yml
+++ b/www/mkdocs.yml
@@ -96,6 +96,7 @@ nav:
- customization/nfpm.md
- customization/checksum.md
- customization/snapcraft.md
+ - customization/chocolatey.md
- customization/docker.md
- customization/docker_manifest.md
- customization/sbom.md