Repository: goreleaser/goreleaser
Pull request #2509 (2021-09-25 01:25:41)

feat: publish: GoFish integration
Signed-off-by: Engin Diri <engin.diri@mail.schwarz>

This PR adds the support for [GoFish](https://gofi.sh/). I recently wanted to publish my minectl project to GoFish, and while checking for open issues for GoFish support i stumbeled over this #2420.

So I created the GoFish integration.

I hope its okay. 😄 

vercel[bot]: @dirien is attempting to deploy a commit to the **goreleaser** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=goreleaser&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22bfa0d2f3fd77462e01657b8be614ceeca6ecab1f%22%7D%2C%22id%22%3A%22QmWXMEgC2cxyjn8yR6YrzGeG9K3boYq6gMDt2ZFEtxoYTB%22%2C%22org%22%3A%22goreleaser%22%2C%22prId%22%3A2509%2C%22repo%22%3A%22goreleaser%22%7D).


dirien: I tested it via a local push with the build cli

here is the repo:
-> https://github.com/dirien/gofish-rig/blob/main/Food/minectl.lua

```yaml
rigs:
  - rig:
      owner: dirien
      name: gofish-rig

    commit_author:
      name: dirien
      email: engin.diri@mail.schwarz

    homepage: "https://github.com/dirien/minectl"
    description: "minectl️️ 🗺️ is a cli for creating Minecraft (java or bedrock) server on different cloud provider."
    license: "Apache License 2.0"
    skip_upload: auto
```

