github.com/ahmet2mir/goreleaser@v0.180.3-0.20210927151101-8e5ee5a9b8c5/internal/pipe/scoop/scoop.go (about) 1 // Package scoop provides a Pipe that generates a scoop.sh App Manifest and pushes it to a bucket 2 package scoop 3 4 import ( 5 "bytes" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "os" 10 "path" 11 "path/filepath" 12 "strings" 13 14 "github.com/apex/log" 15 "github.com/goreleaser/goreleaser/internal/artifact" 16 "github.com/goreleaser/goreleaser/internal/client" 17 "github.com/goreleaser/goreleaser/internal/pipe" 18 "github.com/goreleaser/goreleaser/internal/tmpl" 19 "github.com/goreleaser/goreleaser/pkg/config" 20 "github.com/goreleaser/goreleaser/pkg/context" 21 ) 22 23 // ErrNoWindows when there is no build for windows (goos doesn't contain windows). 24 var ErrNoWindows = errors.New("scoop requires a windows build") 25 26 // ErrTokenTypeNotImplementedForScoop indicates that a new token type was not implemented for this pipe. 27 var ErrTokenTypeNotImplementedForScoop = errors.New("token type not implemented for scoop pipe") 28 29 const scoopConfigExtra = "ScoopConfig" 30 31 // Pipe that builds and publishes scoop manifests. 32 type Pipe struct{} 33 34 func (Pipe) String() string { return "scoop manifests" } 35 func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Scoop.Bucket.Name == "" } 36 37 // Run creates the scoop manifest locally. 38 func (Pipe) Run(ctx *context.Context) error { 39 client, err := client.New(ctx) 40 if err != nil { 41 return err 42 } 43 return doRun(ctx, client) 44 } 45 46 // Publish scoop manifest. 47 func (Pipe) Publish(ctx *context.Context) error { 48 client, err := client.New(ctx) 49 if err != nil { 50 return err 51 } 52 return doPublish(ctx, client) 53 } 54 55 // Default sets the pipe defaults. 56 func (Pipe) Default(ctx *context.Context) error { 57 if ctx.Config.Scoop.Name == "" { 58 ctx.Config.Scoop.Name = ctx.Config.ProjectName 59 } 60 if ctx.Config.Scoop.CommitAuthor.Name == "" { 61 ctx.Config.Scoop.CommitAuthor.Name = "goreleaserbot" 62 } 63 if ctx.Config.Scoop.CommitAuthor.Email == "" { 64 ctx.Config.Scoop.CommitAuthor.Email = "goreleaser@carlosbecker.com" 65 } 66 if ctx.Config.Scoop.CommitMessageTemplate == "" { 67 ctx.Config.Scoop.CommitMessageTemplate = "Scoop update for {{ .ProjectName }} version {{ .Tag }}" 68 } 69 return nil 70 } 71 72 func doRun(ctx *context.Context, cl client.Client) error { 73 scoop := ctx.Config.Scoop 74 75 // TODO: multiple archives 76 if ctx.Config.Archives[0].Format == "binary" { 77 return pipe.Skip("archive format is binary") 78 } 79 80 archives := ctx.Artifacts.Filter( 81 artifact.And( 82 artifact.ByGoos("windows"), 83 artifact.ByType(artifact.UploadableArchive), 84 ), 85 ).List() 86 if len(archives) == 0 { 87 return ErrNoWindows 88 } 89 90 filename := scoop.Name + ".json" 91 92 data, err := dataFor(ctx, cl, archives) 93 if err != nil { 94 return err 95 } 96 content, err := doBuildManifest(data) 97 if err != nil { 98 return err 99 } 100 101 path := filepath.Join(ctx.Config.Dist, filename) 102 log.WithField("manifest", path).Info("writing") 103 if err := os.WriteFile(path, content.Bytes(), 0o644); err != nil { 104 return fmt.Errorf("failed to write scoop manifest: %w", err) 105 } 106 107 ctx.Artifacts.Add(&artifact.Artifact{ 108 Name: filename, 109 Path: path, 110 Type: artifact.ScoopManifest, 111 Extra: map[string]interface{}{ 112 scoopConfigExtra: scoop, 113 }, 114 }) 115 return nil 116 } 117 118 func doPublish(ctx *context.Context, cl client.Client) error { 119 manifests := ctx.Artifacts.Filter(artifact.ByType(artifact.ScoopManifest)).List() 120 if len(manifests) == 0 { // should never happen 121 return nil 122 } 123 124 manifest := manifests[0] 125 scoop := manifest.Extra[scoopConfigExtra].(config.Scoop) 126 127 var err error 128 cl, err = client.NewIfToken(ctx, cl, scoop.Bucket.Token) 129 if err != nil { 130 return err 131 } 132 133 if strings.TrimSpace(scoop.SkipUpload) == "true" { 134 return pipe.Skip("scoop.skip_upload is true") 135 } 136 if strings.TrimSpace(scoop.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" { 137 return pipe.Skip("release is prerelease") 138 } 139 if ctx.Config.Release.Draft { 140 return pipe.Skip("release is marked as draft") 141 } 142 if ctx.Config.Release.Disable { 143 return pipe.Skip("release is disabled") 144 } 145 146 commitMessage, err := tmpl.New(ctx).Apply(scoop.CommitMessageTemplate) 147 if err != nil { 148 return err 149 } 150 151 content, err := os.ReadFile(manifest.Path) 152 if err != nil { 153 return err 154 } 155 156 repo := client.RepoFromRef(scoop.Bucket) 157 return cl.CreateFile( 158 ctx, 159 scoop.CommitAuthor, 160 repo, 161 content, 162 path.Join(scoop.Folder, manifest.Name), 163 commitMessage, 164 ) 165 } 166 167 // Manifest represents a scoop.sh App Manifest. 168 // more info: https://github.com/lukesampson/scoop/wiki/App-Manifests 169 type Manifest struct { 170 Version string `json:"version"` // The version of the app that this manifest installs. 171 Architecture map[string]Resource `json:"architecture"` // `architecture`: If the app has 32- and 64-bit versions, architecture can be used to wrap the differences. 172 Homepage string `json:"homepage,omitempty"` // `homepage`: The home page for the program. 173 License string `json:"license,omitempty"` // `license`: The software license for the program. For well-known licenses, this will be a string like "MIT" or "GPL2". For custom licenses, this should be the URL of the license. 174 Description string `json:"description,omitempty"` // Description of the app 175 Persist []string `json:"persist,omitempty"` // Persist data between updates 176 PreInstall []string `json:"pre_install,omitempty"` // An array of strings, of the commands to be executed before an application is installed. 177 PostInstall []string `json:"post_install,omitempty"` // An array of strings, of the commands to be executed after an application is installed. 178 } 179 180 // Resource represents a combination of a url and a binary name for an architecture. 181 type Resource struct { 182 URL string `json:"url"` // URL to the archive 183 Bin []string `json:"bin"` // name of binary inside the archive 184 Hash string `json:"hash"` // the archive checksum 185 } 186 187 func doBuildManifest(manifest Manifest) (bytes.Buffer, error) { 188 var result bytes.Buffer 189 data, err := json.MarshalIndent(manifest, "", " ") 190 if err != nil { 191 return result, err 192 } 193 _, err = result.Write(data) 194 return result, err 195 } 196 197 func dataFor(ctx *context.Context, cl client.Client, artifacts []*artifact.Artifact) (Manifest, error) { 198 manifest := Manifest{ 199 Version: ctx.Version, 200 Architecture: map[string]Resource{}, 201 Homepage: ctx.Config.Scoop.Homepage, 202 License: ctx.Config.Scoop.License, 203 Description: ctx.Config.Scoop.Description, 204 Persist: ctx.Config.Scoop.Persist, 205 PreInstall: ctx.Config.Scoop.PreInstall, 206 PostInstall: ctx.Config.Scoop.PostInstall, 207 } 208 209 if ctx.Config.Scoop.URLTemplate == "" { 210 url, err := cl.ReleaseURLTemplate(ctx) 211 if err != nil { 212 if client.IsNotImplementedErr(err) { 213 return manifest, ErrTokenTypeNotImplementedForScoop 214 } 215 return manifest, err 216 } 217 ctx.Config.Scoop.URLTemplate = url 218 } 219 220 for _, artifact := range artifacts { 221 if artifact.Goos != "windows" { 222 continue 223 } 224 225 var arch string 226 switch { 227 case artifact.Goarch == "386": 228 arch = "32bit" 229 case artifact.Goarch == "amd64": 230 arch = "64bit" 231 default: 232 continue 233 } 234 235 url, err := tmpl.New(ctx). 236 WithArtifact(artifact, map[string]string{}). 237 Apply(ctx.Config.Scoop.URLTemplate) 238 if err != nil { 239 return manifest, err 240 } 241 242 sum, err := artifact.Checksum("sha256") 243 if err != nil { 244 return manifest, err 245 } 246 247 log.WithFields(log.Fields{ 248 "artifactExtras": artifact.Extra, 249 "fromURLTemplate": ctx.Config.Scoop.URLTemplate, 250 "templatedBrewURL": url, 251 "sum": sum, 252 }).Debug("scoop url templating") 253 254 manifest.Architecture[arch] = Resource{ 255 URL: url, 256 Bin: binaries(artifact), 257 Hash: sum, 258 } 259 } 260 261 return manifest, nil 262 } 263 264 func binaries(a *artifact.Artifact) []string { 265 // nolint: prealloc 266 var bins []string 267 wrap := a.ExtraOr("WrappedIn", "").(string) 268 for _, b := range a.ExtraOr("Builds", []*artifact.Artifact{}).([]*artifact.Artifact) { 269 bins = append(bins, filepath.Join(wrap, b.Name)) 270 } 271 return bins 272 }