diff --git a/config/config.go b/config/config.go index b37276dd8..ff977629d 100644 --- a/config/config.go +++ b/config/config.go @@ -49,6 +49,15 @@ type Homebrew struct { DownloadStrategy string `yaml:"download_strategy,omitempty"` } +// Scoop contains the scoop.sh section +type Scoop struct { + Bucket Repo `yaml:",omitempty"` + CommitAuthor CommitAuthor `yaml:"commit_author,omitempty"` + Homepage string `yaml:",omitempty"` + Description string `yaml:",omitempty"` + License string `yaml:",omitempty"` +} + // CommitAuthor is the author of a Git commit type CommitAuthor struct { Name string `yaml:",omitempty"` @@ -205,6 +214,7 @@ type Project struct { ProjectName string `yaml:"project_name,omitempty"` Release Release `yaml:",omitempty"` Brew Homebrew `yaml:",omitempty"` + Scoop Scoop `yaml:",omitempty"` Builds []Build `yaml:",omitempty"` Archive Archive `yaml:",omitempty"` FPM FPM `yaml:",omitempty"` diff --git a/goreleaserlib/goreleaser.go b/goreleaserlib/goreleaser.go index bce4045b1..499085fbb 100644 --- a/goreleaserlib/goreleaser.go +++ b/goreleaserlib/goreleaser.go @@ -30,6 +30,7 @@ import ( "github.com/goreleaser/goreleaser/pipeline/fpm" "github.com/goreleaser/goreleaser/pipeline/git" "github.com/goreleaser/goreleaser/pipeline/release" + "github.com/goreleaser/goreleaser/pipeline/scoop" "github.com/goreleaser/goreleaser/pipeline/sign" "github.com/goreleaser/goreleaser/pipeline/snapcraft" ) @@ -60,6 +61,7 @@ var pipes = []pipeline.Piper{ artifactory.Pipe{}, // push to artifactory release.Pipe{}, // release to github brew.Pipe{}, // push to brew tap + scoop.Pipe{}, // push to scoop bucket } // Flags interface represents an extractor of cli flags diff --git a/pipeline/defaults/defaults.go b/pipeline/defaults/defaults.go index 7b3e1e7d2..b854509ac 100644 --- a/pipeline/defaults/defaults.go +++ b/pipeline/defaults/defaults.go @@ -15,6 +15,7 @@ import ( "github.com/goreleaser/goreleaser/pipeline/env" "github.com/goreleaser/goreleaser/pipeline/fpm" "github.com/goreleaser/goreleaser/pipeline/release" + "github.com/goreleaser/goreleaser/pipeline/scoop" "github.com/goreleaser/goreleaser/pipeline/sign" "github.com/goreleaser/goreleaser/pipeline/snapcraft" "github.com/goreleaser/goreleaser/pipeline/snapshot" @@ -40,6 +41,7 @@ var defaulters = []pipeline.Defaulter{ docker.Pipe{}, artifactory.Pipe{}, brew.Pipe{}, + scoop.Pipe{}, } // Run the pipe diff --git a/pipeline/scoop/scoop.go b/pipeline/scoop/scoop.go new file mode 100644 index 000000000..3ad8d38ae --- /dev/null +++ b/pipeline/scoop/scoop.go @@ -0,0 +1,165 @@ +// Package scoop provides a Pipe that generates a scoop.sh App Manifest and pushes it to a bucket +package scoop + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + + "github.com/goreleaser/goreleaser/config" + "github.com/goreleaser/goreleaser/context" + "github.com/goreleaser/goreleaser/internal/artifact" + "github.com/goreleaser/goreleaser/internal/client" + "github.com/goreleaser/goreleaser/pipeline" +) + +// ErrNoWindows when there is no build for windows (goos doesn't contain windows) +var ErrNoWindows = errors.New("scoop requires a windows build") + +// Pipe for build +type Pipe struct{} + +func (Pipe) String() string { + return "creating Scoop Manifest" +} + +// Run the pipe +func (Pipe) Run(ctx *context.Context) error { + client, err := client.NewGitHub(ctx) + if err != nil { + return err + } + return doRun(ctx, client) +} + +// Default sets the pipe defaults +func (Pipe) Default(ctx *context.Context) error { + if ctx.Config.Scoop.CommitAuthor.Name == "" { + ctx.Config.Scoop.CommitAuthor.Name = "goreleaserbot" + } + if ctx.Config.Scoop.CommitAuthor.Email == "" { + ctx.Config.Scoop.CommitAuthor.Email = "goreleaser@carlosbecker.com" + } + return nil +} + +func isScoopBuild(build config.Build) bool { + for _, ignore := range build.Ignore { + if ignore.Goos == "windows" { + return false + } + } + return contains(build.Goos, "darwin") +} + +func contains(ss []string, s string) bool { + for _, zs := range ss { + if zs == s { + return true + } + } + return false +} + +func doRun(ctx *context.Context, client client.Client) error { + if ctx.Config.Scoop.Bucket.Name == "" { + return pipeline.Skip("scoop section is not configured") + } + if ctx.Config.Archive.Format == "binary" { + return pipeline.Skip("archive format is binary") + } + + var archives = ctx.Artifacts.Filter( + artifact.And( + artifact.ByGoos("windows"), + artifact.ByType(artifact.UploadableArchive), + ), + ).List() + if len(archives) == 0 { + return ErrNoWindows + } + + path := ctx.Config.ProjectName + ".json" + + content, err := buildManifest(ctx, client, archives) + if err != nil { + return err + } + + if !ctx.Publish { + return pipeline.ErrSkipPublish + } + if ctx.Config.Release.Draft { + return pipeline.Skip("release is marked as draft") + } + + return client.CreateFile( + ctx, + ctx.Config.Scoop.CommitAuthor, + ctx.Config.Scoop.Bucket, + content, + path) +} + +// Manifest represents a scoop.sh App Manifest, more info: +// https://github.com/lukesampson/scoop/wiki/App-Manifests +type Manifest struct { + Version string `json:"version"` // The version of the app that this manifest installs. + Architecture map[string]Resource `json:"architecture"` // `architecture`: If the app has 32- and 64-bit versions, architecture can be used to wrap the differences. + Homepage string `json:"homepage,omitempty"` // `homepage`: The home page for the program. + 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. + Description string `json:"description,omitempty"` // Description of the app +} + +// Resource represents a combination of a url and a binary name for an architecture +type Resource struct { + URL string `json:"url"` // URL to the archive + Bin string `json:"bin"` // name of binary inside the archive +} + +func buildManifest(ctx *context.Context, client client.Client, artifacts []artifact.Artifact) (result bytes.Buffer, err error) { + var githubURL = "https://github.com" + if ctx.Config.GitHubURLs.Download != "" { + githubURL = ctx.Config.GitHubURLs.Download + } + + manifest := Manifest{ + Version: ctx.Version, + Architecture: make(map[string]Resource), + Homepage: ctx.Config.Scoop.Homepage, + License: ctx.Config.Scoop.License, + Description: ctx.Config.Scoop.Description, + } + + for _, artifact := range artifacts { + if artifact.Goarch == "amd64" { + manifest.Architecture["64bit"] = Resource{ + URL: getDownloadURL(ctx, githubURL, artifact.Name), + Bin: ctx.Config.Builds[0].Binary + ".exe", + } + } else if artifact.Goarch == "386" { + manifest.Architecture["32bit"] = Resource{ + URL: getDownloadURL(ctx, githubURL, artifact.Name), + Bin: ctx.Config.Builds[0].Binary + ".exe", + } + } + } + + data, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + return + } + _, err = result.Write(data) + + return +} + +func getDownloadURL(ctx *context.Context, githubURL, file string) (url string) { + return fmt.Sprintf("%s/%s/%s/releases/download/%s/%s", + githubURL, + ctx.Config.Release.GitHub.Owner, + ctx.Config.Release.GitHub.Name, + ctx.Version, + file) +}