diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go index 06434e02c4c1..1118ff202d42 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 ) func (t Type) String() string { @@ -104,6 +106,8 @@ func (t Type) String() string { return "PKGBUILD" case SrcInfo: return "SRCINFO" + case PublishableChocolatey: + return "Chocolatey" default: return "unknown" } diff --git a/internal/pipe/chocolatey/chocolatey.go b/internal/pipe/chocolatey/chocolatey.go new file mode 100644 index 000000000000..bc9791b59759 --- /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.exe", "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.exe", 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 000000000000..0e593ca1fc13 --- /dev/null +++ b/internal/pipe/chocolatey/chocolatey_test.go @@ -0,0 +1,331 @@ +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/template.go b/internal/pipe/chocolatey/template.go new file mode 100644 index 000000000000..252950f1cbc0 --- /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/Test_buildNuspec.nuspec.golden b/internal/pipe/chocolatey/testdata/Test_buildNuspec.nuspec.golden new file mode 100644 index 000000000000..7601fef7ef6c --- /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 000000000000..779da005c226 --- /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 cb4c288cb10c..dfae28d4716d 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" @@ -49,6 +50,7 @@ var publishers = []Publisher{ krew.Pipe{}, scoop.Pipe{}, milestone.Pipe{}, + chocolatey.Pipe{}, } // Pipe that publishes artifacts. diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 9d5dd6843686..10f61ed47210 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 6060202c23b8..0cb19762a916 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -929,6 +929,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"` @@ -1115,3 +1116,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 bdb625e12ae7..321f64205223 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 000000000000..665900b84c27 --- /dev/null +++ b/www/docs/customization/chocolatey.md @@ -0,0 +1,127 @@ +# 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 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 }}" + + # 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 e7dc0bb516df..b9e7f870c6f4 100644 --- a/www/docs/static/schema.json +++ b/www/docs/static/schema.json @@ -490,6 +490,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": { @@ -1741,6 +1837,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 4c0abdf3cd2e..04502f8b941b 100644 --- a/www/mkdocs.yml +++ b/www/mkdocs.yml @@ -91,6 +91,7 @@ nav: - customization/nfpm.md - customization/checksum.md - customization/snapcraft.md + - customization/chocolatey.md - customization/docker.md - customization/docker_manifest.md - customization/sbom.md