feat: chocolatey support (#3509)
This PR adds support for generating the structure used to pack and push
Chocolatey Packages. And will solve the #3154
Is not ready for merge yet, but has the main structure, and ready for
comments.
Accordingly to Chocolatey, in order to build a package, it's necessary a
`.nuspec` and `chocolateyinstall.ps1` files at least, having these ones,
we could pack and distribute without adding the binary inside the final
package and that was implemented here.
To complete, will be necessary to define the package build and
distribute, however will be required to have Chocolatey installed
(Windows Only). One of alternatives that I thought was, publish the
files like Scoop and Brew in a separate repository, and there we could
use `chocolatey` through
[crazy-max/ghaction-chocolatey](https://github.com/crazy-max/ghaction-chocolatey).
Chocolatey has a lot of good examples of repositories:
https://github.com/chocolatey-community/chocolatey-packages/tree/master/automatic/curl
A final compilation of the missing parts:
- [x] How to pack and push (chocolatey)
- [x] Documentation
Sorry for the long description😄
All feedback very welcome!
Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2022-11-12 03:52:32 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-11-25 15:26:14 -03:00
|
|
|
url, err := tmpl.New(ctx).WithArtifact(artifact).Apply(choco.URLTemplate)
|
feat: chocolatey support (#3509)
This PR adds support for generating the structure used to pack and push
Chocolatey Packages. And will solve the #3154
Is not ready for merge yet, but has the main structure, and ready for
comments.
Accordingly to Chocolatey, in order to build a package, it's necessary a
`.nuspec` and `chocolateyinstall.ps1` files at least, having these ones,
we could pack and distribute without adding the binary inside the final
package and that was implemented here.
To complete, will be necessary to define the package build and
distribute, however will be required to have Chocolatey installed
(Windows Only). One of alternatives that I thought was, publish the
files like Scoop and Brew in a separate repository, and there we could
use `chocolatey` through
[crazy-max/ghaction-chocolatey](https://github.com/crazy-max/ghaction-chocolatey).
Chocolatey has a lot of good examples of repositories:
https://github.com/chocolatey-community/chocolatey-packages/tree/master/automatic/curl
A final compilation of the missing parts:
- [x] How to pack and push (chocolatey)
- [x] Documentation
Sorry for the long description😄
All feedback very welcome!
Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2022-11-12 03:52:32 +01:00
|
|
|
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()
|
|
|
|
}
|