Repository: goreleaser/goreleaser
Pull request #2437 (2021-08-28 01:34:56)

docs: update check instructions
to be merged only after the next release

refs https://github.com/goreleaser/goreleaser/commit/c100de32a3b63668c469d2c95acefafaf107584a

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/goreleaser/goreleaser/FeHuLmyjh48oAcYUc2t217hU5u7Q](https://vercel.com/goreleaser/goreleaser/FeHuLmyjh48oAcYUc2t217hU5u7Q)  
✅ Preview: *In Progress*


codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2437?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2437](https://codecov.io/gh/goreleaser/goreleaser/pull/2437?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (a3c0750) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/70057e81e5af60a1a9035ef72e14d0c0303a6d16?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (70057e8) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2437/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/2437?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@           Coverage Diff           @@
##           master    #2437   +/-   ##
=======================================
  Coverage   83.01%   83.01%           
=======================================
  Files          84       84           
  Lines        6165     6165           
=======================================
  Hits         5118     5118           
  Misses        894      894           
  Partials      153      153           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2437?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/2437?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [70057e8...a3c0750](https://codecov.io/gh/goreleaser/goreleaser/pull/2437?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/.github/workflows/build.yml b/.github/workflows/build.yml
index af688f12..f1099173 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -46,7 +46,7 @@ jobs:
           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
           restore-keys: |
             ${{ runner.os }}-go-
-      - uses: sigstore/cosign-installer@main
+      - uses: sigstore/cosign-installer@v1.1.0
       -
         name: Make Setup
         run: |
diff --git a/go.mod b/go.mod
index 15e8ef82..a7464b7c 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/Masterminds/semver/v3 v3.1.1
 	github.com/apex/log v1.9.0
 	github.com/caarlos0/ctrlc v1.0.0
-	github.com/caarlos0/env/v6 v6.6.2
+	github.com/caarlos0/env/v6 v6.7.0
 	github.com/caarlos0/go-shellwords v1.0.12
 	github.com/dghubble/go-twitter v0.0.0-20210609183100-2fdbf421508e
 	github.com/dghubble/oauth1 v0.7.0
@@ -21,6 +21,7 @@ require (
 	github.com/spf13/cobra v1.2.1
 	github.com/stretchr/testify v1.7.0
 	github.com/ulikunitz/xz v0.5.10
+	github.com/vartanbeno/go-reddit/v2 v2.0.1
 	github.com/xanzy/go-gitlab v0.50.3
 	gocloud.dev v0.23.0
 	golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
@@ -94,6 +95,7 @@ require (
 	github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
+	github.com/slack-go/slack v0.9.4
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/xanzy/ssh-agent v0.3.0 // indirect
 	go.opencensus.io v0.23.0 // indirect
@@ -114,3 +116,5 @@ require (
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
+
+require github.com/gorilla/websocket v1.4.2 // indirect
diff --git a/go.sum b/go.sum
index 1033f62d..57598f59 100644
--- a/go.sum
+++ b/go.sum
@@ -152,8 +152,8 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4
 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
 github.com/caarlos0/ctrlc v1.0.0 h1:2DtF8GSIcajgffDFJzyG15vO+1PuBWOMUdFut7NnXhw=
 github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
-github.com/caarlos0/env/v6 v6.6.2 h1:BypLXDWQTA32rS4UM7pBz+/0BOuvs6C7LSeQAxMwyvI=
-github.com/caarlos0/env/v6 v6.6.2/go.mod h1:P0BVSgU9zfkxfSpFUs6KsO3uWR4k3Ac0P66ibAGTybM=
+github.com/caarlos0/env/v6 v6.7.0 h1:OftrMgQyETinUI4YU3WxhHeKtCRonDMtnUO14+ZRXdY=
+github.com/caarlos0/env/v6 v6.7.0/go.mod h1:FE0jGiAnQqtv2TenJ4KTa8+/T2Ss8kdS5s1VEjasoN0=
 github.com/caarlos0/go-shellwords v1.0.12 h1:HWrUnu6lGbWfrDcFiHcZiwOLzHWjjrPVehULaTFgPp8=
 github.com/caarlos0/go-shellwords v1.0.12/go.mod h1:bYeeX1GrTLPl5cAMYEzdm272qdsQAZiaHgeF0KTk1Gw=
 github.com/caarlos0/testfs v0.4.3 h1:q1zEM5hgsssqWanAfevJYYa0So60DdK6wlJeTc/yfUE=
@@ -244,6 +244,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
+github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
@@ -351,6 +353,7 @@ github.com/goreleaser/fileglob v1.2.0/go.mod h1:rFyb2pXaK3YdnYnSjn6lifw0h2Q6s8Of
 github.com/goreleaser/nfpm/v2 v2.6.0 h1:bwDU9o4/CVTSpqASJA7+r+rkqpTGamQKYHMRH3wDlRE=
 github.com/goreleaser/nfpm/v2 v2.6.0/go.mod h1:qaMnjBaZz/2vInOIWx0IbuKuaZpaVB6O8oLG0u4qH1Y=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -532,6 +535,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/slack-go/slack v0.9.4 h1:C+FC3zLxLxUTQjDy2RZeMHYon005zsCROiZNWVo+opQ=
+github.com/slack-go/slack v0.9.4/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
@@ -581,6 +586,8 @@ github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oW
 github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
 github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/vartanbeno/go-reddit/v2 v2.0.1 h1:P6ITpf5YHjdy7DHZIbUIDn/iNAoGcEoDQnMa+L4vutw=
+github.com/vartanbeno/go-reddit/v2 v2.0.1/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
 github.com/xanzy/go-gitlab v0.50.3 h1:M7ncgNhCN4jaFNyXxarJhCLa9Qi6fdmCxFFhMTQPZiY=
 github.com/xanzy/go-gitlab v0.50.3/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE=
 github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
diff --git a/internal/builders/golang/targets.go b/internal/builders/golang/targets.go
index 2f93152e..cafaa07f 100644
--- a/internal/builders/golang/targets.go
+++ b/internal/builders/golang/targets.go
@@ -2,6 +2,7 @@ package golang
 
 import (
 	"fmt"
+	"os"
 	"os/exec"
 	"regexp"
 	"strings"
@@ -133,7 +134,11 @@ var (
 
 func goVersion(build config.Build) ([]byte, error) {
 	cmd := exec.Command(build.GoBinary, "version")
-	cmd.Dir = build.Dir // Set Dir to build directory in case of reletive path to GoBinary
+	// If the build.Dir is acessible, set the cmd dir to it in case
+	// of reletive path to GoBinary
+	if _, err := os.Stat(build.Dir); err == nil {
+		cmd.Dir = build.Dir
+	}
 	bts, err := cmd.CombinedOutput()
 	if err != nil {
 		return nil, fmt.Errorf("unable to determine version of go binary (%s): %w", build.GoBinary, err)
diff --git a/internal/builders/golang/testdata/.gitkeep b/internal/builders/golang/testdata/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/pipe/announce/announce.go b/internal/pipe/announce/announce.go
index a12b8eaf..67720af7 100644
--- a/internal/pipe/announce/announce.go
+++ b/internal/pipe/announce/announce.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 
 	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/pipe/reddit"
+	"github.com/goreleaser/goreleaser/internal/pipe/slack"
 	"github.com/goreleaser/goreleaser/internal/pipe/twitter"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -26,6 +28,8 @@ type Announcer interface {
 // nolint: gochecknoglobals
 var announcers = []Announcer{
 	twitter.Pipe{}, // announce to twitter
+	reddit.Pipe{},  // announce to twitter
+	slack.Pipe{},   // announce to slack
 }
 
 // Run the pipe.
diff --git a/internal/pipe/brew/brew.go b/internal/pipe/brew/brew.go
index 6a5b9c5c..07edd23d 100644
--- a/internal/pipe/brew/brew.go
+++ b/internal/pipe/brew/brew.go
@@ -92,6 +92,9 @@ func (Pipe) Default(ctx *context.Context) error {
 		if brew.CommitAuthor.Email == "" {
 			brew.CommitAuthor.Email = "goreleaser@carlosbecker.com"
 		}
+		if brew.CommitMessageTemplate == "" {
+			brew.CommitMessageTemplate = "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
+		}
 		if brew.Name == "" {
 			brew.Name = ctx.Config.ProjectName
 		}
@@ -182,7 +185,10 @@ func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
 		WithField("repo", repo.String()).
 		Info("pushing")
 
-	msg := fmt.Sprintf("Brew formula update for %s version %s", ctx.Config.ProjectName, ctx.Git.CurrentTag)
+	msg, err := tmpl.New(ctx).Apply(brew.CommitMessageTemplate)
+	if err != nil {
+		return err
+	}
 	return cl.CreateFile(ctx, brew.CommitAuthor, repo, []byte(content), gpath, msg)
 }
 
diff --git a/internal/pipe/checksums/checksums.go b/internal/pipe/checksums/checksums.go
index 03d1281c..fc2c7a5f 100644
--- a/internal/pipe/checksums/checksums.go
+++ b/internal/pipe/checksums/checksums.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/extrafiles"
 	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/semerrgroup"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
@@ -55,6 +56,19 @@ func (Pipe) Run(ctx *context.Context) (err error) {
 		return nil
 	}
 
+	extraFiles, err := extrafiles.Find(ctx.Config.Checksum.ExtraFiles)
+	if err != nil {
+		return err
+	}
+
+	for name, path := range extraFiles {
+		artifactList = append(artifactList, &artifact.Artifact{
+			Name: name,
+			Path: path,
+			Type: artifact.UploadableFile,
+		})
+	}
+
 	g := semerrgroup.New(ctx.Parallelism)
 	sumLines := make([]string, len(artifactList))
 	for i, artifact := range artifactList {
diff --git a/internal/pipe/checksums/testdata/foo.txt b/internal/pipe/checksums/testdata/foo.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/pipe/checksums/testdata/foo/bar.txt b/internal/pipe/checksums/testdata/foo/bar.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/pipe/docker/api.go b/internal/pipe/docker/api.go
index 16cd2626..993dfbb3 100644
--- a/internal/pipe/docker/api.go
+++ b/internal/pipe/docker/api.go
@@ -59,7 +59,7 @@ func runCommand(ctx context.Context, dir, binary string, args ...string) error {
 	cmd.Stderr = io.MultiWriter(logext.NewWriter(fields, logext.Error), w)
 	cmd.Stdout = io.MultiWriter(logext.NewWriter(fields, logext.Info), w)
 
-	log.WithFields(fields).Debug("running")
+	log.WithFields(fields).WithField("args", args[1:]).Debug("running")
 	if err := cmd.Run(); err != nil {
 		return fmt.Errorf("%w: %s", err, b.String())
 	}
diff --git a/internal/pipe/nfpm/nfpm.go b/internal/pipe/nfpm/nfpm.go
index f383f557..d6efe12a 100644
--- a/internal/pipe/nfpm/nfpm.go
+++ b/internal/pipe/nfpm/nfpm.go
@@ -140,6 +140,21 @@ func create(ctx *context.Context, fpm config.NFPM, format, arch string, binaries
 		return err
 	}
 
+	debKeyFile, err := tmpl.Apply(overridden.Deb.Signature.KeyFile)
+	if err != nil {
+		return err
+	}
+
+	rpmKeyFile, err := tmpl.Apply(overridden.RPM.Signature.KeyFile)
+	if err != nil {
+		return err
+	}
+
+	apkKeyFile, err := tmpl.Apply(overridden.APK.Signature.KeyFile)
+	if err != nil {
+		return err
+	}
+
 	contents := files.Contents{}
 	for _, content := range overridden.Contents {
 		src, err := tmpl.Apply(content.Source)
@@ -221,7 +236,7 @@ func create(ctx *context.Context, fpm config.NFPM, format, arch string, binaries
 				Breaks: overridden.Deb.Breaks,
 				Signature: nfpm.DebSignature{
 					PackageSignature: nfpm.PackageSignature{
-						KeyFile:       overridden.Deb.Signature.KeyFile,
+						KeyFile:       debKeyFile,
 						KeyPassphrase: getPassphraseFromEnv(ctx, "DEB", fpm.ID),
 					},
 					Type: overridden.Deb.Signature.Type,
@@ -233,7 +248,7 @@ func create(ctx *context.Context, fpm config.NFPM, format, arch string, binaries
 				Compression: overridden.RPM.Compression,
 				Signature: nfpm.RPMSignature{
 					PackageSignature: nfpm.PackageSignature{
-						KeyFile:       overridden.RPM.Signature.KeyFile,
+						KeyFile:       rpmKeyFile,
 						KeyPassphrase: getPassphraseFromEnv(ctx, "RPM", fpm.ID),
 					},
 				},
@@ -245,7 +260,7 @@ func create(ctx *context.Context, fpm config.NFPM, format, arch string, binaries
 			APK: nfpm.APK{
 				Signature: nfpm.APKSignature{
 					PackageSignature: nfpm.PackageSignature{
-						KeyFile:       overridden.APK.Signature.KeyFile,
+						KeyFile:       apkKeyFile,
 						KeyPassphrase: getPassphraseFromEnv(ctx, "APK", fpm.ID),
 					},
 					KeyName: overridden.APK.Signature.KeyName,
diff --git a/internal/pipe/reddit/reddit.go b/internal/pipe/reddit/reddit.go
new file mode 100644
index 00000000..96c996df
--- /dev/null
+++ b/internal/pipe/reddit/reddit.go
@@ -0,0 +1,83 @@
+package reddit
+
+import (
+	"fmt"
+
+	"github.com/apex/log"
+	"github.com/caarlos0/env/v6"
+	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/vartanbeno/go-reddit/v2/reddit"
+)
+
+const (
+	defaultTitleTemplate = `{{ .ProjectName }} {{ .Tag }} is out!`
+	defaultURLTemplate   = `{{ .GitURL }}/releases/tag/{{ .Tag }}`
+)
+
+type Pipe struct{}
+
+func (Pipe) String() string { return "reddit" }
+
+type Config struct {
+	Secret   string `env:"REDDIT_SECRET,notEmpty"`
+	Password string `env:"REDDIT_PASSWORD,notEmpty"`
+}
+
+func (Pipe) Default(ctx *context.Context) error {
+	if ctx.Config.Announce.Reddit.TitleTemplate == "" {
+		ctx.Config.Announce.Reddit.TitleTemplate = defaultTitleTemplate
+	}
+
+	if ctx.Config.Announce.Reddit.URLTemplate == "" {
+		ctx.Config.Announce.Reddit.URLTemplate = defaultURLTemplate
+	}
+
+	return nil
+}
+
+func (Pipe) Announce(ctx *context.Context) error {
+	if ctx.SkipAnnounce {
+		return pipe.ErrSkipAnnounceEnabled
+	}
+	if !ctx.Config.Announce.Reddit.Enabled {
+		return pipe.ErrSkipDisabledPipe
+	}
+
+	title, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Reddit.TitleTemplate)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to reddit: %w", err)
+	}
+
+	url, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Reddit.URLTemplate)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to reddit: %w", err)
+	}
+
+	linkRequest := reddit.SubmitLinkRequest{
+		Subreddit: ctx.Config.Announce.Reddit.Sub,
+		Title:     title,
+		URL:       url,
+	}
+
+	var cfg Config
+	if err := env.Parse(&cfg); err != nil {
+		return fmt.Errorf("announce: failed to announce to reddit: %w", err)
+	}
+
+	credentials := reddit.Credentials{ID: ctx.Config.Announce.Reddit.ApplicationID, Secret: cfg.Secret, Username: ctx.Config.Announce.Reddit.Username, Password: cfg.Password}
+	client, err := reddit.NewClient(credentials)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to reddit: %w", err)
+	}
+
+	post, _, err := client.Post.SubmitLink(ctx, linkRequest)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to reddit: %w", err)
+	}
+
+	log.Infof("announce: The text post is available at: %s\n", post.URL)
+
+	return nil
+}
diff --git a/internal/pipe/scoop/scoop.go b/internal/pipe/scoop/scoop.go
index 9f2e4911..31ce07c6 100644
--- a/internal/pipe/scoop/scoop.go
+++ b/internal/pipe/scoop/scoop.go
@@ -52,11 +52,9 @@ func (Pipe) Default(ctx *context.Context) error {
 	if ctx.Config.Scoop.CommitAuthor.Email == "" {
 		ctx.Config.Scoop.CommitAuthor.Email = "goreleaser@carlosbecker.com"
 	}
-
 	if ctx.Config.Scoop.CommitMessageTemplate == "" {
 		ctx.Config.Scoop.CommitMessageTemplate = "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
 	}
-
 	return nil
 }
 
@@ -127,8 +125,7 @@ func doRun(ctx *context.Context, cl client.Client) error {
 		return pipe.Skip("release is disabled")
 	}
 
-	commitMessage, err := tmpl.New(ctx).
-		Apply(scoop.CommitMessageTemplate)
+	commitMessage, err := tmpl.New(ctx).Apply(scoop.CommitMessageTemplate)
 	if err != nil {
 		return err
 	}
diff --git a/internal/pipe/slack/slack.go b/internal/pipe/slack/slack.go
new file mode 100644
index 00000000..0653cbf5
--- /dev/null
+++ b/internal/pipe/slack/slack.go
@@ -0,0 +1,71 @@
+package slack
+
+import (
+	"fmt"
+
+	"github.com/apex/log"
+	"github.com/caarlos0/env/v6"
+	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/slack-go/slack"
+)
+
+const (
+	defaultUsername        = `GoReleaser`
+	defaultMessageTemplate = `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .GitURL }}/releases/tag/{{ .Tag }}`
+)
+
+type Pipe struct{}
+
+func (Pipe) String() string { return "slack" }
+
+type Config struct {
+	Webhook string `env:"SLACK_WEBHOOK,notEmpty"`
+}
+
+func (Pipe) Default(ctx *context.Context) error {
+	if ctx.Config.Announce.Slack.MessageTemplate == "" {
+		ctx.Config.Announce.Slack.MessageTemplate = defaultMessageTemplate
+	}
+	if ctx.Config.Announce.Slack.Username == "" {
+		ctx.Config.Announce.Slack.Username = defaultUsername
+	}
+	return nil
+}
+
+func (Pipe) Announce(ctx *context.Context) error {
+	if ctx.SkipAnnounce {
+		return pipe.ErrSkipAnnounceEnabled
+	}
+	if !ctx.Config.Announce.Slack.Enabled {
+		return pipe.ErrSkipDisabledPipe
+	}
+
+	msg, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Slack.MessageTemplate)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to slack: %w", err)
+	}
+
+	var cfg Config
+	if err := env.Parse(&cfg); err != nil {
+		return fmt.Errorf("announce: failed to announce to slack: %w", err)
+	}
+
+	log.Infof("posting: '%s'", msg)
+
+	wm := &slack.WebhookMessage{
+		Username:  ctx.Config.Announce.Slack.Username,
+		IconEmoji: ctx.Config.Announce.Slack.IconEmoji,
+		IconURL:   ctx.Config.Announce.Slack.IconURL,
+		Channel:   ctx.Config.Announce.Slack.Channel,
+		Text:      msg,
+	}
+
+	err = slack.PostWebhook(cfg.Webhook, wm)
+	if err != nil {
+		return fmt.Errorf("announce: failed to announce to slack: %w", err)
+	}
+
+	return nil
+}
diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go
index 2bcac9e7..9da00b1d 100644
--- a/pkg/archive/archive.go
+++ b/pkg/archive/archive.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 
 	"github.com/goreleaser/goreleaser/pkg/archive/gzip"
+	"github.com/goreleaser/goreleaser/pkg/archive/tar"
 	"github.com/goreleaser/goreleaser/pkg/archive/targz"
 	"github.com/goreleaser/goreleaser/pkg/archive/tarxz"
 	"github.com/goreleaser/goreleaser/pkg/archive/zip"
@@ -32,5 +33,8 @@ func New(file *os.File) Archive {
 	if strings.HasSuffix(file.Name(), ".zip") {
 		return zip.New(file)
 	}
+	if strings.HasSuffix(file.Name(), ".tar") {
+		return tar.New(file)
+	}
 	return targz.New(file)
 }
diff --git a/pkg/archive/gzip/gzip.go b/pkg/archive/gzip/gzip.go
index 842508ee..a38d2669 100644
--- a/pkg/archive/gzip/gzip.go
+++ b/pkg/archive/gzip/gzip.go
@@ -16,11 +16,6 @@ type Archive struct {
 	gw *gzip.Writer
 }
 
-// Close all closeables.
-func (a Archive) Close() error {
-	return a.gw.Close()
-}
-
 // New gz archive.
 func New(target io.Writer) Archive {
 	// the error will be nil since the compression level is valid
@@ -30,6 +25,11 @@ func New(target io.Writer) Archive {
 	}
 }
 
+// Close all closeables.
+func (a Archive) Close() error {
+	return a.gw.Close()
+}
+
 // Add file to the archive.
 func (a Archive) Add(f config.File) error {
 	if a.gw.Header.Name != "" {
diff --git a/pkg/archive/tar/tar.go b/pkg/archive/tar/tar.go
new file mode 100644
index 00000000..bdae3c4a
--- /dev/null
+++ b/pkg/archive/tar/tar.go
@@ -0,0 +1,74 @@
+// Package tar implements the Archive interface providing tar archiving.
+package tar
+
+import (
+	"archive/tar"
+	"io"
+	"os"
+
+	"github.com/goreleaser/goreleaser/pkg/config"
+)
+
+// Archive as tar.
+type Archive struct {
+	tw *tar.Writer
+}
+
+// New tar archive.
+func New(target io.Writer) Archive {
+	return Archive{
+		tw: tar.NewWriter(target),
+	}
+}
+
+// Close all closeables.
+func (a Archive) Close() error {
+	return a.tw.Close()
+}
+
+// Add file to the archive.
+func (a Archive) Add(f config.File) error {
+	file, err := os.Open(f.Source) // #nosec
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	info, err := os.Lstat(f.Source) // #nosec
+	if err != nil {
+		return err
+	}
+	var link string
+	if info.Mode()&os.ModeSymlink != 0 {
+		link, err = os.Readlink(f.Source) // #nosec
+		if err != nil {
+			return err
+		}
+	}
+	header, err := tar.FileInfoHeader(info, link)
+	if err != nil {
+		return err
+	}
+	header.Name = f.Destination
+	if !f.Info.MTime.IsZero() {
+		header.ModTime = f.Info.MTime
+	}
+	if f.Info.Mode != 0 {
+		header.Mode = int64(f.Info.Mode)
+	}
+	if f.Info.Owner != "" {
+		header.Uid = 0
+		header.Uname = f.Info.Owner
+	}
+	if f.Info.Group != "" {
+		header.Gid = 0
+		header.Gname = f.Info.Group
+	}
+	if err = a.tw.WriteHeader(header); err != nil {
+		return err
+	}
+	if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
+		return nil
+	}
+	_, err = io.Copy(a.tw, file)
+	return err
+}
diff --git a/pkg/archive/targz/targz.go b/pkg/archive/targz/targz.go
index 67a3dea3..983cf529 100644
--- a/pkg/archive/targz/targz.go
+++ b/pkg/archive/targz/targz.go
@@ -3,75 +3,39 @@
 package targz
 
 import (
-	"archive/tar"
 	"compress/gzip"
 	"io"
-	"os"
 
+	"github.com/goreleaser/goreleaser/pkg/archive/tar"
 	"github.com/goreleaser/goreleaser/pkg/config"
 )
 
 // Archive as tar.gz.
 type Archive struct {
 	gw *gzip.Writer
-	tw *tar.Writer
-}
-
-// Close all closeables.
-func (a Archive) Close() error {
-	if err := a.tw.Close(); err != nil {
-		return err
-	}
-	return a.gw.Close()
+	tw *tar.Archive
 }
 
 // New tar.gz archive.
 func New(target io.Writer) Archive {
 	// the error will be nil since the compression level is valid
 	gw, _ := gzip.NewWriterLevel(target, gzip.BestCompression)
-	tw := tar.NewWriter(gw)
+	tw := tar.New(gw)
 	return Archive{
 		gw: gw,
-		tw: tw,
+		tw: &tw,
 	}
 }
 
-// Add file to the archive.
-func (a Archive) Add(f config.File) error {
-	file, err := os.Open(f.Source) // #nosec
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-	info, err := file.Stat()
-	if err != nil {
-		return err
-	}
-	header, err := tar.FileInfoHeader(info, f.Destination)
-	if err != nil {
-		return err
-	}
-	header.Name = f.Destination
-	if !f.Info.MTime.IsZero() {
-		header.ModTime = f.Info.MTime
-	}
-	if f.Info.Mode != 0 {
-		header.Mode = int64(f.Info.Mode)
-	}
-	if f.Info.Owner != "" {
-		header.Uid = 0
-		header.Uname = f.Info.Owner
-	}
-	if f.Info.Group != "" {
-		header.Gid = 0
-		header.Gname = f.Info.Group
-	}
-	if err = a.tw.WriteHeader(header); err != nil {
+// Close all closeables.
+func (a Archive) Close() error {
+	if err := a.tw.Close(); err != nil {
 		return err
 	}
-	if info.IsDir() {
-		return nil
-	}
-	_, err = io.Copy(a.tw, file)
-	return err
+	return a.gw.Close()
+}
+
+// Add file to the archive.
+func (a Archive) Add(f config.File) error {
+	return a.tw.Add(f)
 }
diff --git a/pkg/archive/tarxz/tarxz.go b/pkg/archive/tarxz/tarxz.go
index e36040b5..34e29bc6 100644
--- a/pkg/archive/tarxz/tarxz.go
+++ b/pkg/archive/tarxz/tarxz.go
@@ -3,10 +3,9 @@
 package tarxz
 
 import (
-	"archive/tar"
 	"io"
-	"os"
 
+	"github.com/goreleaser/goreleaser/pkg/archive/tar"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/ulikunitz/xz"
 )
@@ -14,63 +13,28 @@ import (
 // Archive as tar.xz.
 type Archive struct {
 	xzw *xz.Writer
-	tw  *tar.Writer
-}
-
-// Close all closeables.
-func (a Archive) Close() error {
-	if err := a.tw.Close(); err != nil {
-		return err
-	}
-	return a.xzw.Close()
+	tw  *tar.Archive
 }
 
 // New tar.xz archive.
 func New(target io.Writer) Archive {
 	xzw, _ := xz.WriterConfig{DictCap: 16 * 1024 * 1024}.NewWriter(target)
-	tw := tar.NewWriter(xzw)
+	tw := tar.New(xzw)
 	return Archive{
 		xzw: xzw,
-		tw:  tw,
+		tw:  &tw,
 	}
 }
 
-// Add file to the archive.
-func (a Archive) Add(f config.File) error {
-	file, err := os.Open(f.Source) // #nosec
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-	info, err := file.Stat()
-	if err != nil {
-		return err
-	}
-	header, err := tar.FileInfoHeader(info, f.Destination)
-	if err != nil {
-		return err
-	}
-	header.Name = f.Destination
-	if !f.Info.MTime.IsZero() {
-		header.ModTime = f.Info.MTime
-	}
-	if f.Info.Mode != 0 {
-		header.Mode = int64(f.Info.Mode)
-	}
-	if f.Info.Owner != "" {
-		header.Uid = 0
-		header.Uname = f.Info.Owner
-	}
-	if f.Info.Group != "" {
-		header.Gid = 0
-		header.Gname = f.Info.Group
-	}
-	if err = a.tw.WriteHeader(header); err != nil {
+// Close all closeables.
+func (a Archive) Close() error {
+	if err := a.tw.Close(); err != nil {
 		return err
 	}
-	if info.IsDir() {
-		return nil
-	}
-	_, err = io.Copy(a.tw, file)
-	return err
+	return a.xzw.Close()
+}
+
+// Add file to the archive.
+func (a Archive) Add(f config.File) error {
+	return a.tw.Add(f)
 }
diff --git a/pkg/archive/zip/zip.go b/pkg/archive/zip/zip.go
index 89dcf566..ec2e6361 100644
--- a/pkg/archive/zip/zip.go
+++ b/pkg/archive/zip/zip.go
@@ -16,11 +16,6 @@ type Archive struct {
 	z *zip.Writer
 }
 
-// Close all closeables.
-func (a Archive) Close() error {
-	return a.z.Close()
-}
-
 // New zip archive.
 func New(target io.Writer) Archive {
 	compressor := zip.NewWriter(target)
@@ -32,6 +27,11 @@ func New(target io.Writer) Archive {
 	}
 }
 
+// Close all closeables.
+func (a Archive) Close() error {
+	return a.z.Close()
+}
+
 // Add a file to the zip archive.
 func (a Archive) Add(f config.File) error {
 	file, err := os.Open(f.Source) // #nosec
@@ -39,7 +39,7 @@ func (a Archive) Add(f config.File) error {
 		return err
 	}
 	defer file.Close()
-	info, err := file.Stat()
+	info, err := os.Lstat(f.Source) // #nosec
 	if err != nil {
 		return err
 	}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 3b289383..06413757 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -90,27 +90,28 @@ func (r Repo) String() string {
 
 // Homebrew contains the brew section.
 type Homebrew struct {
-	Name             string               `yaml:",omitempty"`
-	Tap              RepoRef              `yaml:",omitempty"`
-	CommitAuthor     CommitAuthor         `yaml:"commit_author,omitempty"`
-	Folder           string               `yaml:",omitempty"`
-	Caveats          string               `yaml:",omitempty"`
-	Plist            string               `yaml:",omitempty"`
-	Install          string               `yaml:",omitempty"`
-	PostInstall      string               `yaml:"post_install,omitempty"`
-	Dependencies     []HomebrewDependency `yaml:",omitempty"`
-	Test             string               `yaml:",omitempty"`
-	Conflicts        []string             `yaml:",omitempty"`
-	Description      string               `yaml:",omitempty"`
-	Homepage         string               `yaml:",omitempty"`
-	License          string               `yaml:",omitempty"`
-	SkipUpload       string               `yaml:"skip_upload,omitempty"`
-	DownloadStrategy string               `yaml:"download_strategy,omitempty"`
-	URLTemplate      string               `yaml:"url_template,omitempty"`
-	CustomRequire    string               `yaml:"custom_require,omitempty"`
-	CustomBlock      string               `yaml:"custom_block,omitempty"`
-	IDs              []string             `yaml:"ids,omitempty"`
-	Goarm            string               `yaml:"goarm,omitempty"`
+	Name                  string               `yaml:",omitempty"`
+	Tap                   RepoRef              `yaml:",omitempty"`
+	CommitAuthor          CommitAuthor         `yaml:"commit_author,omitempty"`
+	CommitMessageTemplate string               `yaml:"commit_msg_template,omitempty"`
+	Folder                string               `yaml:",omitempty"`
+	Caveats               string               `yaml:",omitempty"`
+	Plist                 string               `yaml:",omitempty"`
+	Install               string               `yaml:",omitempty"`
+	PostInstall           string               `yaml:"post_install,omitempty"`
+	Dependencies          []HomebrewDependency `yaml:",omitempty"`
+	Test                  string               `yaml:",omitempty"`
+	Conflicts             []string             `yaml:",omitempty"`
+	Description           string               `yaml:",omitempty"`
+	Homepage              string               `yaml:",omitempty"`
+	License               string               `yaml:",omitempty"`
+	SkipUpload            string               `yaml:"skip_upload,omitempty"`
+	DownloadStrategy      string               `yaml:"download_strategy,omitempty"`
+	URLTemplate           string               `yaml:"url_template,omitempty"`
+	CustomRequire         string               `yaml:"custom_require,omitempty"`
+	CustomBlock           string               `yaml:"custom_block,omitempty"`
+	IDs                   []string             `yaml:"ids,omitempty"`
+	Goarm                 string               `yaml:"goarm,omitempty"`
 }
 
 // Scoop contains the scoop.sh section.
@@ -533,10 +534,11 @@ type Snapshot struct {
 
 // Checksum config.
 type Checksum struct {
-	NameTemplate string   `yaml:"name_template,omitempty"`
-	Algorithm    string   `yaml:"algorithm,omitempty"`
-	IDs          []string `yaml:"ids,omitempty"`
-	Disable      bool     `yaml:"disable,omitempty"`
+	NameTemplate string      `yaml:"name_template,omitempty"`
+	Algorithm    string      `yaml:"algorithm,omitempty"`
+	IDs          []string    `yaml:"ids,omitempty"`
+	Disable      bool        `yaml:"disable,omitempty"`
+	ExtraFiles   []ExtraFile `yaml:"extra_files,omitempty"`
 }
 
 // Docker image config.
@@ -690,6 +692,8 @@ type GoMod struct {
 
 type Announce struct {
 	Twitter Twitter `yaml:"twitter,omitempty"`
+	Reddit  Reddit  `yaml:"reddit,omitempty"`
+	Slack   Slack   `yaml:"slack,omitempty"`
 }
 
 type Twitter struct {
@@ -697,6 +701,24 @@ type Twitter struct {
 	MessageTemplate string `yaml:"message_template,omitempty"`
 }
 
+type Reddit struct {
+	Enabled       bool   `yaml:"enabled,omitempty"`
+	ApplicationID string `yaml:"application_id,omitempty"`
+	Username      string `yaml:"username,omitempty"`
+	TitleTemplate string `yaml:"title_template,omitempty"`
+	URLTemplate   string `yaml:"url_template,omitempty"`
+	Sub           string `yaml:"sub,omitempty"`
+}
+
+type Slack struct {
+	Enabled         bool   `yaml:"enabled,omitempty"`
+	MessageTemplate string `yaml:"message_template,omitempty"`
+	Channel         string `yaml:"channel,omitempty"`
+	Username        string `yaml:"username,omitempty"`
+	IconEmoji       string `yaml:"icon_emoji,omitempty"`
+	IconURL         string `yaml:"icon_url,omitempty"`
+}
+
 // Load config file.
 func Load(file string) (config Project, err error) {
 	f, err := os.Open(file) // #nosec
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index 24da35b4..59b5056f 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -16,9 +16,11 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/milestone"
 	"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
 	"github.com/goreleaser/goreleaser/internal/pipe/project"
+	"github.com/goreleaser/goreleaser/internal/pipe/reddit"
 	"github.com/goreleaser/goreleaser/internal/pipe/release"
 	"github.com/goreleaser/goreleaser/internal/pipe/scoop"
 	"github.com/goreleaser/goreleaser/internal/pipe/sign"
+	"github.com/goreleaser/goreleaser/internal/pipe/slack"
 	"github.com/goreleaser/goreleaser/internal/pipe/snapcraft"
 	"github.com/goreleaser/goreleaser/internal/pipe/snapshot"
 	"github.com/goreleaser/goreleaser/internal/pipe/sourcearchive"
@@ -57,5 +59,7 @@ var Defaulters = []Defaulter{
 	brew.Pipe{},
 	scoop.Pipe{},
 	twitter.Pipe{},
+	reddit.Pipe{},
+	slack.Pipe{},
 	milestone.Pipe{},
 }
diff --git a/www/docs/ci/gitlab.md b/www/docs/ci/gitlab.md
index fa7cca29..c9fd6152 100644
--- a/www/docs/ci/gitlab.md
+++ b/www/docs/ci/gitlab.md
@@ -2,6 +2,8 @@
 
 Below are some example GitLab CI jobs that use GoReleaser to release a project. 
 
+> If you are using private hosted or Enterprise version of Gitlab, please follow this [guide](/scm/gitlab/) before diving into the details.
+
 ## Basic Releasing
 
 You can easily run GoReleaser in GitLab CI using its Docker container. 
diff --git a/www/docs/customization/announce.md b/www/docs/customization/announce.md
index cd3b3681..b1a05e1c 100644
--- a/www/docs/customization/announce.md
+++ b/www/docs/customization/announce.md
@@ -2,7 +2,7 @@
 title: Announce
 ---
 
-GoReleaser can also announce new releases, currently, to Twitter only.
+GoReleaser can also announce new releases, currently, to Twitter, Reddit and Slack only.
 
 It runs at the very end of the pipeline.
 
@@ -30,5 +30,70 @@ announce:
     message_template: 'Awesome project {{.Tag}} is out!'
 ```
 
+## Slack
+
+For it to work, you'll need to [create a new Incoming Webhook](https://api.slack.com/messaging/webhooks), and set some environment variables on your pipeline:
+
+- `SLACK_WEBHOOK`
+
+Then, you can add something like the following to your `.goreleaser.yml` config:
+
+```yaml
+# .goreleaser.yml
+announce:
+  slack:
+    # Wether its enabled or not.
+    # Defaults to false.
+    enabled: true
+
+    # 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!'
+
+    # 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/).
+
+## Reddit
+
+For it to work, you'll need to [create a new Reddit app](https://www.reddit.com/prefs/apps), and set some environment variables on your pipeline:
+
+- `REDDIT_SECRET`
+- `REDDIT_PASSWORD`
+
+Then, you can add something like the following to your `.goreleaser.yml` config:
+
+```yaml
+# .goreleaser.yml
+announce:
+  reddit:
+    # Wether its enabled or not.
+    # Defaults to false.
+    enabled: true
+
+    # Application ID for Reddit Application
+    application_id: ""
+
+    # Username for your Reddit account
+    username: ""
+
+    # URL template to use while publishing.
+    # Defaults to `{{ .GitURL }}/releases/tag/{{ .Tag }}`
+    url_template: 'https://github.com/goreleaser/goreleaser/releases/tag/{{ .Tag }}'
+
+    # Title template to use while publishing.
+    # Defaults to `{{ .ProjectName }} {{ .Tag }} is out!`
+    title_template: ''GoReleaser {{ .Tag }} was just released!''
+```
diff --git a/www/docs/customization/archive.md b/www/docs/customization/archive.md
index ea3fcf38..6da7aef8 100644
--- a/www/docs/customization/archive.md
+++ b/www/docs/customization/archive.md
@@ -20,7 +20,7 @@ archives:
     builds:
     - default
 
-    # Archive format. Valid options are `tar.gz`, `tar.xz`, `gz`, `zip` and `binary`.
+    # Archive format. Valid options are `tar.gz`, `tar.xz`, `tar`, `gz`, `zip` and `binary`.
     # If format is `binary`, no archives are created and the binaries are instead
     # uploaded directly.
     # Default is `tar.gz`.
diff --git a/www/docs/customization/checksum.md b/www/docs/customization/checksum.md
index 0dfe84f0..59def905 100644
--- a/www/docs/customization/checksum.md
+++ b/www/docs/customization/checksum.md
@@ -30,6 +30,15 @@ checksum:
   # Disable the generation/upload of the checksum file.
   # Default is false.
   disable: true
+
+  # You can add extra pre-existing files to the checksums file.
+  # The filename on the checksums file will be the last part of the path (base). If
+  # another file with the same name exists, the latest one found will be used.
+  # Defaults to empty.
+  extra_files:
+    - glob: ./path/to/file.txt
+    - glob: ./glob/**/to/**/file/**/*
+    - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
 ```
 
 !!! tip
diff --git a/www/docs/customization/homebrew.md b/www/docs/customization/homebrew.md
index 36347848..4aea3aa7 100644
--- a/www/docs/customization/homebrew.md
+++ b/www/docs/customization/homebrew.md
@@ -57,7 +57,7 @@ brews:
     # to implement the strategy and add it to your tap repository.
     # Example: https://docs.brew.sh/Formula-Cookbook#specifying-the-download-strategy-explicitly
     # Default is empty.
-    download_strategy: CurlDownloadStrategy.
+    download_strategy: CurlDownloadStrategy
 
     # Allows you to add a custom require_relative at the top of the formula template
     # Default is empty
@@ -69,6 +69,9 @@ brews:
       name: goreleaserbot
       email: goreleaser@carlosbecker.com
 
+    # The project name and current git tag are used in the format string.
+    commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
+
     # Folder inside the repository to put the formula.
     # Default is the root folder.
     folder: Formula
diff --git a/www/docs/customization/nfpm.md b/www/docs/customization/nfpm.md
index b597069d..b6d9097f 100644
--- a/www/docs/customization/nfpm.md
+++ b/www/docs/customization/nfpm.md
@@ -270,13 +270,14 @@ nfpms:
 
       # The package is signed if a key_file is set
       signature:
-        # PGP secret key (can also be ASCII-armored). The passphrase is taken
-        # from the environment variable $NFPM_ID_RPM_PASSPHRASE with a fallback
-        # to $NFPM_ID_PASSPHRASE, where ID is the id of the current nfpm config.
+        # Template to the PGP secret key file path (can also be ASCII-armored).
+        # The passphrase is taken from the environment variable
+        # `$NFPM_ID_RPM_PASSPHRASE` with a fallback to `$NFPM_ID_PASSPHRASE`,
+        # where ID is the id of the current nfpm config.
         # The id will be transformed to uppercase.
         # E.g. If your nfpm id is 'default' then the rpm-specific passphrase
-        # should be set as $NFPM_DEFAULT_RPM_PASSPHRASE
-        key_file: key.gpg
+        # should be set as `$NFPM_DEFAULT_RPM_PASSPHRASE`
+        key_file: '{{ .Env.GPG_KEY_PATH }}'
 
     # Custom configuration applied only to the Deb packager.
     deb:
@@ -306,13 +307,15 @@ nfpms:
 
       # The package is signed if a key_file is set
       signature:
-        # PGP secret key (can also be ASCII-armored). The passphrase is taken
-        # from the environment variable $NFPM_ID_DEB_PASSPHRASE with a fallback
-        # to $NFPM_ID_PASSPHRASE, where ID is the id of the current nfpm config.
+        # Template to the PGP secret key file path (can also be ASCII-armored).
+        # The passphrase is taken from the environment variable
+        # `$NFPM_ID_DEB_PASSPHRASE` with a fallback to `$NFPM_ID_PASSPHRASE`,
+        # where ID is the id of the current nfpm config.
         # The id will be transformed to uppercase.
         # E.g. If your nfpm id is 'default' then the deb-specific passphrase
-        # should be set as $NFPM_DEFAULT_DEB_PASSPHRASE
-        key_file: key.gpg
+        # should be set as `$NFPM_DEFAULT_DEB_PASSPHRASE`
+        key_file: '{{ .Env.GPG_KEY_PATH }}'
+
         # The type describes the signers role, possible values are "origin",
         # "maint" and "archive". If unset, the type defaults to "origin".
         type: origin
@@ -327,13 +330,16 @@ nfpms:
 
       # The package is signed if a key_file is set
       signature:
-        # RSA private key in the PEM format. The passphrase is taken
-        # from the environment variable $NFPM_ID_APK_PASSPHRASE with a fallback
-        # to $NFPM_ID_PASSPHRASE, where ID is the id of the current nfpm config.
+        # Template to the PGP secret key file path (can also be ASCII-armored).
+        # The passphrase is taken from the environment variable
+        # `$NFPM_ID_APK_PASSPHRASE` with a fallback to `$NFPM_ID_PASSPHRASE`,
+        # where ID is the id of the current nfpm config.
         # The id will be transformed to uppercase.
-        # E.g. If your nfpm id is 'default' then the deb-specific passphrase
-        # should be set as $NFPM_DEFAULT_APK_PASSPHRASE
-        key_file: key.gpg
+        # E.g. If your nfpm id is 'default' then the apk-specific passphrase
+        # should be set as `$NFPM_DEFAULT_APK_PASSPHRASE`
+        key_file: '{{ .Env.GPG_KEY_PATH }}'
+
+
         # The name of the signing key. When verifying a package, the signature
         # is matched to the public key store in /etc/apk/keys/<key_name>.rsa.pub.
         # If unset, it defaults to the maintainer email address.
diff --git a/www/docs/install.md b/www/docs/install.md
index 063037b2..e7fb5e41 100644
--- a/www/docs/install.md
+++ b/www/docs/install.md
@@ -132,7 +132,7 @@ All artifacts are checksummed and the checksum file is signed with [cosign][].
 You can verify it using [our public key](https://goreleaser.com/static/goreleaser.pub).
 
 === "OSS"
-    1. Download the files you want, the `goreleaser_checksums.txt` and `goreleaser_checksums.txt.sig` files from the [releases][releases] page.
+    1. Download the files you want, the `checksums.txt` and `checksums.txt.sig` files from the [releases][releases] page.
     1. Get our public key:
       ```sh
       wget https://goreleaser.com/static/goreleaser.pub
@@ -141,16 +141,16 @@ You can verify it using [our public key](https://goreleaser.com/static/gorelease
       ```sh
       cosign verify-blob \
         -key goreleaser.pub \
-        -signature goreleaser_checksums.txt.sig \
-        goreleaser_checksums.txt
+        -signature checksums.txt.sig \
+        checksums.txt
       ```
     1. If the signature is valid, you can then verify the SHA256 sums match with the downloaded binary:
       ```sh
-      sha256sum --ignore-missing -c goreleaser_checksums.txt
+      sha256sum --ignore-missing -c checksums.txt
       ```
 
 === "Pro"
-    1. Download the files you want, the `goreleaser-pro_checksums.txt` and `goreleaser-pro_checksums.txt.sig` files from the [releases][pro-releases] page.
+    1. Download the files you want, the `checksums.txt` and `checksums.txt.sig` files from the [releases][pro-releases] page.
     1. Get our public key:
       ```sh
       wget https://goreleaser.com/static/goreleaser.pub
@@ -159,12 +159,12 @@ You can verify it using [our public key](https://goreleaser.com/static/gorelease
       ```sh
       cosign verify-blob \
         -key goreleaser.pub \
-        -signature goreleaser-pro_checksums.txt.sig \
-        goreleaser-pro_checksums.txt
+        -signature checksums.txt.sig \
+        checksums.txt
       ```
     1. If the signature is ok, you can then verify the SHA256 sums match with the downloaded binary:
       ```sh
-      sha256sum --ignore-missing -c goreleaser-pro_checksums.txt
+      sha256sum --ignore-missing -c checksums.txt
       ```
 
 ## Verifying docker images


Test patch:
diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go
index 75e8dddc..078e5452 100644
--- a/internal/builders/golang/build_test.go
+++ b/internal/builders/golang/build_test.go
@@ -88,6 +88,36 @@ func TestWithDefaults(t *testing.T) {
 			},
 			goBinary: "go",
 		},
+		"empty with custom dir": {
+			build: config.Build{
+				ID:     "foo2",
+				Binary: "foo",
+				Dir:    "./testdata",
+			},
+			targets: []string{
+				"linux_amd64",
+				"linux_386",
+				"linux_arm64",
+				"darwin_amd64",
+				"darwin_arm64",
+			},
+			goBinary: "go",
+		},
+		"empty with custom dir that doest exist": {
+			build: config.Build{
+				ID:     "foo2",
+				Binary: "foo",
+				Dir:    "./nope",
+			},
+			targets: []string{
+				"linux_amd64",
+				"linux_386",
+				"linux_arm64",
+				"darwin_amd64",
+				"darwin_arm64",
+			},
+			goBinary: "go",
+		},
 	} {
 		t.Run(name, func(t *testing.T) {
 			if testcase.build.GoBinary != "" && testcase.build.GoBinary != "go" {
diff --git a/internal/pipe/brew/brew_test.go b/internal/pipe/brew/brew_test.go
index 99e976d3..e0d546c5 100644
--- a/internal/pipe/brew/brew_test.go
+++ b/internal/pipe/brew/brew_test.go
@@ -127,43 +127,65 @@ func TestSplit(t *testing.T) {
 }
 
 func TestRunPipe(t *testing.T) {
-	for name, fn := range map[string]func(ctx *context.Context){
-		"default": func(ctx *context.Context) {
-			ctx.TokenType = context.TokenTypeGitHub
-			ctx.Config.Brews[0].Tap.Owner = "test"
-			ctx.Config.Brews[0].Tap.Name = "test"
-			ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
-		},
-		"custom_download_strategy": func(ctx *context.Context) {
-			ctx.TokenType = context.TokenTypeGitHub
-			ctx.Config.Brews[0].Tap.Owner = "test"
-			ctx.Config.Brews[0].Tap.Name = "test"
-			ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
-
-			ctx.Config.Brews[0].DownloadStrategy = "GitHubPrivateRepositoryReleaseDownloadStrategy"
-		},
-		"custom_require": func(ctx *context.Context) {
-			ctx.TokenType = context.TokenTypeGitHub
-			ctx.Config.Brews[0].Tap.Owner = "test"
-			ctx.Config.Brews[0].Tap.Name = "test"
-			ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
-
-			ctx.Config.Brews[0].DownloadStrategy = "CustomDownloadStrategy"
-			ctx.Config.Brews[0].CustomRequire = "custom_download_strategy"
-		},
-		"custom_block": func(ctx *context.Context) {
-			ctx.TokenType = context.TokenTypeGitHub
-			ctx.Config.Brews[0].Tap.Owner = "test"
-			ctx.Config.Brews[0].Tap.Name = "test"
-			ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
-
-			ctx.Config.Brews[0].CustomBlock = `head "https://github.com/caarlos0/test.git"`
-		},
-		"default_gitlab": func(ctx *context.Context) {
-			ctx.TokenType = context.TokenTypeGitLab
-			ctx.Config.Brews[0].Tap.Owner = "test"
-			ctx.Config.Brews[0].Tap.Name = "test"
-			ctx.Config.Brews[0].Homepage = "https://gitlab.com/goreleaser"
+	type testcase struct {
+		prepare       func(ctx *context.Context)
+		expectedError string
+	}
+	for name, tt := range map[string]testcase{
+		"default": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitHub
+				ctx.Config.Brews[0].Tap.Owner = "test"
+				ctx.Config.Brews[0].Tap.Name = "test"
+				ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
+			},
+		},
+		"custom_download_strategy": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitHub
+				ctx.Config.Brews[0].Tap.Owner = "test"
+				ctx.Config.Brews[0].Tap.Name = "test"
+				ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
+
+				ctx.Config.Brews[0].DownloadStrategy = "GitHubPrivateRepositoryReleaseDownloadStrategy"
+			},
+		},
+		"custom_require": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitHub
+				ctx.Config.Brews[0].Tap.Owner = "test"
+				ctx.Config.Brews[0].Tap.Name = "test"
+				ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
+
+				ctx.Config.Brews[0].DownloadStrategy = "CustomDownloadStrategy"
+				ctx.Config.Brews[0].CustomRequire = "custom_download_strategy"
+			},
+		},
+		"custom_block": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitHub
+				ctx.Config.Brews[0].Tap.Owner = "test"
+				ctx.Config.Brews[0].Tap.Name = "test"
+				ctx.Config.Brews[0].Homepage = "https://github.com/goreleaser"
+
+				ctx.Config.Brews[0].CustomBlock = `head "https://github.com/caarlos0/test.git"`
+			},
+		},
+		"default_gitlab": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitLab
+				ctx.Config.Brews[0].Tap.Owner = "test"
+				ctx.Config.Brews[0].Tap.Name = "test"
+				ctx.Config.Brews[0].Homepage = "https://gitlab.com/goreleaser"
+			},
+		},
+		"invalid_commit_template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.Brews[0].Tap.Owner = "test"
+				ctx.Config.Brews[0].Tap.Name = "test"
+				ctx.Config.Brews[0].CommitMessageTemplate = "{{ .Asdsa }"
+			},
+			expectedError: `template: tmpl:1: unexpected "}" in operand`,
 		},
 	} {
 		t.Run(name, func(t *testing.T) {
@@ -197,7 +219,7 @@ func TestRunPipe(t *testing.T) {
 					},
 				},
 			}
-			fn(ctx)
+			tt.prepare(ctx)
 			ctx.Artifacts.Add(&artifact.Artifact{
 				Name:   "bar_bin.tar.gz",
 				Path:   "doesnt matter",
@@ -228,6 +250,11 @@ func TestRunPipe(t *testing.T) {
 			client := &DummyClient{}
 			distFile := filepath.Join(folder, name+".rb")
 
+			if tt.expectedError != "" {
+				require.EqualError(t, doRun(ctx, ctx.Config.Brews[0], client), tt.expectedError)
+				return
+			}
+
 			require.NoError(t, doRun(ctx, ctx.Config.Brews[0], client))
 			require.True(t, client.CreatedFile)
 			golden.RequireEqualRb(t, []byte(client.Content))
@@ -855,6 +882,7 @@ func TestDefault(t *testing.T) {
 	require.Equal(t, ctx.Config.ProjectName, ctx.Config.Brews[0].Name)
 	require.NotEmpty(t, ctx.Config.Brews[0].CommitAuthor.Name)
 	require.NotEmpty(t, ctx.Config.Brews[0].CommitAuthor.Email)
+	require.NotEmpty(t, ctx.Config.Brews[0].CommitMessageTemplate)
 	require.Equal(t, `bin.install "myproject"`, ctx.Config.Brews[0].Install)
 }
 
diff --git a/internal/pipe/checksums/checksums_test.go b/internal/pipe/checksums/checksums_test.go
index e6f8db62..a901c5d4 100644
--- a/internal/pipe/checksums/checksums_test.go
+++ b/internal/pipe/checksums/checksums_test.go
@@ -238,4 +238,108 @@ func TestDefaultSet(t *testing.T) {
 	require.Equal(t, "checksums.txt", ctx.Config.Checksum.NameTemplate)
 }
 
+func TestPipeCheckSumsWithExtraFiles(t *testing.T) {
+	const binary = "binary"
+	const checksums = "checksums.txt"
+	const extraFileFooRelPath = "./testdata/foo.txt"
+	const extraFileBarRelPath = "./testdata/**/bar.txt"
+	const extraFileFoo = "foo.txt"
+	const extraFileBar = "bar.txt"
+
+	tests := map[string]struct {
+		extraFiles []config.ExtraFile
+		want       []string
+	}{
+		"default": {
+			extraFiles: nil,
+			want: []string{
+				binary,
+			},
+		},
+		"one extra file": {
+			extraFiles: []config.ExtraFile{
+				{Glob: extraFileFooRelPath},
+			},
+			want: []string{
+				extraFileFoo,
+			},
+		},
+		"multiple extra files": {
+			extraFiles: []config.ExtraFile{
+				{Glob: extraFileFooRelPath},
+				{Glob: extraFileBarRelPath},
+			},
+			want: []string{
+				extraFileFoo,
+				extraFileBar,
+			},
+		},
+	}
+
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			folder := t.TempDir()
+			file := filepath.Join(folder, binary)
+			require.NoError(t, os.WriteFile(file, []byte("some string"), 0o644))
+			ctx := context.New(
+				config.Project{
+					Dist:        folder,
+					ProjectName: binary,
+					Checksum: config.Checksum{
+						Algorithm:    "sha256",
+						NameTemplate: "checksums.txt",
+						ExtraFiles:   tt.extraFiles,
+					},
+				},
+			)
+
+			ctx.Artifacts.Add(&artifact.Artifact{
+				Name: binary,
+				Path: file,
+				Type: artifact.UploadableBinary,
+				Extra: map[string]interface{}{
+					"ID": "id-1",
+				},
+			})
+
+			require.NoError(t, Pipe{}.Run(ctx))
+
+			bts, err := os.ReadFile(filepath.Join(folder, checksums))
+
+			require.NoError(t, err)
+			for _, want := range tt.want {
+				if tt.extraFiles == nil {
+					require.Contains(t, string(bts), "61d034473102d7dac305902770471fd50f4c5b26f6831a56dd90b5184b3c30fc  "+want)
+				} else {
+					require.Contains(t, string(bts), "3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  "+want)
+				}
+			}
+		})
+	}
+}
+
+func TestExtraFilesNoMatch(t *testing.T) {
+	dir := t.TempDir()
+	ctx := context.New(
+		config.Project{
+			Dist:        dir,
+			ProjectName: "fake",
+			Checksum: config.Checksum{
+				Algorithm:    "sha256",
+				NameTemplate: "checksums.txt",
+				ExtraFiles:   []config.ExtraFile{{Glob: "./nope/nope.txt"}},
+			},
+		},
+	)
+
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name: "fake",
+		Path: "fake-path",
+		Type: artifact.UploadableBinary,
+	})
+
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.EqualError(t, Pipe{}.Run(ctx), `globbing failed for pattern ./nope/nope.txt: matching "./nope/nope.txt": file does not exist`)
+}
+
 // TODO: add tests for LinuxPackage and UploadableSourceArchive
diff --git a/internal/pipe/nfpm/nfpm_test.go b/internal/pipe/nfpm/nfpm_test.go
index 492c4074..e249b4e4 100644
--- a/internal/pipe/nfpm/nfpm_test.go
+++ b/internal/pipe/nfpm/nfpm_test.go
@@ -189,7 +189,7 @@ func TestRunPipe(t *testing.T) {
 	require.Len(t, ctx.Config.NFPMs[0].Contents, 5, "should not modify the config file list")
 }
 
-func TestInvalidNameTemplate(t *testing.T) {
+func TestInvalidTemplate(t *testing.T) {
 	makeCtx := func() *context.Context {
 		ctx := &context.Context{
 			Version:     "1.2.3",
@@ -261,6 +261,24 @@ func TestInvalidNameTemplate(t *testing.T) {
 		ctx.Config.NFPMs[0].Homepage = "{{ .NOPE_HOMEPAGE }}"
 		require.Contains(t, Pipe{}.Run(ctx).Error(), `template: tmpl:1:3: executing "tmpl" at <.NOPE_HOMEPAGE>: map has no entry for key "NOPE_HOMEPAGE"`)
 	})
+
+	t.Run("deb key file", func(t *testing.T) {
+		ctx := makeCtx()
+		ctx.Config.NFPMs[0].Deb.Signature.KeyFile = "{{ .NOPE_KEY_FILE }}"
+		require.Contains(t, Pipe{}.Run(ctx).Error(), `template: tmpl:1:3: executing "tmpl" at <.NOPE_KEY_FILE>: map has no entry for key "NOPE_KEY_FILE"`)
+	})
+
+	t.Run("rpm key file", func(t *testing.T) {
+		ctx := makeCtx()
+		ctx.Config.NFPMs[0].RPM.Signature.KeyFile = "{{ .NOPE_KEY_FILE }}"
+		require.Contains(t, Pipe{}.Run(ctx).Error(), `template: tmpl:1:3: executing "tmpl" at <.NOPE_KEY_FILE>: map has no entry for key "NOPE_KEY_FILE"`)
+	})
+
+	t.Run("apk key file", func(t *testing.T) {
+		ctx := makeCtx()
+		ctx.Config.NFPMs[0].APK.Signature.KeyFile = "{{ .NOPE_KEY_FILE }}"
+		require.Contains(t, Pipe{}.Run(ctx).Error(), `template: tmpl:1:3: executing "tmpl" at <.NOPE_KEY_FILE>: map has no entry for key "NOPE_KEY_FILE"`)
+	})
 }
 
 func TestRunPipeInvalidContentsSourceTemplate(t *testing.T) {
diff --git a/internal/pipe/reddit/reddit_test.go b/internal/pipe/reddit/reddit_test.go
new file mode 100644
index 00000000..508fcab0
--- /dev/null
+++ b/internal/pipe/reddit/reddit_test.go
@@ -0,0 +1,74 @@
+package reddit
+
+import (
+	"testing"
+
+	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestStringer(t *testing.T) {
+	require.Equal(t, Pipe{}.String(), "reddit")
+}
+
+func TestDefault(t *testing.T) {
+	ctx := context.New(config.Project{})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Equal(t, ctx.Config.Announce.Reddit.TitleTemplate, defaultTitleTemplate)
+}
+
+func TestAnnounceDisabled(t *testing.T) {
+	ctx := context.New(config.Project{})
+	require.NoError(t, Pipe{}.Default(ctx))
+	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
+}
+
+func TestAnnounceInvalidURLTemplate(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Reddit: config.Reddit{
+				Enabled:     true,
+				URLTemplate: "{{ .Foo }",
+			},
+		},
+	})
+	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to reddit: template: tmpl:1: unexpected "}" in operand`)
+}
+
+func TestAnnounceInvalidTitleTemplate(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Reddit: config.Reddit{
+				Enabled:       true,
+				TitleTemplate: "{{ .Foo }",
+			},
+		},
+	})
+	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to reddit: template: tmpl:1: unexpected "}" in operand`)
+}
+
+func TestAnnounceMissingEnv(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Reddit: config.Reddit{
+				Enabled: true,
+			},
+		},
+	})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to reddit: env: environment variable "REDDIT_SECRET" should not be empty`)
+}
+
+func TestAnnounceSkipAnnounce(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Reddit: config.Reddit{
+				Enabled: true,
+			},
+		},
+	})
+	ctx.SkipAnnounce = true
+	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
+}
diff --git a/internal/pipe/slack/slack_test.go b/internal/pipe/slack/slack_test.go
new file mode 100644
index 00000000..c95691b4
--- /dev/null
+++ b/internal/pipe/slack/slack_test.go
@@ -0,0 +1,62 @@
+package slack
+
+import (
+	"testing"
+
+	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestStringer(t *testing.T) {
+	require.Equal(t, Pipe{}.String(), "slack")
+}
+
+func TestDefault(t *testing.T) {
+	ctx := context.New(config.Project{})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Equal(t, ctx.Config.Announce.Slack.MessageTemplate, defaultMessageTemplate)
+}
+
+func TestAnnounceDisabled(t *testing.T) {
+	ctx := context.New(config.Project{})
+	require.NoError(t, Pipe{}.Default(ctx))
+	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
+}
+
+func TestAnnounceInvalidTemplate(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Slack: config.Slack{
+				Enabled:         true,
+				MessageTemplate: "{{ .Foo }",
+			},
+		},
+	})
+	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to slack: template: tmpl:1: unexpected "}" in operand`)
+}
+
+func TestAnnounceMissingEnv(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Slack: config.Slack{
+				Enabled: true,
+			},
+		},
+	})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to slack: env: environment variable "SLACK_WEBHOOK" should not be empty`)
+}
+
+func TestAnnounceSkipAnnounce(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Slack: config.Slack{
+				Enabled: true,
+			},
+		},
+	})
+	ctx.SkipAnnounce = true
+	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
+}
diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go
index feb2aebe..6de1a02a 100644
--- a/pkg/archive/archive_test.go
+++ b/pkg/archive/archive_test.go
@@ -15,7 +15,7 @@ func TestArchive(t *testing.T) {
 	require.NoError(t, empty.Close())
 	require.NoError(t, os.Mkdir(folder+"/folder-inside", 0o755))
 
-	for _, format := range []string{"tar.gz", "zip", "gz", "tar.xz", "willbeatargzanyway"} {
+	for _, format := range []string{"tar.gz", "zip", "gz", "tar.xz", "tar", "willbeatargzanyway"} {
 		format := format
 		t.Run(format, func(t *testing.T) {
 			file, err := os.Create(folder + "/folder." + format)
diff --git a/pkg/archive/tar/tar_test.go b/pkg/archive/tar/tar_test.go
new file mode 100644
index 00000000..a5beeb5e
--- /dev/null
+++ b/pkg/archive/tar/tar_test.go
@@ -0,0 +1,165 @@
+package tar
+
+import (
+	"archive/tar"
+	"io"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/stretchr/testify/require"
+)
+
+func TestTarFile(t *testing.T) {
+	tmp := t.TempDir()
+	f, err := os.Create(filepath.Join(tmp, "test.tar"))
+	require.NoError(t, err)
+	defer f.Close() // nolint: errcheck
+	archive := New(f)
+	defer archive.Close() // nolint: errcheck
+
+	require.Error(t, archive.Add(config.File{
+		Source:      "../testdata/nope.txt",
+		Destination: "nope.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/foo.txt",
+		Destination: "foo.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/sub1",
+		Destination: "sub1",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/sub1/bar.txt",
+		Destination: "sub1/bar.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/sub1/executable",
+		Destination: "sub1/executable",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/sub1/sub2",
+		Destination: "sub1/sub2",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/sub1/sub2/subfoo.txt",
+		Destination: "sub1/sub2/subfoo.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/regular.txt",
+		Destination: "regular.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/link.txt",
+		Destination: "link.txt",
+	}))
+
+	require.NoError(t, archive.Close())
+	require.Error(t, archive.Add(config.File{
+		Source:      "tar.go",
+		Destination: "tar.go",
+	}))
+	require.NoError(t, f.Close())
+
+	f, err = os.Open(f.Name())
+	require.NoError(t, err)
+	defer f.Close() // nolint: errcheck
+
+	info, err := f.Stat()
+	require.NoError(t, err)
+	require.Truef(t, info.Size() < 10000, "archived file should be smaller than %d", info.Size())
+
+	var paths []string
+	r := tar.NewReader(f)
+	for {
+		next, err := r.Next()
+		if err == io.EOF {
+			break
+		}
+		require.NoError(t, err)
+		paths = append(paths, next.Name)
+		t.Logf("%s: %v", next.Name, next.FileInfo().Mode())
+		if next.Name == "sub1/executable" {
+			ex := next.FileInfo().Mode() | 0o111
+			require.Equal(t, next.FileInfo().Mode().String(), ex.String())
+		}
+		if next.Name == "link.txt" {
+			require.Equal(t, next.Linkname, "regular.txt")
+		}
+	}
+	require.Equal(t, []string{
+		"foo.txt",
+		"sub1",
+		"sub1/bar.txt",
+		"sub1/executable",
+		"sub1/sub2",
+		"sub1/sub2/subfoo.txt",
+		"regular.txt",
+		"link.txt",
+	}, paths)
+}
+
+func TestTarFileInfo(t *testing.T) {
+	now := time.Now().Truncate(time.Second)
+	f, err := os.Create(filepath.Join(t.TempDir(), "test.tar"))
+	require.NoError(t, err)
+	defer f.Close() // nolint: errcheck
+	archive := New(f)
+	defer archive.Close() // nolint: errcheck
+
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/foo.txt",
+		Destination: "nope.txt",
+		Info: config.FileInfo{
+			Mode:  0o755,
+			Owner: "carlos",
+			Group: "root",
+			MTime: now,
+		},
+	}))
+
+	require.NoError(t, archive.Close())
+	require.NoError(t, f.Close())
+
+	f, err = os.Open(f.Name())
+	require.NoError(t, err)
+	defer f.Close() // nolint: errcheck
+
+	var found int
+	r := tar.NewReader(f)
+	for {
+		next, err := r.Next()
+		if err == io.EOF {
+			break
+		}
+		require.NoError(t, err)
+
+		found++
+		require.Equal(t, "nope.txt", next.Name)
+		require.Equal(t, now, next.ModTime)
+		require.Equal(t, fs.FileMode(0o755), next.FileInfo().Mode())
+		require.Equal(t, "carlos", next.Uname)
+		require.Equal(t, 0, next.Uid)
+		require.Equal(t, "root", next.Gname)
+		require.Equal(t, 0, next.Gid)
+	}
+	require.Equal(t, 1, found)
+}
+
+func TestTarInvalidLink(t *testing.T) {
+	tmp := t.TempDir()
+	f, err := os.Create(filepath.Join(tmp, "test.tar"))
+	require.NoError(t, err)
+	defer f.Close() // nolint: errcheck
+	archive := New(f)
+	defer archive.Close() // nolint: errcheck
+
+	require.EqualError(t, archive.Add(config.File{
+		Source:      "../testdata/badlink.txt",
+		Destination: "badlink.txt",
+	}), "open ../testdata/badlink.txt: no such file or directory")
+}
diff --git a/pkg/archive/targz/targz_test.go b/pkg/archive/targz/targz_test.go
index 39163a4e..7488ae8f 100644
--- a/pkg/archive/targz/targz_test.go
+++ b/pkg/archive/targz/targz_test.go
@@ -50,6 +50,14 @@ func TestTarGzFile(t *testing.T) {
 		Source:      "../testdata/sub1/sub2/subfoo.txt",
 		Destination: "sub1/sub2/subfoo.txt",
 	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/regular.txt",
+		Destination: "regular.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/link.txt",
+		Destination: "link.txt",
+	}))
 
 	require.NoError(t, archive.Close())
 	require.Error(t, archive.Add(config.File{
@@ -84,6 +92,9 @@ func TestTarGzFile(t *testing.T) {
 			ex := next.FileInfo().Mode() | 0o111
 			require.Equal(t, next.FileInfo().Mode().String(), ex.String())
 		}
+		if next.Name == "link.txt" {
+			require.Equal(t, next.Linkname, "regular.txt")
+		}
 	}
 	require.Equal(t, []string{
 		"foo.txt",
@@ -92,6 +103,8 @@ func TestTarGzFile(t *testing.T) {
 		"sub1/executable",
 		"sub1/sub2",
 		"sub1/sub2/subfoo.txt",
+		"regular.txt",
+		"link.txt",
 	}, paths)
 }
 
diff --git a/pkg/archive/tarxz/tarxz_test.go b/pkg/archive/tarxz/tarxz_test.go
index 3ac68a75..59125bea 100644
--- a/pkg/archive/tarxz/tarxz_test.go
+++ b/pkg/archive/tarxz/tarxz_test.go
@@ -50,6 +50,14 @@ func TestTarXzFile(t *testing.T) {
 		Source:      "../testdata/sub1/sub2/subfoo.txt",
 		Destination: "sub1/sub2/subfoo.txt",
 	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/regular.txt",
+		Destination: "regular.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/link.txt",
+		Destination: "link.txt",
+	}))
 
 	require.NoError(t, archive.Close())
 	require.Error(t, archive.Add(config.File{
@@ -83,6 +91,9 @@ func TestTarXzFile(t *testing.T) {
 			ex := next.FileInfo().Mode() | 0o111
 			require.Equal(t, next.FileInfo().Mode().String(), ex.String())
 		}
+		if next.Name == "link.txt" {
+			require.Equal(t, next.Linkname, "regular.txt")
+		}
 	}
 	require.Equal(t, []string{
 		"foo.txt",
@@ -91,6 +102,8 @@ func TestTarXzFile(t *testing.T) {
 		"sub1/executable",
 		"sub1/sub2",
 		"sub1/sub2/subfoo.txt",
+		"regular.txt",
+		"link.txt",
 	}, paths)
 }
 
diff --git a/pkg/archive/testdata/badlink.txt b/pkg/archive/testdata/badlink.txt
new file mode 120000
index 00000000..35b629a1
--- /dev/null
+++ b/pkg/archive/testdata/badlink.txt
@@ -0,0 +1,1 @@
+regular3.txt
\ No newline at end of file
diff --git a/pkg/archive/testdata/link.txt b/pkg/archive/testdata/link.txt
new file mode 120000
index 00000000..397e1077
--- /dev/null
+++ b/pkg/archive/testdata/link.txt
@@ -0,0 +1,1 @@
+regular.txt
\ No newline at end of file
diff --git a/pkg/archive/testdata/regular.txt b/pkg/archive/testdata/regular.txt
new file mode 100644
index 00000000..fa4665e9
--- /dev/null
+++ b/pkg/archive/testdata/regular.txt
@@ -0,0 +1,1 @@
+regular file
diff --git a/pkg/archive/zip/zip_test.go b/pkg/archive/zip/zip_test.go
index 49428fb6..fb371149 100644
--- a/pkg/archive/zip/zip_test.go
+++ b/pkg/archive/zip/zip_test.go
@@ -48,6 +48,14 @@ func TestZipFile(t *testing.T) {
 		Source:      "../testdata/sub1/sub2/subfoo.txt",
 		Destination: "sub1/sub2/subfoo.txt",
 	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/regular.txt",
+		Destination: "regular.txt",
+	}))
+	require.NoError(t, archive.Add(config.File{
+		Source:      "../testdata/link.txt",
+		Destination: "link.txt",
+	}))
 
 	require.NoError(t, archive.Close())
 	require.Error(t, archive.Add(config.File{
@@ -62,7 +70,7 @@ func TestZipFile(t *testing.T) {
 
 	info, err := f.Stat()
 	require.NoError(t, err)
-	require.Truef(t, info.Size() < 900, "archived file should be smaller than %d", info.Size())
+	require.Truef(t, info.Size() < 1000, "archived file should be smaller than %d", info.Size())
 
 	r, err := zip.NewReader(f, info.Size())
 	require.NoError(t, err)
@@ -75,12 +83,17 @@ func TestZipFile(t *testing.T) {
 			ex := zf.Mode() | 0o111
 			require.Equal(t, zf.Mode().String(), ex.String())
 		}
+		if zf.Name == "link.txt" {
+			require.True(t, zf.FileInfo().Mode()&os.ModeSymlink != 0)
+		}
 	}
 	require.Equal(t, []string{
 		"foo.txt",
 		"sub1/bar.txt",
 		"sub1/executable",
 		"sub1/sub2/subfoo.txt",
+		"regular.txt",
+		"link.txt",
 	}, paths)
 }