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  }