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