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