github.com/windmeup/goreleaser@v1.21.95/internal/pipe/winget/winget.go (about)

     1  package winget
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/caarlos0/log"
    12  	"github.com/windmeup/goreleaser/internal/artifact"
    13  	"github.com/windmeup/goreleaser/internal/client"
    14  	"github.com/windmeup/goreleaser/internal/commitauthor"
    15  	"github.com/windmeup/goreleaser/internal/pipe"
    16  	"github.com/windmeup/goreleaser/internal/skips"
    17  	"github.com/windmeup/goreleaser/internal/tmpl"
    18  	"github.com/windmeup/goreleaser/pkg/config"
    19  	"github.com/windmeup/goreleaser/pkg/context"
    20  )
    21  
    22  var (
    23  	errNoRepoName               = pipe.Skip("winget.repository.name name is required")
    24  	errNoPublisher              = pipe.Skip("winget.publisher is required")
    25  	errNoLicense                = pipe.Skip("winget.license is required")
    26  	errNoShortDescription       = pipe.Skip("winget.short_description is required")
    27  	errInvalidPackageIdentifier = pipe.Skip("winget.package_identifier is invalid")
    28  	errSkipUpload               = pipe.Skip("winget.skip_upload is set")
    29  	errSkipUploadAuto           = pipe.Skip("winget.skip_upload is set to 'auto', and current version is a pre-release")
    30  	errMultipleArchives         = pipe.Skip("found multiple archives for the same platform, please consider filtering by id")
    31  
    32  	// copied from winget src
    33  	packageIdentifierValid = regexp.MustCompile("^[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}(\\.[^\\.\\s\\\\/:\\*\\?\"<>\\|\\x01-\\x1f]{1,32}){1,7}$")
    34  )
    35  
    36  type errNoArchivesFound struct {
    37  	goamd64 string
    38  	ids     []string
    39  }
    40  
    41  func (e errNoArchivesFound) Error() string {
    42  	return fmt.Sprintf("no zip archives found matching goos=[windows] goarch=[amd64 386] goamd64=%s ids=%v", e.goamd64, e.ids)
    43  }
    44  
    45  const wingetConfigExtra = "WingetConfig"
    46  
    47  type Pipe struct{}
    48  
    49  func (Pipe) String() string        { return "winget" }
    50  func (Pipe) ContinueOnError() bool { return true }
    51  func (p Pipe) Skip(ctx *context.Context) bool {
    52  	return skips.Any(ctx, skips.Winget) || len(ctx.Config.Winget) == 0
    53  }
    54  
    55  func (Pipe) Default(ctx *context.Context) error {
    56  	for i := range ctx.Config.Winget {
    57  		winget := &ctx.Config.Winget[i]
    58  
    59  		winget.CommitAuthor = commitauthor.Default(winget.CommitAuthor)
    60  
    61  		if winget.CommitMessageTemplate == "" {
    62  			winget.CommitMessageTemplate = "New version: {{ .PackageIdentifier }} {{ .Version }}"
    63  		}
    64  		if winget.Name == "" {
    65  			winget.Name = ctx.Config.ProjectName
    66  		}
    67  		if winget.Goamd64 == "" {
    68  			winget.Goamd64 = "v1"
    69  		}
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  func (p Pipe) Run(ctx *context.Context) error {
    76  	cli, err := client.NewReleaseClient(ctx)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	return p.runAll(ctx, cli)
    82  }
    83  
    84  // Publish .
    85  func (p Pipe) Publish(ctx *context.Context) error {
    86  	cli, err := client.New(ctx)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	return p.publishAll(ctx, cli)
    91  }
    92  
    93  func (p Pipe) runAll(ctx *context.Context, cli client.ReleaseURLTemplater) error {
    94  	for _, winget := range ctx.Config.Winget {
    95  		err := p.doRun(ctx, winget, cli)
    96  		if err != nil {
    97  			return err
    98  		}
    99  	}
   100  	return nil
   101  }
   102  
   103  func (p Pipe) doRun(ctx *context.Context, winget config.Winget, cl client.ReleaseURLTemplater) error {
   104  	if winget.Repository.Name == "" {
   105  		return errNoRepoName
   106  	}
   107  
   108  	tp := tmpl.New(ctx)
   109  
   110  	err := tp.ApplyAll(
   111  		&winget.Publisher,
   112  		&winget.Name,
   113  		&winget.Author,
   114  		&winget.PublisherURL,
   115  		&winget.PublisherSupportURL,
   116  		&winget.Homepage,
   117  		&winget.SkipUpload,
   118  		&winget.Description,
   119  		&winget.ShortDescription,
   120  		&winget.ReleaseNotesURL,
   121  		&winget.Path,
   122  		&winget.Copyright,
   123  		&winget.CopyrightURL,
   124  		&winget.License,
   125  		&winget.LicenseURL,
   126  	)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	if winget.Publisher == "" {
   132  		return errNoPublisher
   133  	}
   134  
   135  	if winget.License == "" {
   136  		return errNoLicense
   137  	}
   138  
   139  	winget.Repository, err = client.TemplateRef(tp.Apply, winget.Repository)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	if winget.ShortDescription == "" {
   145  		return errNoShortDescription
   146  	}
   147  
   148  	winget.ReleaseNotes, err = tp.WithExtraFields(tmpl.Fields{
   149  		"Changelog": ctx.ReleaseNotes,
   150  	}).Apply(winget.ReleaseNotes)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	if winget.URLTemplate == "" {
   156  		winget.URLTemplate, err = cl.ReleaseURLTemplate(ctx)
   157  		if err != nil {
   158  			return err
   159  		}
   160  	}
   161  
   162  	if winget.Path == "" {
   163  		winget.Path = filepath.Join("manifests", strings.ToLower(string(winget.Publisher[0])), winget.Publisher, winget.Name, ctx.Version)
   164  	}
   165  
   166  	filters := []artifact.Filter{
   167  		artifact.ByGoos("windows"),
   168  		artifact.ByFormats("zip"),
   169  		artifact.ByType(artifact.UploadableArchive),
   170  		artifact.Or(
   171  			artifact.ByGoarch("386"),
   172  			artifact.ByGoarch("arm64"),
   173  			artifact.And(
   174  				artifact.ByGoamd64(winget.Goamd64),
   175  				artifact.ByGoarch("amd64"),
   176  			),
   177  		),
   178  	}
   179  	if len(winget.IDs) > 0 {
   180  		filters = append(filters, artifact.ByIDs(winget.IDs...))
   181  	}
   182  	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
   183  	if len(archives) == 0 {
   184  		return errNoArchivesFound{
   185  			goamd64: winget.Goamd64,
   186  			ids:     winget.IDs,
   187  		}
   188  	}
   189  
   190  	if winget.PackageIdentifier == "" {
   191  		winget.PackageIdentifier = winget.Publisher + "." + winget.Name
   192  	}
   193  
   194  	if !packageIdentifierValid.MatchString(winget.PackageIdentifier) {
   195  		return fmt.Errorf("%w: %s", errInvalidPackageIdentifier, winget.PackageIdentifier)
   196  	}
   197  
   198  	if err := createYAML(ctx, winget, Version{
   199  		PackageIdentifier: winget.PackageIdentifier,
   200  		PackageVersion:    ctx.Version,
   201  		DefaultLocale:     defaultLocale,
   202  		ManifestType:      "version",
   203  		ManifestVersion:   manifestVersion,
   204  	}, artifact.WingetVersion); err != nil {
   205  		return err
   206  	}
   207  
   208  	var deps []PackageDependency
   209  	for _, dep := range winget.Dependencies {
   210  		if err := tp.ApplyAll(&dep.MinimumVersion, &dep.PackageIdentifier); err != nil {
   211  			return err
   212  		}
   213  		deps = append(deps, PackageDependency{
   214  			PackageIdentifier: dep.PackageIdentifier,
   215  			MinimumVersion:    dep.MinimumVersion,
   216  		})
   217  	}
   218  
   219  	installer := Installer{
   220  		PackageIdentifier: winget.PackageIdentifier,
   221  		PackageVersion:    ctx.Version,
   222  		InstallerLocale:   defaultLocale,
   223  		InstallerType:     "zip",
   224  		Commands:          []string{},
   225  		ReleaseDate:       ctx.Date.Format(time.DateOnly),
   226  		Installers:        []InstallerItem{},
   227  		ManifestType:      "installer",
   228  		ManifestVersion:   manifestVersion,
   229  		Dependencies: Dependencies{
   230  			PackageDependencies: deps,
   231  		},
   232  	}
   233  
   234  	var amd64Count, i386count int
   235  	for _, archive := range archives {
   236  		sha256, err := archive.Checksum("sha256")
   237  		if err != nil {
   238  			return err
   239  		}
   240  		var files []InstallerItemFile
   241  		folder := artifact.ExtraOr(*archive, artifact.ExtraWrappedIn, ".")
   242  		for _, bin := range artifact.ExtraOr(*archive, artifact.ExtraBinaries, []string{}) {
   243  			files = append(files, InstallerItemFile{
   244  				RelativeFilePath:     strings.ReplaceAll(filepath.Join(folder, bin), "/", "\\"),
   245  				PortableCommandAlias: strings.TrimSuffix(filepath.Base(bin), ".exe"),
   246  			})
   247  		}
   248  		url, err := tmpl.New(ctx).WithArtifact(archive).Apply(winget.URLTemplate)
   249  		if err != nil {
   250  			return err
   251  		}
   252  		installer.Installers = append(installer.Installers, InstallerItem{
   253  			Architecture:         fromGoArch[archive.Goarch],
   254  			NestedInstallerType:  "portable",
   255  			NestedInstallerFiles: files,
   256  			InstallerURL:         url,
   257  			InstallerSha256:      sha256,
   258  			UpgradeBehavior:      "uninstallPrevious",
   259  		})
   260  		switch archive.Goarch {
   261  		case "386":
   262  			i386count++
   263  		case "amd64":
   264  			amd64Count++
   265  		}
   266  	}
   267  
   268  	if i386count > 1 || amd64Count > 1 {
   269  		return errMultipleArchives
   270  	}
   271  
   272  	if err := createYAML(ctx, winget, installer, artifact.WingetInstaller); err != nil {
   273  		return err
   274  	}
   275  
   276  	return createYAML(ctx, winget, Locale{
   277  		PackageIdentifier:   winget.PackageIdentifier,
   278  		PackageVersion:      ctx.Version,
   279  		PackageLocale:       defaultLocale,
   280  		Publisher:           winget.Publisher,
   281  		PublisherURL:        winget.PublisherURL,
   282  		PublisherSupportURL: winget.PublisherSupportURL,
   283  		Author:              winget.Author,
   284  		PackageName:         winget.Name,
   285  		PackageURL:          winget.Homepage,
   286  		License:             winget.License,
   287  		LicenseURL:          winget.LicenseURL,
   288  		Copyright:           winget.Copyright,
   289  		CopyrightURL:        winget.CopyrightURL,
   290  		ShortDescription:    winget.ShortDescription,
   291  		Description:         winget.Description,
   292  		Moniker:             winget.Name,
   293  		Tags:                winget.Tags,
   294  		ReleaseNotes:        winget.ReleaseNotes,
   295  		ReleaseNotesURL:     winget.ReleaseNotesURL,
   296  		ManifestType:        "defaultLocale",
   297  		ManifestVersion:     manifestVersion,
   298  	}, artifact.WingetDefaultLocale)
   299  }
   300  
   301  func (p Pipe) publishAll(ctx *context.Context, cli client.Client) error {
   302  	skips := pipe.SkipMemento{}
   303  	for _, files := range ctx.Artifacts.Filter(artifact.Or(
   304  		artifact.ByType(artifact.WingetInstaller),
   305  		artifact.ByType(artifact.WingetVersion),
   306  		artifact.ByType(artifact.WingetDefaultLocale),
   307  	)).GroupByID() {
   308  		err := doPublish(ctx, cli, files)
   309  		if err != nil && pipe.IsSkip(err) {
   310  			skips.Remember(err)
   311  			continue
   312  		}
   313  		if err != nil {
   314  			return err
   315  		}
   316  	}
   317  	return skips.Evaluate()
   318  }
   319  
   320  func doPublish(ctx *context.Context, cl client.Client, wingets []*artifact.Artifact) error {
   321  	winget, err := artifact.Extra[config.Winget](*wingets[0], wingetConfigExtra)
   322  	if err != nil {
   323  		return err
   324  	}
   325  
   326  	if strings.TrimSpace(winget.SkipUpload) == "true" {
   327  		return errSkipUpload
   328  	}
   329  
   330  	if strings.TrimSpace(winget.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
   331  		return errSkipUploadAuto
   332  	}
   333  
   334  	msg, err := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
   335  		"PackageIdentifier": winget.PackageIdentifier,
   336  	}).Apply(winget.CommitMessageTemplate)
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	author, err := commitauthor.Get(ctx, winget.CommitAuthor)
   342  	if err != nil {
   343  		return err
   344  	}
   345  
   346  	repo := client.RepoFromRef(winget.Repository)
   347  
   348  	var files []client.RepoFile
   349  	for _, pkg := range wingets {
   350  		content, err := os.ReadFile(pkg.Path)
   351  		if err != nil {
   352  			return err
   353  		}
   354  		files = append(files, client.RepoFile{
   355  			Content:    content,
   356  			Path:       filepath.Join(winget.Path, pkg.Name),
   357  			Identifier: repoFileID(pkg.Type),
   358  		})
   359  	}
   360  
   361  	if winget.Repository.Git.URL != "" {
   362  		return client.NewGitUploadClient(repo.Branch).
   363  			CreateFiles(ctx, author, repo, msg, files)
   364  	}
   365  
   366  	cl, err = client.NewIfToken(ctx, cl, winget.Repository.Token)
   367  	if err != nil {
   368  		return err
   369  	}
   370  
   371  	for _, file := range files {
   372  		if err := cl.CreateFile(
   373  			ctx,
   374  			author,
   375  			repo,
   376  			file.Content,
   377  			file.Path,
   378  			msg+": add "+file.Identifier,
   379  		); err != nil {
   380  			return err
   381  		}
   382  	}
   383  
   384  	if !winget.Repository.PullRequest.Enabled {
   385  		log.Debug("wingets.pull_request disabled")
   386  		return nil
   387  	}
   388  
   389  	log.Info("winget.pull_request enabled, creating a PR")
   390  	pcl, ok := cl.(client.PullRequestOpener)
   391  	if !ok {
   392  		return fmt.Errorf("client does not support pull requests")
   393  	}
   394  
   395  	return pcl.OpenPullRequest(ctx, client.Repo{
   396  		Name:   winget.Repository.PullRequest.Base.Name,
   397  		Owner:  winget.Repository.PullRequest.Base.Owner,
   398  		Branch: winget.Repository.PullRequest.Base.Branch,
   399  	}, repo, msg, winget.Repository.PullRequest.Draft)
   400  }
   401  
   402  func langserverLineFor(tp artifact.Type) string {
   403  	switch tp {
   404  	case artifact.WingetInstaller:
   405  		return installerLangServer
   406  	case artifact.WingetDefaultLocale:
   407  		return defaultLocaleLangServer
   408  	default:
   409  		return versionLangServer
   410  	}
   411  }
   412  
   413  func extFor(tp artifact.Type) string {
   414  	switch tp {
   415  	case artifact.WingetVersion:
   416  		return ".yaml"
   417  	case artifact.WingetInstaller:
   418  		return ".installer.yaml"
   419  	case artifact.WingetDefaultLocale:
   420  		return ".locale." + defaultLocale + ".yaml"
   421  	default:
   422  		// should never happen
   423  		return ""
   424  	}
   425  }
   426  
   427  func repoFileID(tp artifact.Type) string {
   428  	switch tp {
   429  	case artifact.WingetVersion:
   430  		return "version"
   431  	case artifact.WingetInstaller:
   432  		return "installer"
   433  	case artifact.WingetDefaultLocale:
   434  		return "locale"
   435  	default:
   436  		// should never happen
   437  		return ""
   438  	}
   439  }