github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/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 "path/filepath" 9 "strings" 10 11 "github.com/apex/log" 12 "github.com/goreleaser/goreleaser/internal/artifact" 13 "github.com/goreleaser/goreleaser/internal/client" 14 "github.com/goreleaser/goreleaser/internal/pipe" 15 "github.com/goreleaser/goreleaser/internal/tmpl" 16 "github.com/goreleaser/goreleaser/pkg/context" 17 ) 18 19 // ErrNoWindows when there is no build for windows (goos doesn't contain windows). 20 var ErrNoWindows = errors.New("scoop requires a windows build") 21 22 // ErrTokenTypeNotImplementedForScoop indicates that a new token type was not implemented for this pipe. 23 var ErrTokenTypeNotImplementedForScoop = errors.New("token type not implemented for scoop pipe") 24 25 // Pipe for build. 26 type Pipe struct{} 27 28 func (Pipe) String() string { 29 return "scoop manifests" 30 } 31 32 // Publish scoop manifest. 33 func (Pipe) Publish(ctx *context.Context) error { 34 if ctx.SkipPublish { 35 return pipe.ErrSkipPublishEnabled 36 } 37 38 client, err := client.New(ctx) 39 if err != nil { 40 return err 41 } 42 return doRun(ctx, client) 43 } 44 45 // Default sets the pipe defaults. 46 func (Pipe) Default(ctx *context.Context) error { 47 if ctx.Config.Scoop.Name == "" { 48 ctx.Config.Scoop.Name = ctx.Config.ProjectName 49 } 50 if ctx.Config.Scoop.CommitAuthor.Name == "" { 51 ctx.Config.Scoop.CommitAuthor.Name = "goreleaserbot" 52 } 53 if ctx.Config.Scoop.CommitAuthor.Email == "" { 54 ctx.Config.Scoop.CommitAuthor.Email = "goreleaser@carlosbecker.com" 55 } 56 57 if ctx.Config.Scoop.CommitMessageTemplate == "" { 58 ctx.Config.Scoop.CommitMessageTemplate = "Scoop update for {{ .ProjectName }} version {{ .Tag }}" 59 } 60 61 return nil 62 } 63 64 func doRun(ctx *context.Context, cl client.Client) error { 65 scoop := ctx.Config.Scoop 66 if scoop.Bucket.Name == "" { 67 return pipe.Skip("scoop section is not configured") 68 } 69 70 if scoop.Bucket.Token != "" { 71 token, err := tmpl.New(ctx).ApplySingleEnvOnly(scoop.Bucket.Token) 72 if err != nil { 73 return err 74 } 75 log.Debug("using custom token to publish scoop manifest") 76 c, err := client.NewWithToken(ctx, token) 77 if err != nil { 78 return err 79 } 80 cl = c 81 } 82 83 // TODO: multiple archives 84 if ctx.Config.Archives[0].Format == "binary" { 85 return pipe.Skip("archive format is binary") 86 } 87 88 var archives = ctx.Artifacts.Filter( 89 artifact.And( 90 artifact.ByGoos("windows"), 91 artifact.ByType(artifact.UploadableArchive), 92 ), 93 ).List() 94 if len(archives) == 0 { 95 return ErrNoWindows 96 } 97 98 var path = scoop.Name + ".json" 99 100 data, err := dataFor(ctx, cl, archives) 101 if err != nil { 102 return err 103 } 104 content, err := doBuildManifest(data) 105 if err != nil { 106 return err 107 } 108 109 if ctx.SkipPublish { 110 return pipe.ErrSkipPublishEnabled 111 } 112 if strings.TrimSpace(scoop.SkipUpload) == "true" { 113 return pipe.Skip("scoop.skip_upload is true") 114 } 115 if strings.TrimSpace(scoop.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" { 116 return pipe.Skip("release is prerelease") 117 } 118 if ctx.Config.Release.Draft { 119 return pipe.Skip("release is marked as draft") 120 } 121 if ctx.Config.Release.Disable { 122 return pipe.Skip("release is disabled") 123 } 124 125 commitMessage, err := tmpl.New(ctx). 126 Apply(scoop.CommitMessageTemplate) 127 if err != nil { 128 return err 129 } 130 131 repo := client.RepoFromRef(scoop.Bucket) 132 return cl.CreateFile( 133 ctx, 134 scoop.CommitAuthor, 135 repo, 136 content.Bytes(), 137 path, 138 commitMessage, 139 ) 140 } 141 142 // Manifest represents a scoop.sh App Manifest. 143 // more info: https://github.com/lukesampson/scoop/wiki/App-Manifests 144 type Manifest struct { 145 Version string `json:"version"` // The version of the app that this manifest installs. 146 Architecture map[string]Resource `json:"architecture"` // `architecture`: If the app has 32- and 64-bit versions, architecture can be used to wrap the differences. 147 Homepage string `json:"homepage,omitempty"` // `homepage`: The home page for the program. 148 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. 149 Description string `json:"description,omitempty"` // Description of the app 150 Persist []string `json:"persist,omitempty"` // Persist data between updates 151 PreInstall []string `json:"pre_install,omitempty"` // An array of strings, of the commands to be executed before an application is installed. 152 PostInstall []string `json:"post_install,omitempty"` // An array of strings, of the commands to be executed after an application is installed. 153 } 154 155 // Resource represents a combination of a url and a binary name for an architecture. 156 type Resource struct { 157 URL string `json:"url"` // URL to the archive 158 Bin []string `json:"bin"` // name of binary inside the archive 159 Hash string `json:"hash"` // the archive checksum 160 } 161 162 func doBuildManifest(manifest Manifest) (bytes.Buffer, error) { 163 var result bytes.Buffer 164 data, err := json.MarshalIndent(manifest, "", " ") 165 if err != nil { 166 return result, err 167 } 168 _, err = result.Write(data) 169 return result, err 170 } 171 172 func dataFor(ctx *context.Context, cl client.Client, artifacts []*artifact.Artifact) (Manifest, error) { 173 var manifest = Manifest{ 174 Version: ctx.Version, 175 Architecture: map[string]Resource{}, 176 Homepage: ctx.Config.Scoop.Homepage, 177 License: ctx.Config.Scoop.License, 178 Description: ctx.Config.Scoop.Description, 179 Persist: ctx.Config.Scoop.Persist, 180 PreInstall: ctx.Config.Scoop.PreInstall, 181 PostInstall: ctx.Config.Scoop.PostInstall, 182 } 183 184 if ctx.Config.Scoop.URLTemplate == "" { 185 url, err := cl.ReleaseURLTemplate(ctx) 186 if err != nil { 187 if client.IsNotImplementedErr(err) { 188 return manifest, ErrTokenTypeNotImplementedForScoop 189 } 190 return manifest, err 191 } 192 ctx.Config.Scoop.URLTemplate = url 193 } 194 195 for _, artifact := range artifacts { 196 var arch = "64bit" 197 if artifact.Goarch == "386" { 198 arch = "32bit" 199 } 200 201 url, err := tmpl.New(ctx). 202 WithArtifact(artifact, map[string]string{}). 203 Apply(ctx.Config.Scoop.URLTemplate) 204 if err != nil { 205 return manifest, err 206 } 207 208 sum, err := artifact.Checksum("sha256") 209 if err != nil { 210 return manifest, err 211 } 212 213 log.WithFields(log.Fields{ 214 "artifactExtras": artifact.Extra, 215 "fromURLTemplate": ctx.Config.Scoop.URLTemplate, 216 "templatedBrewURL": url, 217 "sum": sum, 218 }).Debug("scoop url templating") 219 220 manifest.Architecture[arch] = Resource{ 221 URL: url, 222 Bin: binaries(artifact), 223 Hash: sum, 224 } 225 } 226 227 return manifest, nil 228 } 229 230 func binaries(a *artifact.Artifact) []string { 231 // nolint: prealloc 232 var bins []string 233 var wrap = a.ExtraOr("WrappedIn", "").(string) 234 for _, b := range a.ExtraOr("Builds", []*artifact.Artifact{}).([]*artifact.Artifact) { 235 bins = append(bins, filepath.Join(wrap, b.Name)) 236 } 237 return bins 238 }