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 }