mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-06 03:13:48 +02:00
c57cdab0c6
If applied, this commit simplifies the code required to support templated fields in configuration files. This should make it easier to add more templated fields in the future. As [discussed first on Discord](https://discord.com/channels/890434333251362866/1032457293687685191/1124033219747127316).
394 lines
11 KiB
Go
394 lines
11 KiB
Go
// Package scoop provides a Pipe that generates a scoop.sh App Manifest and pushes it to a bucket.
|
|
package scoop
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/caarlos0/log"
|
|
"github.com/goreleaser/goreleaser/internal/artifact"
|
|
"github.com/goreleaser/goreleaser/internal/client"
|
|
"github.com/goreleaser/goreleaser/internal/commitauthor"
|
|
"github.com/goreleaser/goreleaser/internal/deprecate"
|
|
"github.com/goreleaser/goreleaser/internal/pipe"
|
|
"github.com/goreleaser/goreleaser/internal/tmpl"
|
|
"github.com/goreleaser/goreleaser/pkg/config"
|
|
"github.com/goreleaser/goreleaser/pkg/context"
|
|
)
|
|
|
|
// ErrIncorrectArchiveCount happens when a given filter evaluates 0 or more
|
|
// than 1 archives.
|
|
type ErrIncorrectArchiveCount struct {
|
|
goamd64 string
|
|
ids []string
|
|
archives []*artifact.Artifact
|
|
}
|
|
|
|
func (e ErrIncorrectArchiveCount) Error() string {
|
|
b := strings.Builder{}
|
|
|
|
_, _ = b.WriteString("scoop requires a single windows archive, ")
|
|
if len(e.archives) == 0 {
|
|
_, _ = b.WriteString("but no archives ")
|
|
} else {
|
|
_, _ = b.WriteString(fmt.Sprintf("but found %d archives ", len(e.archives)))
|
|
}
|
|
|
|
_, _ = b.WriteString(fmt.Sprintf("matching the given filters: goos=windows goarch=[386 amd64] goamd64=%s ids=%s", e.goamd64, e.ids))
|
|
|
|
if len(e.archives) > 0 {
|
|
names := make([]string, 0, len(e.archives))
|
|
for _, a := range e.archives {
|
|
names = append(names, a.Name)
|
|
}
|
|
_, _ = b.WriteString(fmt.Sprintf(": %s", names))
|
|
}
|
|
|
|
_, _ = b.WriteString("\nLearn more at https://goreleaser.com/errors/scoop-archive\n")
|
|
return b.String()
|
|
}
|
|
|
|
const scoopConfigExtra = "ScoopConfig"
|
|
|
|
// Pipe that builds and publishes scoop manifests.
|
|
type Pipe struct{}
|
|
|
|
func (Pipe) String() string { return "scoop manifests" }
|
|
func (Pipe) ContinueOnError() bool { return true }
|
|
func (Pipe) Skip(ctx *context.Context) bool {
|
|
return ctx.Config.Scoop.Repository.Name == "" && len(ctx.Config.Scoops) == 0
|
|
}
|
|
|
|
// Run creates the scoop manifest locally.
|
|
func (Pipe) Run(ctx *context.Context) error {
|
|
client, err := client.New(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return runAll(ctx, client)
|
|
}
|
|
|
|
// Publish scoop manifest.
|
|
func (Pipe) Publish(ctx *context.Context) error {
|
|
client, err := client.New(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return publishAll(ctx, client)
|
|
}
|
|
|
|
// Default sets the pipe defaults.
|
|
func (Pipe) Default(ctx *context.Context) error {
|
|
if !reflect.DeepEqual(ctx.Config.Scoop.Bucket, config.RepoRef{}) ||
|
|
!reflect.DeepEqual(ctx.Config.Scoop.Repository, config.RepoRef{}) {
|
|
deprecate.Notice(ctx, "scoop")
|
|
ctx.Config.Scoops = append(ctx.Config.Scoops, ctx.Config.Scoop)
|
|
}
|
|
for i := range ctx.Config.Scoops {
|
|
scoop := &ctx.Config.Scoops[i]
|
|
if scoop.Name == "" {
|
|
scoop.Name = ctx.Config.ProjectName
|
|
}
|
|
scoop.CommitAuthor = commitauthor.Default(scoop.CommitAuthor)
|
|
if scoop.CommitMessageTemplate == "" {
|
|
scoop.CommitMessageTemplate = "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
|
|
}
|
|
if scoop.Goamd64 == "" {
|
|
scoop.Goamd64 = "v1"
|
|
}
|
|
if !reflect.DeepEqual(scoop.Bucket, config.RepoRef{}) {
|
|
scoop.Repository = scoop.Bucket
|
|
deprecate.Notice(ctx, "scoops.bucket")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runAll(ctx *context.Context, cl client.ReleaserURLTemplater) error {
|
|
for _, scoop := range ctx.Config.Scoops {
|
|
err := doRun(ctx, scoop, cl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func doRun(ctx *context.Context, scoop config.Scoop, cl client.ReleaserURLTemplater) error {
|
|
filters := []artifact.Filter{
|
|
artifact.ByGoos("windows"),
|
|
artifact.ByType(artifact.UploadableArchive),
|
|
artifact.Or(
|
|
artifact.And(
|
|
artifact.ByGoarch("amd64"),
|
|
artifact.ByGoamd64(scoop.Goamd64),
|
|
),
|
|
artifact.ByGoarch("386"),
|
|
),
|
|
}
|
|
|
|
if len(scoop.IDs) > 0 {
|
|
filters = append(filters, artifact.ByIDs(scoop.IDs...))
|
|
}
|
|
|
|
filtered := ctx.Artifacts.Filter(artifact.And(filters...))
|
|
archives := filtered.List()
|
|
for _, platArchives := range filtered.GroupByPlatform() {
|
|
// there might be multiple archives, but only of for each platform
|
|
if len(platArchives) != 1 {
|
|
return ErrIncorrectArchiveCount{scoop.Goamd64, scoop.IDs, archives}
|
|
}
|
|
}
|
|
// handle no archives found whatsoever
|
|
if len(archives) == 0 {
|
|
return ErrIncorrectArchiveCount{scoop.Goamd64, scoop.IDs, archives}
|
|
}
|
|
|
|
tp := tmpl.New(ctx)
|
|
|
|
if err := tp.ApplyAll(
|
|
&scoop.Name,
|
|
&scoop.Description,
|
|
&scoop.Homepage,
|
|
&scoop.SkipUpload,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
ref, err := client.TemplateRef(tmpl.New(ctx).Apply, scoop.Repository)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scoop.Repository = ref
|
|
|
|
data, err := dataFor(ctx, scoop, cl, archives)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content, err := doBuildManifest(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filename := scoop.Name + ".json"
|
|
path := filepath.Join(ctx.Config.Dist, "scoop", scoop.Folder, filename)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
log.WithField("manifest", path).Info("writing")
|
|
if err := os.WriteFile(path, content.Bytes(), 0o644); err != nil {
|
|
return fmt.Errorf("failed to write scoop manifest: %w", err)
|
|
}
|
|
|
|
ctx.Artifacts.Add(&artifact.Artifact{
|
|
Name: filename,
|
|
Path: path,
|
|
Type: artifact.ScoopManifest,
|
|
Extra: map[string]interface{}{
|
|
scoopConfigExtra: scoop,
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func publishAll(ctx *context.Context, cli client.Client) error {
|
|
// even if one of them skips, we run them all, and then show return the
|
|
// skips all at once. this is needed so we actually create the
|
|
// `dist/foo.json` file, which is useful for debugging.
|
|
skips := pipe.SkipMemento{}
|
|
for _, manifest := range ctx.Artifacts.Filter(artifact.ByType(artifact.ScoopManifest)).List() {
|
|
err := doPublish(ctx, manifest, cli)
|
|
if err != nil && pipe.IsSkip(err) {
|
|
skips.Remember(err)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return skips.Evaluate()
|
|
}
|
|
|
|
func doPublish(ctx *context.Context, manifest *artifact.Artifact, cl client.Client) error {
|
|
scoop, err := artifact.Extra[config.Scoop](*manifest, scoopConfigExtra)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if strings.TrimSpace(scoop.SkipUpload) == "true" {
|
|
return pipe.Skip("scoop.skip_upload is true")
|
|
}
|
|
if strings.TrimSpace(scoop.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
|
|
return pipe.Skip("release is prerelease")
|
|
}
|
|
|
|
commitMessage, err := tmpl.New(ctx).Apply(scoop.CommitMessageTemplate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
author, err := commitauthor.Get(ctx, scoop.CommitAuthor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
content, err := os.ReadFile(manifest.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
repo := client.RepoFromRef(scoop.Repository)
|
|
gpath := path.Join(scoop.Folder, manifest.Name)
|
|
|
|
if scoop.Repository.Git.URL != "" {
|
|
return client.NewGitUploadClient(repo.Branch).
|
|
CreateFile(ctx, author, repo, content, gpath, commitMessage)
|
|
}
|
|
|
|
cl, err = client.NewIfToken(ctx, cl, scoop.Repository.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !scoop.Repository.PullRequest.Enabled {
|
|
return cl.CreateFile(ctx, author, repo, content, gpath, commitMessage)
|
|
}
|
|
|
|
log.Info("brews.pull_request enabled, creating a PR")
|
|
pcl, ok := cl.(client.PullRequestOpener)
|
|
if !ok {
|
|
return fmt.Errorf("client does not support pull requests")
|
|
}
|
|
|
|
if err := cl.CreateFile(ctx, author, repo, content, gpath, commitMessage); err != nil {
|
|
return err
|
|
}
|
|
|
|
return pcl.OpenPullRequest(ctx, client.Repo{
|
|
Name: scoop.Repository.PullRequest.Base.Name,
|
|
Owner: scoop.Repository.PullRequest.Base.Owner,
|
|
Branch: scoop.Repository.PullRequest.Base.Branch,
|
|
}, repo, commitMessage, scoop.Repository.PullRequest.Draft)
|
|
}
|
|
|
|
// 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
|
|
Persist []string `json:"persist,omitempty"` // Persist data between updates
|
|
PreInstall []string `json:"pre_install,omitempty"` // An array of strings, of the commands to be executed before an application is installed.
|
|
PostInstall []string `json:"post_install,omitempty"` // An array of strings, of the commands to be executed after an application is installed.
|
|
Depends []string `json:"depends,omitempty"` // A string or an array of strings.
|
|
Shortcuts [][]string `json:"shortcuts,omitempty"` // A two-dimensional array of string, specifies the shortcut values to make available in the startmenu.
|
|
}
|
|
|
|
// 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
|
|
Hash string `json:"hash"` // the archive checksum
|
|
}
|
|
|
|
func doBuildManifest(manifest Manifest) (bytes.Buffer, error) {
|
|
var result bytes.Buffer
|
|
data, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
_, err = result.Write(data)
|
|
return result, err
|
|
}
|
|
|
|
func dataFor(ctx *context.Context, scoop config.Scoop, cl client.ReleaserURLTemplater, artifacts []*artifact.Artifact) (Manifest, error) {
|
|
manifest := Manifest{
|
|
Version: ctx.Version,
|
|
Architecture: map[string]Resource{},
|
|
Homepage: scoop.Homepage,
|
|
License: scoop.License,
|
|
Description: scoop.Description,
|
|
Persist: scoop.Persist,
|
|
PreInstall: scoop.PreInstall,
|
|
PostInstall: scoop.PostInstall,
|
|
Depends: scoop.Depends,
|
|
Shortcuts: scoop.Shortcuts,
|
|
}
|
|
|
|
if scoop.URLTemplate == "" {
|
|
url, err := cl.ReleaseURLTemplate(ctx)
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
scoop.URLTemplate = url
|
|
}
|
|
|
|
for _, artifact := range artifacts {
|
|
if artifact.Goos != "windows" {
|
|
continue
|
|
}
|
|
|
|
var arch string
|
|
switch artifact.Goarch {
|
|
case "386":
|
|
arch = "32bit"
|
|
case "amd64":
|
|
arch = "64bit"
|
|
default:
|
|
continue
|
|
}
|
|
|
|
url, err := tmpl.New(ctx).WithArtifact(artifact).Apply(scoop.URLTemplate)
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
|
|
sum, err := artifact.Checksum("sha256")
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
|
|
log.
|
|
WithField("artifactExtras", artifact.Extra).
|
|
WithField("fromURLTemplate", scoop.URLTemplate).
|
|
WithField("templatedBrewURL", url).
|
|
WithField("sum", sum).
|
|
Debug("scoop url templating")
|
|
|
|
binaries, err := binaries(*artifact)
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
|
|
manifest.Architecture[arch] = Resource{
|
|
URL: url,
|
|
Bin: binaries,
|
|
Hash: sum,
|
|
}
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
|
|
func binaries(a artifact.Artifact) ([]string, error) {
|
|
// nolint: prealloc
|
|
var result []string
|
|
wrap := artifact.ExtraOr(a, artifact.ExtraWrappedIn, "")
|
|
bins, err := artifact.Extra[[]string](a, artifact.ExtraBinaries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, b := range bins {
|
|
result = append(result, filepath.Join(wrap, b))
|
|
}
|
|
return result, nil
|
|
}
|