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