Skip to content

Commit

Permalink
feat: chocolatey package support
Browse files Browse the repository at this point in the history
Generates the structure used to pack and push Chocolatey packages.
  • Loading branch information
faabiosr committed Oct 31, 2022
1 parent e642f04 commit c0c32b0
Show file tree
Hide file tree
Showing 8 changed files with 495 additions and 0 deletions.
211 changes: 211 additions & 0 deletions internal/pipe/chocolatey/chocolatey.go
@@ -0,0 +1,211 @@
package chocolatey

import (
"bytes"
"errors"
"os"
"path/filepath"
"text/template"

"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/client"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)

// 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"
}
}

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
}

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
}

scriptFile := filepath.Join(toolsPath, "chocolateyinstall.ps1")
script, err := buildScript(ctx, cl, choco, artifacts)
if err != nil {
return err
}

return os.WriteFile(scriptFile, script, 0o644)
}

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 buildScript(ctx *context.Context, cl client.Client, choco config.Chocolatey, artifacts []*artifact.Artifact) ([]byte, error) {
tp, err := template.New("install").Parse(scriptTemplate)
if err != nil {
return nil, err
}

if choco.URLTemplate == "" {
url, err := cl.ReleaseURLTemplate(ctx)
if err != nil {
return nil, err
}

choco.URLTemplate = url
}

releases := make([]releasePackage, len(artifacts))

for i, artifact := range artifacts {
url, err := tmpl.New(ctx).
WithArtifact(artifact, map[string]string{}).
Apply(choco.URLTemplate)
if err != nil {
return nil, err
}

sum, err := artifact.Checksum("sha256")
if err != nil {
return nil, err
}

releases[i] = releasePackage{
DownloadURL: url,
Checksum: sum,
Arch: artifact.Goarch,
}
}

var out bytes.Buffer
if err = tp.Execute(&out, releases); err != nil {
return nil, err
}

return out.Bytes(), nil
}
173 changes: 173 additions & 0 deletions internal/pipe/chocolatey/chocolatey_test.go
@@ -0,0 +1,173 @@
package chocolatey

import (
"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
artifacts []artifact.Artifact
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: "success",
choco: config.Chocolatey{
Name: "app",
Goamd64: "v1",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
}
})
}
}

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_buildScript(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",
},
}
choco := config.Chocolatey{}
client := client.NewMock()
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,
},
}

out, err := buildScript(ctx, client, choco, artifacts)
require.NoError(t, err)

golden.RequireEqualExt(t, out, ".ps1")
}

0 comments on commit c0c32b0

Please sign in to comment.