```bash
  • gofish fish food cookbook
      • writing                   food=dist/minectl.lua
   • calculating checksums
      • checksumming              file=minectl_0.10.0_darwin_amd64.tar.gz
      • checksumming              file=minectl_0.10.0_linux_amd64.tar.gz
      • checksumming              file=minectl_0.10.0_linux_arm64.tar.gz
      • checksumming              file=minectl_0.10.0_darwin_arm64.tar.gz
      • checksumming              file=minectl_0.10.0_windows_amd64.zip
   • publishing       
      • gofish fish food cookbook
         • pushing                   food=Food/minectl.lua repo=dirien/gofish-rig
   • release succeeded after 19.73s
```
codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2509?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2509](https://codecov.io/gh/goreleaser/goreleaser/pull/2509?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (569ef27) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/3d62f95fd53bfa037e3e39894eefe637a8758333?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (3d62f95) will **decrease** coverage by `0.26%`.
> The diff coverage is `76.35%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2509/graphs/tree.svg?width=650&height=150&src=pr&token=3bJvv6b7hN&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)](https://codecov.io/gh/goreleaser/goreleaser/pull/2509?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##           master    #2509      +/-   ##
==========================================
- Coverage   83.36%   83.09%   -0.27%     
==========================================
  Files          93       94       +1     
  Lines        6450     6708     +258     
==========================================
+ Hits         5377     5574     +197     
- Misses        916      967      +51     
- Partials      157      167      +10     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2509?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/artifact/artifact.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvYXJ0aWZhY3QvYXJ0aWZhY3QuZ28=) | `96.71% <0.00%> (-1.29%)` | :arrow_down: |
| [internal/pipe/publish/publish.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9wdWJsaXNoL3B1Ymxpc2guZ28=) | `80.00% <ø> (ø)` | |
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `90.72% <ø> (ø)` | |
| [internal/pipe/gofish/gofish.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9nb2Zpc2gvZ29maXNoLmdv) | `76.67% <76.67%> (ø)` | |
| [internal/golden/golden.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvZ29sZGVuL2dvbGRlbi5nbw==) | `76.66% <100.00%> (+2.59%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2509?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2509?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [3d62f95...569ef27](https://codecov.io/gh/goreleaser/goreleaser/pull/2509?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).

dirien: @caarlos0: Help... I dont know how the fix the htmltest. I dont see anything wrong on the way in included the new md file. But the internal link to the `/customization/template/` delivers a 404 in the test...
dirien: @caarlos0, did the changes. Ready for the next round 😄 
caarlos0: > @caarlos0: Help... I dont know how the fix the htmltest. I dont see anything wrong on the way in included the new md file. But the internal link to the `/customization/template/` delivers a 404 in the test...

pretty sure you can ignore that... but will check later :) 
dirien: > lgtm, just need to merge with master again (sorry)

All Good, GG! 😄 

Mortal Combat Voice: Round 2.
dirien: Ding ding, next round 🔔 
caarlos0: looks good, will hold the merge a bit in case we need another bugfix release (looks like)!

thanks for the hard work on this 🙏
dirien: @caarlos0, your welcome! Told @cpanato, to wait for the cosign rig 😄 

Patch:
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index ff9df69c..3bbed3b2 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -50,6 +50,8 @@ const (
 	UploadableSourceArchive
 	// BrewTap is an uploadable homebrew tap recipe file.
 	BrewTap
+	// GoFishRig is an uploadable Rigs rig food file.
+	GoFishRig
 	// ScoopManifest is an uploadable scoop manifest file.
 	ScoopManifest
 )
@@ -78,6 +80,8 @@ func (t Type) String() string {
 		return "Source"
 	case BrewTap:
 		return "Brew Tap"
+	case GoFishRig:
+		return "GoFish Rig"
 	case ScoopManifest:
 		return "Scoop Manifest"
 	default:
diff --git a/internal/git/config.go b/internal/git/config.go
index 68dec2da..9de36669 100644
--- a/internal/git/config.go
+++ b/internal/git/config.go
@@ -6,6 +6,7 @@ import (
 	"net/url"
 	"strings"
 
+	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/pkg/config"
 )
 
@@ -18,6 +19,7 @@ func ExtractRepoFromConfig() (result config.Repo, err error) {
 	if err != nil {
 		return result, fmt.Errorf("no remote configured to list refs from")
 	}
+	log.WithField("rawurl", out).Debugf("got git url")
 	return ExtractRepoFromURL(out)
 }
 
@@ -55,8 +57,10 @@ func ExtractRepoFromURL(rawurl string) (config.Repo, error) {
 	if len(ss) < 2 {
 		return config.Repo{}, fmt.Errorf("unsupported repository URL: %s", rawurl)
 	}
-	return config.Repo{
+	repo := config.Repo{
 		Owner: strings.Join(ss[:len(ss)-1], "/"),
 		Name:  ss[len(ss)-1],
-	}, nil
+	}
+	log.WithField("owner", repo.Owner).WithField("name", repo.Name).Debugf("parsed url")
+	return repo, nil
 }
diff --git a/internal/golden/golden.go b/internal/golden/golden.go
index 9598959f..0c27dd76 100644
--- a/internal/golden/golden.go
+++ b/internal/golden/golden.go
@@ -32,6 +32,11 @@ func RequireEqualRb(tb testing.TB, out []byte) {
 	doRequireEqual(tb, out, ".rb")
 }
 
+func RequireEqualLua(tb testing.TB, out []byte) {
+	tb.Helper()
+	doRequireEqual(tb, out, ".lua")
+}
+
 func RequireEqualYaml(tb testing.TB, out []byte) {
 	tb.Helper()
 	doRequireEqual(tb, out, ".yml")
diff --git a/internal/pipe/gofish/doc.go b/internal/pipe/gofish/doc.go
new file mode 100644
index 00000000..bddef256
--- /dev/null
+++ b/internal/pipe/gofish/doc.go
@@ -0,0 +1,3 @@
+// Package gofish implements the Pipe, providing food generation and
+// uploading it to a configured repo.
+package gofish
diff --git a/internal/pipe/gofish/food_template.go b/internal/pipe/gofish/food_template.go
new file mode 100644
index 00000000..a6aca633
--- /dev/null
+++ b/internal/pipe/gofish/food_template.go
@@ -0,0 +1,49 @@
+package gofish
+
+type templateData struct {
+	Name            string
+	Desc            string
+	Homepage        string
+	Version         string
+	License         string
+	ReleasePackages []releasePackage
+}
+
+type releasePackage struct {
+	DownloadURL string
+	SHA256      string
+	OS          string
+	Arch        string
+}
+
+const foodTemplate = `local name = "{{ .Name }}"
+local version = "{{ .Version }}"
+
+food = {
+    name = name,
+    description = "{{ .Desc }}",
+    license = "{{ .License }}",
+    homepage = "{{ .Homepage }}",
+    version = version,
+    packages = {
+    {{- range $element := .ReleasePackages}}
+    {{- if ne $element.OS ""}}
+		{
+            os = "{{ $element.OS }}",
+            arch = "{{ $element.Arch }}",
+            url = "{{ $element.DownloadURL }}",
+            sha256 = "{{ $element.SHA256 }}",
+            resources = {
+                {
+                    path = {{if ne $element.OS "windows"}}name{{else}}name .. ".exe"{{end}},
+                    installpath = {{if ne $element.OS "windows"}}"bin/" .. name,{{else}}"bin\\" .. name .. ".exe"{{end}}
+                    {{- if ne $element.OS "windows"}}
+                    executable = true
+					{{- end }}
+                }
+            }
+        },
+    {{- end }}
+    {{- end}}
+    }
+}`
diff --git a/internal/pipe/gofish/gofish.go b/internal/pipe/gofish/gofish.go
new file mode 100644
index 00000000..1995eeb7
--- /dev/null
+++ b/internal/pipe/gofish/gofish.go
@@ -0,0 +1,302 @@
+package gofish
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"text/template"
+
+	"github.com/apex/log"
+	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/client"
+	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+)
+
+const goFishConfigExtra = "GoFishConfig"
+
+const foodFolder = "Food"
+
+var ErrNoArchivesFound = errors.New("no linux/macos/windows archives found")
+
+var ErrMultipleArchivesSameOS = errors.New("one rig can handle only archive of an OS/Arch combination. Consider using ids in the gofish section")
+
+// ErrTokenTypeNotImplementedForGoFish indicates that a new token type was not implemented for this pipe.
+type ErrTokenTypeNotImplementedForGoFish struct {
+	TokenType context.TokenType
+}
+
+func (e ErrTokenTypeNotImplementedForGoFish) Error() string {
+	return fmt.Sprintf("token type %q not implemented for gofish pipe", e.TokenType)
+}
+
+// Pipe for goFish deployment.
+type Pipe struct{}
+
+func (Pipe) String() string                 { return "gofish fish food cookbook" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Rigs) == 0 }
+
+func (Pipe) Default(ctx *context.Context) error {
+	for i := range ctx.Config.Rigs {
+		goFish := &ctx.Config.Rigs[i]
+
+		if goFish.CommitAuthor.Name == "" {
+			goFish.CommitAuthor.Name = "goreleaserbot"
+		}
+		if goFish.CommitAuthor.Email == "" {
+			goFish.CommitAuthor.Email = "goreleaser@carlosbecker.com"
+		}
+		if goFish.CommitMessageTemplate == "" {
+			goFish.CommitMessageTemplate = "GoFish fish food update for {{ .ProjectName }} version {{ .Tag }}"
+		}
+		if goFish.Name == "" {
+			goFish.Name = ctx.Config.ProjectName
+		}
+		if goFish.Goarm == "" {
+			goFish.Goarm = "6"
+		}
+	}
+
+	return nil
+}
+
+func (Pipe) Run(ctx *context.Context) error {
+	cli, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+
+	return runAll(ctx, cli)
+}
+
+func runAll(ctx *context.Context, cli client.Client) error {
+	for _, goFish := range ctx.Config.Rigs {
+		err := doRun(ctx, goFish, cli)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func doRun(ctx *context.Context, goFish config.GoFish, cl client.Client) error {
+	if goFish.Rig.Name == "" {
+		return pipe.Skip("Rigs rig name is not set")
+	}
+
+	filters := []artifact.Filter{
+		artifact.Or(
+			artifact.ByGoos("darwin"),
+			artifact.ByGoos("linux"),
+			artifact.ByGoos("windows"),
+		),
+		artifact.Or(
+			artifact.ByGoarch("amd64"),
+			artifact.ByGoarch("arm64"),
+			artifact.And(
+				artifact.ByGoarch("arm"),
+				artifact.ByGoarm(goFish.Goarm),
+			),
+		),
+		artifact.ByType(artifact.UploadableArchive),
+	}
+	if len(goFish.IDs) > 0 {
+		filters = append(filters, artifact.ByIDs(goFish.IDs...))
+	}
+
+	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
+	if len(archives) == 0 {
+		return ErrNoArchivesFound
+	}
+
+	name, err := tmpl.New(ctx).Apply(goFish.Name)
+	if err != nil {
+		return err
+	}
+	goFish.Name = name
+
+	content, err := buildFood(ctx, goFish, cl, archives)
+	if err != nil {
+		return err
+	}
+
+	filename := goFish.Name + ".lua"
+	path := filepath.Join(ctx.Config.Dist, filename)
+	log.WithField("food", path).Info("writing")
+	if err := os.WriteFile(path, []byte(content), 0o644); err != nil { //nolint: gosec
+		return fmt.Errorf("failed to write gofish food: %w", err)
+	}
+
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name: filename,
+		Path: path,
+		Type: artifact.GoFishRig,
+		Extra: map[string]interface{}{
+			goFishConfigExtra: goFish,
+		},
+	})
+
+	return nil
+}
+
+func buildFood(ctx *context.Context, goFish config.GoFish, client client.Client, artifacts []*artifact.Artifact) (string, error) {
+	data, err := dataFor(ctx, goFish, client, artifacts)
+	if err != nil {
+		return "", err
+	}
+	return doBuildFood(ctx, data)
+}
+
+func doBuildFood(ctx *context.Context, data templateData) (string, error) {
+	t, err := template.
+		New(data.Name).
+		Parse(foodTemplate)
+	if err != nil {
+		return "", err
+	}
+	var out bytes.Buffer
+	if err := t.Execute(&out, data); err != nil {
+		return "", err
+	}
+
+	content, err := tmpl.New(ctx).Apply(out.String())
+	if err != nil {
+		return "", err
+	}
+	out.Reset()
+
+	// Sanitize the template output and get rid of trailing whitespace.
+	var (
+		r = strings.NewReader(content)
+		s = bufio.NewScanner(r)
+	)
+	for s.Scan() {
+		l := strings.TrimRight(s.Text(), " ")
+		_, _ = out.WriteString(l)
+		_ = out.WriteByte('\n')
+	}
+	if err := s.Err(); err != nil {
+		return "", err
+	}
+
+	return out.String(), nil
+}
+
+func dataFor(ctx *context.Context, cfg config.GoFish, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) {
+	result := templateData{
+		Name:     cfg.Name,
+		Desc:     cfg.Description,
+		Homepage: cfg.Homepage,
+		Version:  ctx.Version,
+		License:  cfg.License,
+	}
+
+	for _, artifact := range artifacts {
+		sum, err := artifact.Checksum("sha256")
+		if err != nil {
+			return result, err
+		}
+
+		if cfg.URLTemplate == "" {
+			url, err := cl.ReleaseURLTemplate(ctx)
+			if err != nil {
+				if client.IsNotImplementedErr(err) {
+					return result, ErrTokenTypeNotImplementedForGoFish{ctx.TokenType}
+				}
+				return result, err
+			}
+			cfg.URLTemplate = url
+		}
+		url, err := tmpl.New(ctx).WithArtifact(artifact, map[string]string{}).Apply(cfg.URLTemplate)
+		if err != nil {
+			return result, err
+		}
+		releasePackage := releasePackage{
+			DownloadURL: url,
+			SHA256:      sum,
+			OS:          artifact.Goos,
+			Arch:        artifact.Goarch,
+		}
+		for _, v := range result.ReleasePackages {
+			if v.OS == artifact.Goos && v.Arch == artifact.Goarch {
+				return result, ErrMultipleArchivesSameOS
+			}
+		}
+		result.ReleasePackages = append(result.ReleasePackages, releasePackage)
+	}
+
+	return result, nil
+}
+
+// Publish gofish rig.
+func (Pipe) Publish(ctx *context.Context) error {
+	cli, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+	return publishAll(ctx, cli)
+}
+
+func publishAll(ctx *context.Context, cli client.Client) error {
+	// even if one of them skips, we run them all, and then show return the skips all at once.
+	// this is needed so we actually create the `dist/foo.lua` file, which is useful for debugging.
+	skips := pipe.SkipMemento{}
+	for _, rig := range ctx.Artifacts.Filter(artifact.ByType(artifact.GoFishRig)).List() {
+		err := doPublish(ctx, rig, cli)
+		if err != nil && pipe.IsSkip(err) {
+			skips.Remember(err)
+			continue
+		}
+		if err != nil {
+			return err
+		}
+	}
+	return skips.Evaluate()
+}
+
+func doPublish(ctx *context.Context, food *artifact.Artifact, cl client.Client) error {
+	rig := food.Extra[goFishConfigExtra].(config.GoFish)
+	var err error
+	cl, err = client.NewIfToken(ctx, cl, rig.Rig.Token)
+	if err != nil {
+		return err
+	}
+
+	if strings.TrimSpace(rig.SkipUpload) == "true" {
+		return pipe.Skip("rig.skip_upload is set")
+	}
+
+	if strings.TrimSpace(rig.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
+		return pipe.Skip("prerelease detected with 'auto' upload, skipping gofish publish")
+	}
+
+	repo := client.RepoFromRef(rig.Rig)
+
+	gpath := buildFoodPath(foodFolder, food.Name)
+	log.WithField("food", gpath).
+		WithField("repo", repo.String()).
+		Info("pushing")
+
+	msg, err := tmpl.New(ctx).Apply(rig.CommitMessageTemplate)
+	if err != nil {
+		return err
+	}
+
+	content, err := os.ReadFile(food.Path)
+	if err != nil {
+		return err
+	}
+
+	return cl.CreateFile(ctx, rig.CommitAuthor, repo, content, gpath, msg)
+}
+
+func buildFoodPath(folder, filename string) string {
+	return path.Join(folder, filename)
+}
diff --git a/internal/pipe/publish/publish.go b/internal/pipe/publish/publish.go
index 75b38a90..c71aed94 100644
--- a/internal/pipe/publish/publish.go
+++ b/internal/pipe/publish/publish.go
@@ -4,6 +4,8 @@ package publish
 import (
 	"fmt"
 
+	"github.com/goreleaser/goreleaser/internal/pipe/gofish"
+
 	"github.com/goreleaser/goreleaser/internal/middleware/errhandler"
 	"github.com/goreleaser/goreleaser/internal/middleware/logging"
 	"github.com/goreleaser/goreleaser/internal/middleware/skip"
@@ -43,6 +45,7 @@ var publishers = []Publisher{
 	release.Pipe{},
 	// brew and scoop use the release URL, so, they should be last
 	brew.Pipe{},
+	gofish.Pipe{},
 	scoop.Pipe{},
 	milestone.Pipe{},
 }
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index 65c4d1f1..d998f7ef 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -17,6 +17,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/effectiveconfig"
 	"github.com/goreleaser/goreleaser/internal/pipe/env"
 	"github.com/goreleaser/goreleaser/internal/pipe/git"
+	"github.com/goreleaser/goreleaser/internal/pipe/gofish"
 	"github.com/goreleaser/goreleaser/internal/pipe/gomod"
 	"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
 	"github.com/goreleaser/goreleaser/internal/pipe/publish"
@@ -63,6 +64,7 @@ var Pipeline = append(
 	nfpm.Pipe{},          // archive via fpm (deb, rpm) using "native" go impl
 	snapcraft.Pipe{},     // archive via snapcraft (snap)
 	brew.Pipe{},          // create brew tap
+	gofish.Pipe{},        // create gofish rig
 	scoop.Pipe{},         // create scoop buckets
 	checksums.Pipe{},     // checksums of the files
 	sign.Pipe{},          // sign artifacts
diff --git a/pkg/config/config.go b/pkg/config/config.go
index f2e3be7d..76293396 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -88,6 +88,21 @@ func (r Repo) String() string {
 	return r.Owner + "/" + r.Name
 }
 
+// GoFish contains the gofish section.
+type GoFish struct {
+	Name                  string       `yaml:",omitempty"`
+	Rig                   RepoRef      `yaml:",omitempty"`
+	CommitAuthor          CommitAuthor `yaml:"commit_author,omitempty"`
+	CommitMessageTemplate string       `yaml:"commit_msg_template,omitempty"`
+	Description           string       `yaml:",omitempty"`
+	Homepage              string       `yaml:",omitempty"`
+	License               string       `yaml:",omitempty"`
+	SkipUpload            string       `yaml:"skip_upload,omitempty"`
+	URLTemplate           string       `yaml:"url_template,omitempty"`
+	IDs                   []string     `yaml:"ids,omitempty"`
+	Goarm                 string       `yaml:"goarm,omitempty"`
+}
+
 // Homebrew contains the brew section.
 type Homebrew struct {
 	Name                  string               `yaml:",omitempty"`
@@ -648,6 +663,7 @@ type Project struct {
 	Release         Release          `yaml:",omitempty"`
 	Milestones      []Milestone      `yaml:",omitempty"`
 	Brews           []Homebrew       `yaml:",omitempty"`
+	Rigs            []GoFish         `yaml:",omitempty"`
 	Scoop           Scoop            `yaml:",omitempty"`
 	Builds          []Build          `yaml:",omitempty"`
 	Archives        []Archive        `yaml:",omitempty"`
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index ea7c9cd2..6aa5dfc2 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -13,6 +13,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/checksums"
 	"github.com/goreleaser/goreleaser/internal/pipe/discord"
 	"github.com/goreleaser/goreleaser/internal/pipe/docker"
+	"github.com/goreleaser/goreleaser/internal/pipe/gofish"
 	"github.com/goreleaser/goreleaser/internal/pipe/gomod"
 	"github.com/goreleaser/goreleaser/internal/pipe/milestone"
 	"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
@@ -60,6 +61,7 @@ var Defaulters = []Defaulter{
 	artifactory.Pipe{},
 	blob.Pipe{},
 	brew.Pipe{},
+	gofish.Pipe{},
 	scoop.Pipe{},
 	discord.Pipe{},
 	reddit.Pipe{},
diff --git a/www/docs/customization/gofish.md b/www/docs/customization/gofish.md
new file mode 100644
index 00000000..472ce607
--- /dev/null
+++ b/www/docs/customization/gofish.md
@@ -0,0 +1,120 @@
+---
+title: GoFish
+---
+
+After releasing to GitHub or GitLab, GoReleaser can generate and publish
+a _Fish Food_ Cookbook into a repository that you have access to.
+
+The `rigs` section specifies how the fish food should be created.
+You can check the
+[GoFish documentation](https://gofi.sh/#intro)
+and the
+[Fish food cookbook](https://gofi.sh/#cookbook)
+for more details.
+
+!!! warning
+  If you have multiple 32-bit arm versions in each `build` section, and
+  you do not specify any `ids` in the rigs section, it will default to all
+  artifacts and GoReleaser will fail.
+
+```yaml
+# .goreleaser.yml
+rigs:
+  -
+    # Name template of the recipe
+    # Default to project name
+    name: myproject
+
+    # IDs of the archives to use.
+    # Defaults to all.
+    ids:
+    - foo
+    - bar
+
+    # GOARM to specify which 32-bit arm version to use if there are multiple versions
+    # from the build section. GoFish fish food support atm only one 32-bit version.
+    # Default is 6 for all artifacts or each id if there a multiple versions.
+    goarm: 6
+
+    # NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the
+    # same kind. We will probably unify this in the next major version like it is done with scoop.
+
+    # GitHub/GitLab repository to push the fish food to
+    # Gitea is not supported yet, but the support coming
+    rig:
+      owner: repo-owner
+      name: gofish-rig
+      # Optionally a token can be provided, if it differs from the token provided to GoReleaser
+      token: "{{ .Env.GOFISH_RIG_GITHUB_TOKEN }}"
+
+    # Template for the url which is determined by the given Token (github or gitlab)
+    # Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
+    # Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}"
+    # Default for gitea is "https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
+    url_template: "http://github.mycompany.com/foo/bar/releases/{{ .Tag }}/{{ .ArtifactName }}"
+
+    # Git author used to commit to the repository.
+    # Defaults are shown.
+    commit_author:
+      name: goreleaserbot
+      email: goreleaser@carlosbecker.com
+
+    # The project name and current git tag are used in the format string.
+    commit_msg_template: "GoFish fish food update for {{ .ProjectName }} version {{ .Tag }}"
+
+    # Your app's homepage.
+    # Default is empty.
+    homepage: "https://example.com/"
+
+    # Template of your app's description.
+    # Default is empty.
+    description: "Software to create fast and easy drum rolls."
+
+    # SPDX identifier of your app's license.
+    # Default is empty.
+    license: "MIT"
+
+    # Setting this will prevent goreleaser to actually try to commit the updated
+    # fish food - instead, the fish food file will be stored on the dist folder only,
+    # leaving the responsibility of publishing it to the user.
+    # If set to auto, the release will not be uploaded to the GoFish rig
+    # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1
+    # Default is false.
+    skip_upload: true
+```
+
+!!! tip
+  Learn more about the [name template engine](/customization/templates/).
+
+By defining the `rigs` section, GoReleaser will take care of publishing the
+GoFish rig.
+Assuming that the current tag is `v1.2.3`, the above configuration will generate a
+`program.lua` fish food in the `Food` folder of `user/gofish-rig` repository:
+
+```lua
+local name = "Program"
+local version = "1.2.3"
+
+food = {
+  name = name,
+  description = "How to use this binary",
+  license = "MIT",
+  homepage = "https://github.com/user/repo",
+  version = version,
+  packages = {
+    {
+      os = "darwin",
+      arch = "amd64",
+      url = "https://github.com/user/repo/releases/download/v1.2.3/program_v1.2.3_macOs_64bit.zip",
+      sha256 = "9ee30fc358fae8d248a2d7538957089885da321dca3f09e3296fe2058e7fff74",
+      resources = {
+        {
+          path = name,
+          installpath = "bin/" .. name,
+          executable = true
+        }
+      }
+    }
+  }
+}
+```
diff --git a/www/docs/scm/gitlab.md b/www/docs/scm/gitlab.md
index 5cfc47bd..b98636e8 100644
--- a/www/docs/scm/gitlab.md
+++ b/www/docs/scm/gitlab.md
@@ -3,7 +3,7 @@
 ## API Token
 
 GoReleaser requires an API token with the `api` scope selected to deploy the artifacts to GitLab.
-You can create one [here](https://gitlab.com/profile/personal_access_tokens).
+You can create one [here](https://gitlab.com/-/profile/personal_access_tokens).
 
 This token should be added to the environment variables as `GITLAB_TOKEN`.
 
diff --git a/www/mkdocs.yml b/www/mkdocs.yml
index 7c41bb9b..09291291 100644
--- a/www/mkdocs.yml
+++ b/www/mkdocs.yml
@@ -94,6 +94,7 @@ nav:
     - customization/blob.md
     - customization/fury.md
     - customization/homebrew.md
+    - customization/gofish.md
     - customization/scoop.md
     - customization/release.md
     - customization/artifactory.md


Test patch:
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
index 6d226226..94b0c4c0 100644
--- a/internal/artifact/artifact_test.go
+++ b/internal/artifact/artifact_test.go
@@ -307,6 +307,7 @@ func TestTypeToString(t *testing.T) {
 		Signature,
 		UploadableSourceArchive,
 		BrewTap,
+		GoFishRig,
 		ScoopManifest,
 	} {
 		t.Run(a.String(), func(t *testing.T) {
diff --git a/internal/git/config_test.go b/internal/git/config_test.go
index c6b0bccf..063db03a 100644
--- a/internal/git/config_test.go
+++ b/internal/git/config_test.go
@@ -35,6 +35,7 @@ func TestExtractRepoFromURL(t *testing.T) {
 	for _, url := range []string{
 		"git@github.com:goreleaser/goreleaser.git",
 		"git@custom:goreleaser/goreleaser.git",
+		"https://foo@github.com/goreleaser/goreleaser",
 		"https://github.com/goreleaser/goreleaser.git",
 		"https://github.enterprise.com/goreleaser/goreleaser.git",
 		"https://gitlab-ci-token:SOME_TOKEN@gitlab.yourcompany.com/goreleaser/goreleaser.git",
@@ -50,6 +51,7 @@ func TestExtractRepoFromURL(t *testing.T) {
 	for _, url := range []string{
 		"git@custom:group/nested/goreleaser/goreleaser.git",
 		"https://gitlab.mycompany.com/group/nested/goreleaser/goreleaser.git",
+		"https://gitlab-ci-token:SOME_TOKEN@gitlab.yourcompany.com/group/nested/goreleaser/goreleaser.git",
 	} {
 		t.Run(url, func(t *testing.T) {
 			repo, err := git.ExtractRepoFromURL(url)
diff --git a/internal/pipe/gofish/gofish_test.go b/internal/pipe/gofish/gofish_test.go
new file mode 100644
index 00000000..5daa1178
--- /dev/null
+++ b/internal/pipe/gofish/gofish_test.go
@@ -0,0 +1,898 @@
+package gofish
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/client"
+	"github.com/goreleaser/goreleaser/internal/golden"
+	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDescription(t *testing.T) {
+	require.NotEmpty(t, Pipe{}.String())
+}
+
+func createTemplateData() templateData {
+	return templateData{
+		Desc:     "Some desc",
+		Homepage: "https://google.com",
+		ReleasePackages: []releasePackage{
+			{
+				Arch:        "amd64",
+				OS:          "darwin",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Darwin_x86_64.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c68",
+			},
+			{
+				Arch:        "arm64",
+				OS:          "darwin",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Darwin_arm64.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b349490sadasdsadsadasdasdsd",
+			},
+			{
+				Arch:        "amd64",
+				OS:          "linux",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+			{
+				Arch:        "arm",
+				OS:          "linux",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Arm6.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+			{
+				Arch:        "arm64",
+				OS:          "linux",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Arm64.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+			{
+				Arch:        "amd64",
+				OS:          "windows",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_windows_amd64.zip",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+		},
+		Name:    "Test",
+		Version: "0.1.3",
+	}
+}
+
+func assertDefaultTemplateData(t *testing.T, food string) {
+	t.Helper()
+	require.Contains(t, food, "food =")
+	require.Contains(t, food, `homepage = "https://google.com"`)
+	require.Contains(t, food, `url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Darwin_x86_64.tar.gz"`)
+	require.Contains(t, food, `sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c68"`)
+	require.Contains(t, food, `local version = "0.1.3"`)
+}
+
+func TestFullFood(t *testing.T) {
+	data := createTemplateData()
+	data.License = "MIT"
+	food, err := doBuildFood(context.New(config.Project{
+		ProjectName: "foo",
+	}), data)
+	require.NoError(t, err)
+
+	golden.RequireEqualLua(t, []byte(food))
+}
+
+func TestFullFoodLinuxOnly(t *testing.T) {
+	data := createTemplateData()
+	for i, v := range data.ReleasePackages {
+		if v.OS != "linux" {
+			data.ReleasePackages[i] = releasePackage{}
+		}
+	}
+
+	formulae, err := doBuildFood(context.New(config.Project{
+		ProjectName: "foo",
+	}), data)
+	require.NoError(t, err)
+
+	golden.RequireEqualLua(t, []byte(formulae))
+}
+
+func TestFullFoodWindowsOnly(t *testing.T) {
+	data := createTemplateData()
+	for i, v := range data.ReleasePackages {
+		if v.OS != "windows" {
+			data.ReleasePackages[i] = releasePackage{}
+		}
+	}
+	formulae, err := doBuildFood(context.New(config.Project{
+		ProjectName: "foo",
+	}), data)
+	require.NoError(t, err)
+
+	golden.RequireEqualLua(t, []byte(formulae))
+}
+
+func TestFormulaeSimple(t *testing.T) {
+	formulae, err := doBuildFood(context.New(config.Project{}), createTemplateData())
+	require.NoError(t, err)
+	assertDefaultTemplateData(t, formulae)
+	require.NotContains(t, formulae, "def caveats")
+	require.NotContains(t, formulae, "def plist;")
+}
+
+func TestFullPipe(t *testing.T) {
+	type testcase struct {
+		prepare              func(ctx *context.Context)
+		expectedPublishError string
+	}
+	for name, tt := range map[string]testcase{
+		"default": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitHub
+				ctx.Config.Rigs[0].Rig.Owner = "test"
+				ctx.Config.Rigs[0].Rig.Name = "test"
+				ctx.Config.Rigs[0].Homepage = "https://github.com/goreleaser"
+			},
+		},
+		"default_gitlab": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitLab
+				ctx.Config.Rigs[0].Rig.Owner = "test"
+				ctx.Config.Rigs[0].Rig.Name = "test"
+				ctx.Config.Rigs[0].Homepage = "https://gitlab.com/goreleaser"
+			},
+		},
+		"invalid_commit_template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.Rigs[0].Rig.Owner = "test"
+				ctx.Config.Rigs[0].Rig.Name = "test"
+				ctx.Config.Rigs[0].CommitMessageTemplate = "{{ .Asdsa }"
+			},
+			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			folder := t.TempDir()
+			ctx := &context.Context{
+				Git: context.GitInfo{
+					CurrentTag: "v1.0.1",
+				},
+				Version:   "1.0.1",
+				Artifacts: artifact.New(),
+				Env: map[string]string{
+					"FOO": "foo_is_bar",
+				},
+				Config: config.Project{
+					Dist:        folder,
+					ProjectName: name,
+					Rigs: []config.GoFish{
+						{
+							Name: name,
+							IDs: []string{
+								"foo",
+							},
+							Description: "A run pipe test formula and FOO={{ .Env.FOO }}",
+						},
+					},
+				},
+			}
+			tt.prepare(ctx)
+			ctx.Artifacts.Add(&artifact.Artifact{
+				Name:   "bar_bin.tar.gz",
+				Path:   "doesnt matter",
+				Goos:   "darwin",
+				Goarch: "amd64",
+				Type:   artifact.UploadableArchive,
+				Extra: map[string]interface{}{
+					"ID":     "bar",
+					"Format": "tar.gz",
+				},
+			})
+			path := filepath.Join(folder, "bin.tar.gz")
+			ctx.Artifacts.Add(&artifact.Artifact{
+				Name:   "bin.tar.gz",
+				Path:   path,
+				Goos:   "darwin",
+				Goarch: "amd64",
+				Type:   artifact.UploadableArchive,
+				Extra: map[string]interface{}{
+					"ID":     "foo",
+					"Format": "tar.gz",
+				},
+			})
+
+			f, err := os.Create(path)
+			require.NoError(t, err)
+			require.NoError(t, f.Close())
+			client := &DummyClient{}
+			distFile := filepath.Join(folder, name+".lua")
+
+			require.NoError(t, runAll(ctx, client))
+			if tt.expectedPublishError != "" {
+				require.EqualError(t, publishAll(ctx, client), tt.expectedPublishError)
+				return
+			}
+
+			require.NoError(t, publishAll(ctx, client))
+			require.True(t, client.CreatedFile)
+			golden.RequireEqualLua(t, []byte(client.Content))
+
+			distBts, err := os.ReadFile(distFile)
+			require.NoError(t, err)
+			require.Equal(t, client.Content, string(distBts))
+		})
+	}
+}
+
+func TestRunPipeNameTemplate(t *testing.T) {
+	folder := t.TempDir()
+	ctx := &context.Context{
+		Git: context.GitInfo{
+			CurrentTag: "v1.0.1",
+		},
+		Version:   "1.0.1",
+		Artifacts: artifact.New(),
+		Env: map[string]string{
+			"FOO_BAR": "is_bar",
+		},
+		Config: config.Project{
+			Dist:        folder,
+			ProjectName: "foo",
+			Rigs: []config.GoFish{
+				{
+					Name: "foo_{{ .Env.FOO_BAR }}",
+					Rig: config.RepoRef{
+						Owner: "foo",
+						Name:  "bar",
+					},
+					IDs: []string{
+						"foo",
+					},
+				},
+			},
+		},
+	}
+	path := filepath.Join(folder, "bin.tar.gz")
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			"ID":     "foo",
+			"Format": "tar.gz",
+		},
+	})
+
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	client := &DummyClient{}
+	distFile := filepath.Join(folder, "foo_is_bar.lua")
+
+	require.NoError(t, runAll(ctx, client))
+	require.NoError(t, publishAll(ctx, client))
+	require.True(t, client.CreatedFile)
+	golden.RequireEqualLua(t, []byte(client.Content))
+	distBts, err := os.ReadFile(distFile)
+	require.NoError(t, err)
+	require.Equal(t, client.Content, string(distBts))
+}
+
+func TestRunPipeMultipleGoFishWithSkip(t *testing.T) {
+	folder := t.TempDir()
+	ctx := &context.Context{
+		Git: context.GitInfo{
+			CurrentTag: "v1.0.1",
+		},
+		Version:   "1.0.1",
+		Artifacts: artifact.New(),
+		Env: map[string]string{
+			"FOO_BAR": "is_bar",
+		},
+		Config: config.Project{
+			Dist:        folder,
+			ProjectName: "foo",
+			Rigs: []config.GoFish{
+				{
+					Name: "foo",
+					Rig: config.RepoRef{
+						Owner: "foo",
+						Name:  "bar",
+					},
+					IDs: []string{
+						"foo",
+					},
+					SkipUpload: "true",
+				},
+				{
+					Name: "bar",
+					Rig: config.RepoRef{
+						Owner: "foo",
+						Name:  "bar",
+					},
+					IDs: []string{
+						"foo",
+					},
+				},
+				{
+					Name: "foobar",
+					Rig: config.RepoRef{
+						Owner: "foo",
+						Name:  "bar",
+					},
+					IDs: []string{
+						"foo",
+					},
+					SkipUpload: "true",
+				},
+			},
+		},
+	}
+	path := filepath.Join(folder, "bin.tar.gz")
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			"ID":     "foo",
+			"Format": "tar.gz",
+		},
+	})
+
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+
+	cli := &DummyClient{}
+	require.NoError(t, runAll(ctx, cli))
+	require.EqualError(t, publishAll(ctx, cli), `rig.skip_upload is set`)
+	require.True(t, cli.CreatedFile)
+
+	for _, food := range ctx.Config.Rigs {
+		distFile := filepath.Join(folder, food.Name+".lua")
+		_, err := os.Stat(distFile)
+		require.NoError(t, err, "file should exist: "+distFile)
+	}
+}
+
+func TestRunPipeForMultipleArmVersions(t *testing.T) {
+	for name, fn := range map[string]func(ctx *context.Context){
+		"multiple_armv5": func(ctx *context.Context) {
+			ctx.Config.Rigs[0].Goarm = "5"
+		},
+		"multiple_armv6": func(ctx *context.Context) {
+			ctx.Config.Rigs[0].Goarm = "6"
+		},
+		"multiple_armv7": func(ctx *context.Context) {
+			ctx.Config.Rigs[0].Goarm = "7"
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			folder := t.TempDir()
+			ctx := &context.Context{
+				TokenType: context.TokenTypeGitHub,
+				Git: context.GitInfo{
+					CurrentTag: "v1.0.1",
+				},
+				Version:   "1.0.1",
+				Artifacts: artifact.New(),
+				Env: map[string]string{
+					"FOO": "foo_is_bar",
+				},
+				Config: config.Project{
+					Dist:        folder,
+					ProjectName: name,
+					Rigs: []config.GoFish{
+						{
+							Name:        name,
+							Description: "A run pipe test formula and FOO={{ .Env.FOO }}",
+							Rig: config.RepoRef{
+								Owner: "test",
+								Name:  "test",
+							},
+							Homepage: "https://github.com/goreleaser",
+						},
+					},
+					GitHubURLs: config.GitHubURLs{
+						Download: "https://github.com",
+					},
+					Release: config.Release{
+						GitHub: config.Repo{
+							Owner: "test",
+							Name:  "test",
+						},
+					},
+				},
+			}
+			fn(ctx)
+			for _, a := range []struct {
+				name   string
+				goos   string
+				goarch string
+				goarm  string
+			}{
+				{
+					name:   "bin",
+					goos:   "darwin",
+					goarch: "amd64",
+				},
+				{
+					name:   "arm64",
+					goos:   "linux",
+					goarch: "arm64",
+				},
+				{
+					name:   "armv5",
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "5",
+				},
+				{
+					name:   "armv6",
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "6",
+				},
+				{
+					name:   "armv7",
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "7",
+				},
+			} {
+				path := filepath.Join(folder, fmt.Sprintf("%s.tar.gz", a.name))
+				ctx.Artifacts.Add(&artifact.Artifact{
+					Name:   fmt.Sprintf("%s.tar.gz", a.name),
+					Path:   path,
+					Goos:   a.goos,
+					Goarch: a.goarch,
+					Goarm:  a.goarm,
+					Type:   artifact.UploadableArchive,
+					Extra: map[string]interface{}{
+						"ID":     a.name,
+						"Format": "tar.gz",
+					},
+				})
+				f, err := os.Create(path)
+				require.NoError(t, err)
+				require.NoError(t, f.Close())
+			}
+
+			client := &DummyClient{}
+			distFile := filepath.Join(folder, name+".lua")
+
+			require.NoError(t, runAll(ctx, client))
+			require.NoError(t, publishAll(ctx, client))
+			require.True(t, client.CreatedFile)
+			golden.RequireEqualLua(t, []byte(client.Content))
+
+			distBts, err := os.ReadFile(distFile)
+			require.NoError(t, err)
+			require.Equal(t, client.Content, string(distBts))
+		})
+	}
+}
+
+func TestRunPipeNoBuilds(t *testing.T) {
+	ctx := &context.Context{
+		TokenType: context.TokenTypeGitHub,
+		Config: config.Project{
+			Rigs: []config.GoFish{
+				{
+					Rig: config.RepoRef{
+						Owner: "test",
+						Name:  "test",
+					},
+				},
+			},
+		},
+	}
+	client := &DummyClient{}
+	require.Equal(t, ErrNoArchivesFound, runAll(ctx, client))
+	require.False(t, client.CreatedFile)
+}
+
+func TestRunPipeMultipleArchivesSameOsBuild(t *testing.T) {
+	ctx := context.New(
+		config.Project{
+			Rigs: []config.GoFish{
+				{
+					Rig: config.RepoRef{
+						Owner: "test",
+						Name:  "test",
+					},
+				},
+			},
+		},
+	)
+
+	ctx.TokenType = context.TokenTypeGitHub
+	f, err := ioutil.TempFile(t.TempDir(), "")
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		require.NoError(t, f.Close())
+	})
+
+	tests := []struct {
+		expectedError error
+		osarchs       []struct {
+			goos   string
+			goarch string
+			goarm  string
+		}
+	}{
+		{
+			expectedError: ErrMultipleArchivesSameOS,
+			osarchs: []struct {
+				goos   string
+				goarch string
+				goarm  string
+			}{
+				{
+					goos:   "darwin",
+					goarch: "amd64",
+				},
+				{
+					goos:   "darwin",
+					goarch: "amd64",
+				},
+			},
+		},
+		{
+			expectedError: ErrMultipleArchivesSameOS,
+			osarchs: []struct {
+				goos   string
+				goarch string
+				goarm  string
+			}{
+				{
+					goos:   "linux",
+					goarch: "amd64",
+				},
+				{
+					goos:   "linux",
+					goarch: "amd64",
+				},
+			},
+		},
+		{
+			expectedError: ErrMultipleArchivesSameOS,
+			osarchs: []struct {
+				goos   string
+				goarch string
+				goarm  string
+			}{
+				{
+					goos:   "linux",
+					goarch: "arm64",
+				},
+				{
+					goos:   "linux",
+					goarch: "arm64",
+				},
+			},
+		},
+		{
+			expectedError: ErrMultipleArchivesSameOS,
+			osarchs: []struct {
+				goos   string
+				goarch string
+				goarm  string
+			}{
+				{
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "6",
+				},
+				{
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "6",
+				},
+			},
+		},
+		{
+			expectedError: ErrMultipleArchivesSameOS,
+			osarchs: []struct {
+				goos   string
+				goarch string
+				goarm  string
+			}{
+				{
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "5",
+				},
+				{
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "6",
+				},
+				{
+					goos:   "linux",
+					goarch: "arm",
+					goarm:  "7",
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		for idx, ttt := range test.osarchs {
+			ctx.Artifacts.Add(&artifact.Artifact{
+				Name:   fmt.Sprintf("bin%d", idx),
+				Path:   f.Name(),
+				Goos:   ttt.goos,
+				Goarch: ttt.goarch,
+				Type:   artifact.UploadableArchive,
+				Extra: map[string]interface{}{
+					"ID":     fmt.Sprintf("foo%d", idx),
+					"Format": "tar.gz",
+				},
+			})
+		}
+		client := &DummyClient{}
+		require.Equal(t, test.expectedError, runAll(ctx, client))
+		require.False(t, client.CreatedFile)
+		// clean the artifacts for the next run
+		ctx.Artifacts = artifact.New()
+	}
+}
+
+func TestRunPipeBinaryRelease(t *testing.T) {
+	ctx := context.New(
+		config.Project{
+			Rigs: []config.GoFish{
+				{
+					Rig: config.RepoRef{
+						Owner: "test",
+						Name:  "test",
+					},
+				},
+			},
+		},
+	)
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin",
+		Path:   "doesnt mather",
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.Binary,
+	})
+	client := &DummyClient{}
+	require.Equal(t, ErrNoArchivesFound, runAll(ctx, client))
+	require.False(t, client.CreatedFile)
+}
+
+func TestRunPipeNoUpload(t *testing.T) {
+	folder := t.TempDir()
+	ctx := context.New(config.Project{
+		Dist:        folder,
+		ProjectName: "foo",
+		Release:     config.Release{},
+		Rigs: []config.GoFish{
+			{
+				Rig: config.RepoRef{
+					Owner: "test",
+					Name:  "test",
+				},
+			},
+		},
+	})
+	ctx.TokenType = context.TokenTypeGitHub
+	ctx.Git = context.GitInfo{CurrentTag: "v1.0.1"}
+	path := filepath.Join(folder, "whatever.tar.gz")
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			"ID":     "foo",
+			"Format": "tar.gz",
+		},
+	})
+	client := &DummyClient{}
+
+	assertNoPublish := func(t *testing.T) {
+		t.Helper()
+		require.NoError(t, runAll(ctx, client))
+		testlib.AssertSkipped(t, publishAll(ctx, client))
+		require.False(t, client.CreatedFile)
+	}
+	t.Run("skip upload true", func(t *testing.T) {
+		ctx.Config.Rigs[0].SkipUpload = "true"
+		ctx.Semver.Prerelease = ""
+		assertNoPublish(t)
+	})
+	t.Run("skip upload auto", func(t *testing.T) {
+		ctx.Config.Rigs[0].SkipUpload = "auto"
+		ctx.Semver.Prerelease = "beta1"
+		assertNoPublish(t)
+	})
+}
+
+func TestRunEmptyTokenType(t *testing.T) {
+	folder := t.TempDir()
+	ctx := context.New(config.Project{
+		Dist:        folder,
+		ProjectName: "foo",
+		Release:     config.Release{},
+		Rigs: []config.GoFish{
+			{
+				Rig: config.RepoRef{
+					Owner: "test",
+					Name:  "test",
+				},
+			},
+		},
+	})
+	ctx.Git = context.GitInfo{CurrentTag: "v1.0.1"}
+	path := filepath.Join(folder, "whatever.tar.gz")
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			"ID":     "foo",
+			"Format": "tar.gz",
+		},
+	})
+	client := &DummyClient{}
+	require.NoError(t, runAll(ctx, client))
+}
+
+func TestRunTokenTypeNotImplementedForGoFish(t *testing.T) {
+	folder := t.TempDir()
+	ctx := context.New(config.Project{
+		Dist:        folder,
+		ProjectName: "foo",
+		Release:     config.Release{},
+		Rigs: []config.GoFish{
+			{
+				Rig: config.RepoRef{
+					Owner: "test",
+					Name:  "test",
+				},
+			},
+		},
+	})
+	ctx.TokenType = context.TokenTypeGitea
+	ctx.Git = context.GitInfo{CurrentTag: "v1.0.1"}
+	path := filepath.Join(folder, "whatever.tar.gz")
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			"ID":     "foo",
+			"Format": "tar.gz",
+		},
+	})
+	client := &DummyClient{NotImplemented: true}
+	require.EqualError(t, runAll(ctx, client), `token type "gitea" not implemented for gofish pipe`)
+}
+
+func TestDefault(t *testing.T) {
+	testlib.Mktmp(t)
+
+	ctx := &context.Context{
+		TokenType: context.TokenTypeGitHub,
+		Config: config.Project{
+			ProjectName: "myproject",
+			Rigs: []config.GoFish{
+				{},
+			},
+			Builds: []config.Build{
+				{
+					Binary: "foo",
+					Goos:   []string{"linux", "darwin"},
+					Goarch: []string{"386", "amd64"},
+				},
+				{
+					Binary: "bar",
+					Goos:   []string{"linux", "darwin"},
+					Goarch: []string{"386", "amd64"},
+					Ignore: []config.IgnoredBuild{
+						{Goos: "darwin", Goarch: "amd64"},
+					},
+				},
+				{
+					Binary: "foobar",
+					Goos:   []string{"linux"},
+					Goarch: []string{"amd64"},
+				},
+			},
+		},
+	}
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Equal(t, ctx.Config.ProjectName, ctx.Config.Rigs[0].Name)
+	require.NotEmpty(t, ctx.Config.Rigs[0].CommitAuthor.Name)
+	require.NotEmpty(t, ctx.Config.Rigs[0].CommitAuthor.Email)
+	require.NotEmpty(t, ctx.Config.Rigs[0].CommitMessageTemplate)
+}
+
+func TestGHFolder(t *testing.T) {
+	require.Equal(t, "bar.lua", buildFoodPath("", "bar.lua"))
+	require.Equal(t, "fooo/bar.lua", buildFoodPath("fooo", "bar.lua"))
+}
+
+type DummyClient struct {
+	CreatedFile    bool
+	Content        string
+	NotImplemented bool
+}
+
+func (dc *DummyClient) CloseMilestone(ctx *context.Context, repo client.Repo, title string) error {
+	return nil
+}
+
+func (dc *DummyClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) {
+	return
+}
+
+func (dc *DummyClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
+	if dc.NotImplemented {
+		return "", client.NotImplementedError{}
+	}
+
+	return "https://dummyhost/download/{{ .Tag }}/{{ .ArtifactName }}", nil
+}
+
+func (dc *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo client.Repo, content []byte, path, msg string) (err error) {
+	dc.CreatedFile = true
+	dc.Content = string(content)
+	return
+}
+
+func (dc *DummyClient) Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) (err error) {
+	return
+}
+
+func TestSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		require.True(t, Pipe{}.Skip(context.New(config.Project{})))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			Rigs: []config.GoFish{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
+func TestRunSkipNoName(t *testing.T) {
+	ctx := context.New(config.Project{
+		Rigs: []config.GoFish{{}},
+	})
+
+	client := &DummyClient{}
+	testlib.AssertSkipped(t, runAll(ctx, client))
+}
diff --git a/internal/pipe/gofish/testdata/TestFullFood.lua.golden b/internal/pipe/gofish/testdata/TestFullFood.lua.golden
new file mode 100644
index 00000000..e579630b
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestFullFood.lua.golden
@@ -0,0 +1,89 @@
+local name = "Test"
+local version = "0.1.3"
+
+food = {
+    name = name,
+    description = "Some desc",
+    license = "MIT",
+    homepage = "https://google.com",
+    version = version,
+    packages = {
+		{
+            os = "darwin",
+            arch = "amd64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Darwin_x86_64.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c68",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "darwin",
+            arch = "arm64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Darwin_arm64.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b349490sadasdsadsadasdasdsd",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "amd64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Arm6.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Arm64.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "windows",
+            arch = "amd64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_windows_amd64.zip",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name .. ".exe",
+                    installpath = "bin\\" .. name .. ".exe"
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestFullFoodLinuxOnly.lua.golden b/internal/pipe/gofish/testdata/TestFullFoodLinuxOnly.lua.golden
new file mode 100644
index 00000000..c0629c18
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestFullFoodLinuxOnly.lua.golden
@@ -0,0 +1,51 @@
+local name = "Test"
+local version = "0.1.3"
+
+food = {
+    name = name,
+    description = "Some desc",
+    license = "",
+    homepage = "https://google.com",
+    version = version,
+    packages = {
+		{
+            os = "linux",
+            arch = "amd64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Arm6.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Arm64.tar.gz",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestFullFoodWindowsOnly.lua.golden b/internal/pipe/gofish/testdata/TestFullFoodWindowsOnly.lua.golden
new file mode 100644
index 00000000..2b98981d
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestFullFoodWindowsOnly.lua.golden
@@ -0,0 +1,24 @@
+local name = "Test"
+local version = "0.1.3"
+
+food = {
+    name = name,
+    description = "Some desc",
+    license = "",
+    homepage = "https://google.com",
+    version = version,
+    packages = {
+		{
+            os = "windows",
+            arch = "amd64",
+            url = "https://github.com/caarlos0/test/releases/download/v0.1.3/test_windows_amd64.zip",
+            sha256 = "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+            resources = {
+                {
+                    path = name .. ".exe",
+                    installpath = "bin\\" .. name .. ".exe"
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestFullPipe/default.lua.golden b/internal/pipe/gofish/testdata/TestFullPipe/default.lua.golden
new file mode 100644
index 00000000..df249e05
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestFullPipe/default.lua.golden
@@ -0,0 +1,25 @@
+local name = "default"
+local version = "1.0.1"
+
+food = {
+    name = name,
+    description = "A run pipe test formula and FOO=foo_is_bar",
+    license = "",
+    homepage = "https://github.com/goreleaser",
+    version = version,
+    packages = {
+		{
+            os = "darwin",
+            arch = "amd64",
+            url = "https://dummyhost/download/v1.0.1/bin.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestFullPipe/default_gitlab.lua.golden b/internal/pipe/gofish/testdata/TestFullPipe/default_gitlab.lua.golden
new file mode 100644
index 00000000..0b574569
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestFullPipe/default_gitlab.lua.golden
@@ -0,0 +1,25 @@
+local name = "default_gitlab"
+local version = "1.0.1"
+
+food = {
+    name = name,
+    description = "A run pipe test formula and FOO=foo_is_bar",
+    license = "",
+    homepage = "https://gitlab.com/goreleaser",
+    version = version,
+    packages = {
+		{
+            os = "darwin",
+            arch = "amd64",
+            url = "https://dummyhost/download/v1.0.1/bin.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv5.lua.golden b/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv5.lua.golden
new file mode 100644
index 00000000..0450d7f8
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv5.lua.golden
@@ -0,0 +1,51 @@
+local name = "multiple_armv5"
+local version = "1.0.1"
+
+food = {
+    name = name,
+    description = "A run pipe test formula and FOO=foo_is_bar",
+    license = "",
+    homepage = "https://github.com/goreleaser",
+    version = version,
+    packages = {
+		{
+            os = "darwin",
+            arch = "amd64",
+            url = "https://dummyhost/download/v1.0.1/bin.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm64",
+            url = "https://dummyhost/download/v1.0.1/arm64.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm",
+            url = "https://dummyhost/download/v1.0.1/armv5.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv6.lua.golden b/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv6.lua.golden
new file mode 100644
index 00000000..31715345
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv6.lua.golden
@@ -0,0 +1,51 @@
+local name = "multiple_armv6"
+local version = "1.0.1"
+
+food = {
+    name = name,
+    description = "A run pipe test formula and FOO=foo_is_bar",
+    license = "",
+    homepage = "https://github.com/goreleaser",
+    version = version,
+    packages = {
+		{
+            os = "darwin",
+            arch = "amd64",
+            url = "https://dummyhost/download/v1.0.1/bin.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm64",
+            url = "https://dummyhost/download/v1.0.1/arm64.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm",
+            url = "https://dummyhost/download/v1.0.1/armv6.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv7.lua.golden b/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv7.lua.golden
new file mode 100644
index 00000000..b13c1bf1
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestRunPipeForMultipleArmVersions/multiple_armv7.lua.golden
@@ -0,0 +1,51 @@
+local name = "multiple_armv7"
+local version = "1.0.1"
+
+food = {
+    name = name,
+    description = "A run pipe test formula and FOO=foo_is_bar",
+    license = "",
+    homepage = "https://github.com/goreleaser",
+    version = version,
+    packages = {
+		{
+            os = "darwin",
+            arch = "amd64",
+            url = "https://dummyhost/download/v1.0.1/bin.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm64",
+            url = "https://dummyhost/download/v1.0.1/arm64.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+		{
+            os = "linux",
+            arch = "arm",
+            url = "https://dummyhost/download/v1.0.1/armv7.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+    }
+}
diff --git a/internal/pipe/gofish/testdata/TestRunPipeNameTemplate.lua.golden b/internal/pipe/gofish/testdata/TestRunPipeNameTemplate.lua.golden
new file mode 100644
index 00000000..e773c658
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestRunPipeNameTemplate.lua.golden
@@ -0,0 +1,25 @@
+local name = "foo_is_bar"
+local version = "1.0.1"
+
+food = {
+    name = name,
+    description = "",
+    license = "",
+    homepage = "",
+    version = version,
+    packages = {
+		{
+            os = "darwin",
+            arch = "amd64",
+            url = "https://dummyhost/download/v1.0.1/bin.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = name,
+                    installpath = "bin/" .. name,
+                    executable = true
+                }
+            }
+        },
+    }
+}
diff --git a/www/htmltest.yml b/www/htmltest.yml
index c99a3d08..6fbf8108 100644
--- a/www/htmltest.yml
+++ b/www/htmltest.yml
@@ -8,6 +8,7 @@ IgnoreURLs:
 - microsoft.com
 - https://github.com/goreleaser/goreleaser/edit/
 - https://discord.com/
+- https://gofi.sh
 IgnoreDirs:
 - overrides
 IgnoreDirectoryMissingTrailingSlash: true