Repository: goreleaser/goreleaser
Pull request #2536 (2021-09-28 12:59:57)

feat: announce: mattermost
Add mattermost to the announcements 

Test Announcement:
![Screenshot 2021-09-28 at 14 58 12](https://user-images.githubusercontent.com/4115580/135091614-72b01868-c4fe-4125-b46c-c4f73afe933b.png)



codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2536?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2536](https://codecov.io/gh/goreleaser/goreleaser/pull/2536?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (4d4828c) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/88813280416f7e62cfb686fa5713a141edfe46fa?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (8881328) will **decrease** coverage by `0.00%`.
> The diff coverage is `82.60%`.

> :exclamation: Current head 4d4828c differs from pull request most recent head 2cb59b0. Consider uploading reports for the commit 2cb59b0 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2536/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/2536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##           master    #2536      +/-   ##
==========================================
- Coverage   83.45%   83.44%   -0.01%     
==========================================
  Files          93       94       +1     
  Lines        6444     6513      +69     
==========================================
+ Hits         5378     5435      +57     
- Misses        910      918       +8     
- Partials      156      160       +4     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/pipe/announce/announce.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9hbm5vdW5jZS9hbm5vdW5jZS5nbw==) | `100.00% <ø> (ø)` | |
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2536/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/mattermost/mattermost.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9tYXR0ZXJtb3N0L21hdHRlcm1vc3QuZ28=) | `82.60% <82.60%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2536?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/2536?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [8881328...2cb59b0](https://codecov.io/gh/goreleaser/goreleaser/pull/2536?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).


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/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/announce/announce.go b/internal/pipe/announce/announce.go
index 32dc8714..a85838fe 100644
--- a/internal/pipe/announce/announce.go
+++ b/internal/pipe/announce/announce.go
@@ -8,6 +8,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/middleware/logging"
 	"github.com/goreleaser/goreleaser/internal/middleware/skip"
 	"github.com/goreleaser/goreleaser/internal/pipe/discord"
+	"github.com/goreleaser/goreleaser/internal/pipe/mattermost"
 	"github.com/goreleaser/goreleaser/internal/pipe/reddit"
 	"github.com/goreleaser/goreleaser/internal/pipe/slack"
 	"github.com/goreleaser/goreleaser/internal/pipe/smtp"
@@ -27,6 +28,7 @@ type Announcer interface {
 var announcers = []Announcer{
 	// XXX: keep asc sorting
 	discord.Pipe{},
+	mattermost.Pipe{},
 	reddit.Pipe{},
 	slack.Pipe{},
 	smtp.Pipe{},
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/mattermost/mattermost.go b/internal/pipe/mattermost/mattermost.go
new file mode 100644
index 00000000..1a722aa7
--- /dev/null
+++ b/internal/pipe/mattermost/mattermost.go
@@ -0,0 +1,147 @@
+package mattermost
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/apex/log"
+	"github.com/caarlos0/env/v6"
+
+	"github.com/goreleaser/goreleaser/internal/tmpl"
+	"github.com/goreleaser/goreleaser/pkg/context"
+)
+
+const (
+	defaultColor           = "#2D313E"
+	defaultUsername        = `GoReleaser`
+	defaultMessageTemplate = `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .GitURL }}/releases/tag/{{ .Tag }}`
+	defaultMessageTitle    = `{{ .ProjectName }} {{ .Tag }} is out!`
+)
+
+type Pipe struct{}
+
+func (Pipe) String() string                 { return "mattermost" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.Mattermost.Enabled }
+
+type Config struct {
+	Webhook string `env:"MATTERMOST_WEBHOOK,notEmpty"`
+}
+
+func (Pipe) Default(ctx *context.Context) error {
+	if ctx.Config.Announce.Mattermost.MessageTemplate == "" {
+		ctx.Config.Announce.Mattermost.MessageTemplate = defaultMessageTemplate
+	}
+	if ctx.Config.Announce.Mattermost.TitleTemplate == "" {
+		ctx.Config.Announce.Mattermost.TitleTemplate = defaultMessageTitle
+	}
+	if ctx.Config.Announce.Mattermost.Username == "" {
+		ctx.Config.Announce.Mattermost.Username = defaultUsername
+	}
+	if ctx.Config.Announce.Teams.Color == "" {
+		ctx.Config.Announce.Teams.Color = defaultColor
+	}
+
+	return nil
+}
+
+func (Pipe) Announce(ctx *context.Context) error {
+	msg, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Mattermost.MessageTemplate)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to mattermost: %w", err)
+	}
+
+	title, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Mattermost.TitleTemplate)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to teams: %w", err)
+	}
+
+	var cfg Config
+	if err := env.Parse(&cfg); err != nil {
+		return fmt.Errorf("announce: failed to announce to mattermost: %w", err)
+	}
+
+	log.Infof("posting: %q", msg)
+
+	wm := &incomingWebhookRequest{
+		Username:    ctx.Config.Announce.Mattermost.Username,
+		IconEmoji:   ctx.Config.Announce.Mattermost.IconEmoji,
+		IconURL:     ctx.Config.Announce.Mattermost.IconURL,
+		ChannelName: ctx.Config.Announce.Mattermost.Channel,
+		Attachments: []*mattermostAttachment{
+			{
+				Title: title,
+				Text:  msg,
+				Color: ctx.Config.Announce.Teams.Color,
+			},
+		},
+	}
+
+	err = postWebhook(ctx, cfg.Webhook, wm)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to mattermost: %w", err)
+	}
+
+	return nil
+}
+
+func postWebhook(ctx *context.Context, url string, msg *incomingWebhookRequest) error {
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		return fmt.Errorf("failed to marshal the message: %w", err)
+	}
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
+	if err != nil {
+		return fmt.Errorf("failed new request: %w", err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	r, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to mattermost: %w", err)
+	}
+	closeBody(r)
+
+	return nil
+}
+
+func closeBody(r *http.Response) {
+	if r.Body != nil {
+		_, _ = io.Copy(ioutil.Discard, r.Body)
+		_ = r.Body.Close()
+	}
+}
+
+type incomingWebhookRequest struct {
+	Text        string                  `json:"text"`
+	Username    string                  `json:"username"`
+	IconURL     string                  `json:"icon_url"`
+	ChannelName string                  `json:"channel"`
+	Attachments []*mattermostAttachment `json:"attachments"`
+	IconEmoji   string                  `json:"icon_emoji"`
+}
+
+type mattermostAttachment struct {
+	Fallback   string                       `json:"fallback"`
+	Color      string                       `json:"color"`
+	Pretext    string                       `json:"pretext"`
+	AuthorName string                       `json:"author_name"`
+	AuthorLink string                       `json:"author_link"`
+	AuthorIcon string                       `json:"author_icon"`
+	Title      string                       `json:"title"`
+	TitleLink  string                       `json:"title_link"`
+	Text       string                       `json:"text"`
+	Fields     []*mattermostAttachmentField `json:"fields"`
+	Footer     string                       `json:"footer"`
+	FooterIcon string                       `json:"footer_icon"`
+}
+
+type mattermostAttachmentField struct {
+	Title string      `json:"title"`
+	Value interface{} `json:"value"`
+	Short bool        `json:"short"`
+}
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..7c1aa0bf 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"`
@@ -691,12 +707,13 @@ type GoMod struct {
 }
 
 type Announce struct {
-	Twitter Twitter `yaml:"twitter,omitempty"`
-	Reddit  Reddit  `yaml:"reddit,omitempty"`
-	Slack   Slack   `yaml:"slack,omitempty"`
-	Discord Discord `yaml:"discord,omitempty"`
-	Teams   Teams   `yaml:"teams,omitempty"`
-	SMTP    SMTP    `yaml:"smtp,omitempty"`
+	Twitter    Twitter    `yaml:"twitter,omitempty"`
+	Reddit     Reddit     `yaml:"reddit,omitempty"`
+	Slack      Slack      `yaml:"slack,omitempty"`
+	Discord    Discord    `yaml:"discord,omitempty"`
+	Teams      Teams      `yaml:"teams,omitempty"`
+	SMTP       SMTP       `yaml:"smtp,omitempty"`
+	Mattermost Mattermost `yaml:"mattermost,omitempty"`
 }
 
 type Twitter struct {
@@ -738,6 +755,17 @@ type Teams struct {
 	IconURL         string `yaml:"icon_url,omitempty"`
 }
 
+type Mattermost struct {
+	Enabled         bool   `yaml:"enabled,omitempty"`
+	MessageTemplate string `yaml:"message_template,omitempty"`
+	TitleTemplate   string `yaml:"title_template,omitempty"`
+	Color           string `yaml:"color,omitempty"`
+	Channel         string `yaml:"channel,omitempty"`
+	Username        string `yaml:"username,omitempty"`
+	IconEmoji       string `yaml:"icon_emoji,omitempty"`
+	IconURL         string `yaml:"icon_url,omitempty"`
+}
+
 type SMTP struct {
 	Enabled            bool     `yaml:"enabled,omitempty"`
 	Host               string   `yaml:"host,omitempty"`
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index ea7c9cd2..c44f959d 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -13,7 +13,9 @@ 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/mattermost"
 	"github.com/goreleaser/goreleaser/internal/pipe/milestone"
 	"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
 	"github.com/goreleaser/goreleaser/internal/pipe/project"
@@ -60,6 +62,7 @@ var Defaulters = []Defaulter{
 	artifactory.Pipe{},
 	blob.Pipe{},
 	brew.Pipe{},
+	gofish.Pipe{},
 	scoop.Pipe{},
 	discord.Pipe{},
 	reddit.Pipe{},
@@ -67,5 +70,6 @@ var Defaulters = []Defaulter{
 	teams.Pipe{},
 	twitter.Pipe{},
 	smtp.Pipe{},
+	mattermost.Pipe{},
 	milestone.Pipe{},
 }
diff --git a/www/docs/customization/announce/mattermost.md b/www/docs/customization/announce/mattermost.md
new file mode 100644
index 00000000..e906ef76
--- /dev/null
+++ b/www/docs/customization/announce/mattermost.md
@@ -0,0 +1,44 @@
+# Mattermost
+
+For it to work, you'll need to [create a new Incoming Webhook](https://docs.mattermost.com/developer/webhooks-incoming.html) in your own Mattermost deployment, and set some
+environment variables on your pipeline:
+
+- `MATTERMOST_WEBHOOK`
+
+Then, you can add something like the following to your `.goreleaser.yml` config:
+
+```yaml
+# .goreleaser.yml
+announce:
+  mattermost:
+    # Whether its enabled or not.
+    # Defaults to false.
+    enabled: true
+
+    # Title template to use while publishing.
+    # Defaults to `{{ .ProjectName }} {{ .Tag }} is out!`
+    title_template: 'GoReleaser {{ .Tag }} was just released!'
+
+    # Message template to use while publishing.
+    # Defaults to `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .GitURL }}/releases/tag/{{ .Tag }}`
+    message_template: 'Awesome project {{.Tag}} is out!'
+
+    # Color code of the message. You have to use hexadecimal.
+    # Defaults to `#2D313E` - the grey-ish from goreleaser
+    color: ''
+
+    # The name of the channel that the user selected as a destination for webhook messages.
+    channel: '#channel'
+
+    # Set your Webhook's user name.
+    username: ''
+
+    # Emoji to use as the icon for this message. Overrides icon_url.
+    icon_emoji: ''
+
+    # URL to an image to use as the icon for this message.
+    icon_url: ''
+```
+
+!!! tip
+    Learn more about the [name template engine](/customization/templates/).
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/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/internal/pipe/mattermost/mattermost_test.go b/internal/pipe/mattermost/mattermost_test.go
new file mode 100644
index 00000000..403ba042
--- /dev/null
+++ b/internal/pipe/mattermost/mattermost_test.go
@@ -0,0 +1,99 @@
+package mattermost
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+)
+
+func TestStringer(t *testing.T) {
+	require.Equal(t, Pipe{}.String(), "mattermost")
+}
+
+func TestDefault(t *testing.T) {
+	ctx := context.New(config.Project{})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Equal(t, ctx.Config.Announce.Mattermost.MessageTemplate, defaultMessageTemplate)
+}
+
+func TestAnnounceInvalidTemplate(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Mattermost: config.Mattermost{
+				MessageTemplate: "{{ .Foo }",
+			},
+		},
+	})
+	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to mattermost: template: tmpl:1: unexpected "}" in operand`)
+}
+
+func TestAnnounceMissingEnv(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Mattermost: config.Mattermost{},
+		},
+	})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to mattermost: env: environment variable "MATTERMOST_WEBHOOK" should not be empty`)
+}
+
+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{
+			Announce: config.Announce{
+				Mattermost: config.Mattermost{
+					Enabled: true,
+				},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
+func TestPostWebhook(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		rc := &incomingWebhookRequest{}
+
+		body, _ := ioutil.ReadAll(r.Body)
+		err := json.Unmarshal(body, rc)
+		require.NoError(t, err)
+		require.Equal(t, defaultColor, rc.Attachments[0].Color)
+		require.Equal(t, "Honk v1.0.0 is out!", rc.Attachments[0].Title)
+		require.Equal(t, "Honk v1.0.0 is out! Check it out at https://github.com/honk/honk/releases/tag/v1.0.0", rc.Attachments[0].Text)
+
+		w.WriteHeader(200)
+		_, err = w.Write([]byte{})
+		require.NoError(t, err)
+	}))
+	defer ts.Close()
+
+	ctx := context.New(config.Project{
+		ProjectName: "Honk",
+		Announce: config.Announce{
+			Mattermost: config.Mattermost{
+				Enabled: true,
+			},
+		},
+	})
+
+	ctx.Git.CurrentTag = "v1.0.0"
+	ctx.Git.URL = "https://github.com/honk/honk"
+
+	os.Setenv("MATTERMOST_WEBHOOK", ts.URL)
+	defer os.Unsetenv("MATTERMOST_WEBHOOK")
+
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.NoError(t, Pipe{}.Announce(ctx))
+}
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