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