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