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  }