Repository: goreleaser/goreleaser
Pull request #2480 (2021-09-12 02:12:34)

feat: improve output and pipe skipping
- pipes may optionally implement the new `Skipper` interface, if they do and the `Skip` method returns `true`, the entire pipe will be skipped and not even its name will show up on the output
  - this is also true for defaulters, publishers and announcers
- less log output also means its more likely that the user will see the deprecation notices and other warnings, as well as making debugging a bit easier
- pipes that implement skipper may also avoid repetitive checks like if its skip publish/announce is set and so on
- hopefully improve test coverage in some places
- left a dozen of todos for the future me
- the `dist/config.yaml` file should now be shorter by avoiding running defaults for features that will be skipped anyway - this should help when debugging user issues, hopefully
- improved brew and scoop run vs publish issue
  - on run, it only creates the formula/manifest in the `dist` folder, and add it as an artifact
  - later, if publishing is enabled, it uploads the files
- fixed some `String` impls here and there, and some godocs as well 

---

Its a very big PR, so sorry, but I got carried away... let me know if you have any questions.

---

If you can help testing it, it would be very much appreciated! I did test everything and test coverage is pretty good, but giving it a run on a project you have shouldn't hurt :) 

---

### Example

Before, with a config file like this:

```yaml
builds:
  - env:
      - CGO_ENABLED=0
    goos: [darwin]
    goarch: [amd64]
checksum:
  name_template: 'checksums.txt'
snapshot:
  name_template: "{{ incpatch .Version }}-next"

```

You'll get an output like this:

<img width="901" alt="image" src="https://user-images.githubusercontent.com/245435/133707408-b4491546-3034-4a2c-a06a-18e8a0a360d9.png">

Whereas after this patch, you'll get something like:

<img width="835" alt="image" src="https://user-images.githubusercontent.com/245435/133707427-83119928-8516-499d-82fa-f74d6b1baaa2.png">


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/7n7uDCW917SLxSh3duzqQo2PKaK6](https://vercel.com/goreleaser/goreleaser/7n7uDCW917SLxSh3duzqQo2PKaK6)  
✅ Preview: [https://goreleaser-git-output-goreleaser.vercel.app](https://goreleaser-git-output-goreleaser.vercel.app)


codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2480?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2480](https://codecov.io/gh/goreleaser/goreleaser/pull/2480?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (16eeec8) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/35cb87e66abcd6a9642fe31ce4e8544ad370f123?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (35cb87e) will **decrease** coverage by `0.54%`.
> The diff coverage is `81.11%`.

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

```diff
@@            Coverage Diff             @@
##           master    #2480      +/-   ##
==========================================
- Coverage   83.41%   82.87%   -0.55%     
==========================================
  Files          91       92       +1     
  Lines        6506     6470      -36     
==========================================
- Hits         5427     5362      -65     
- Misses        918      952      +34     
+ Partials      161      156       -5     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2480?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/artifact/artifact.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvYXJ0aWZhY3QvYXJ0aWZhY3QuZ28=) | `95.36% <0.00%> (-2.60%)` | :arrow_down: |
| [internal/http/http.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvaHR0cC9odHRwLmdv) | `91.92% <ø> (-0.07%)` | :arrow_down: |
| [internal/pipe/pipe.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9waXBlLmdv) | `100.00% <ø> (ø)` | |
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/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/scoop/scoop.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9zY29vcC9zY29vcC5nbw==) | `74.59% <51.11%> (-3.94%)` | :arrow_down: |
| [internal/pipe/brew/brew.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9icmV3L2JyZXcuZ28=) | `77.27% <75.67%> (-1.58%)` | :arrow_down: |
| [cmd/build.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-Y21kL2J1aWxkLmdv) | `97.52% <100.00%> (+0.06%)` | :arrow_up: |
| [cmd/release.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-Y21kL3JlbGVhc2UuZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/middleware/errhandler/error.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvbWlkZGxld2FyZS9lcnJoYW5kbGVyL2Vycm9yLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/middleware/logging/logging.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvbWlkZGxld2FyZS9sb2dnaW5nL2xvZ2dpbmcuZ28=) | `100.00% <100.00%> (ø)` | |
| ... and [34 more](https://codecov.io/gh/goreleaser/goreleaser/pull/2480/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2480?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/2480?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [35cb87e...16eeec8](https://codecov.io/gh/goreleaser/goreleaser/pull/2480?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/cmd/build.go b/cmd/build.go
index 43bb2010..653da43b 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -9,7 +9,9 @@ import (
 	"github.com/apex/log"
 	"github.com/caarlos0/ctrlc"
 	"github.com/fatih/color"
-	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/middleware/errhandler"
+	"github.com/goreleaser/goreleaser/internal/middleware/logging"
+	"github.com/goreleaser/goreleaser/internal/middleware/skip"
 	"github.com/goreleaser/goreleaser/internal/pipeline"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
@@ -97,10 +99,13 @@ func buildProject(options buildOpts) (*context.Context, error) {
 	}
 	return ctx, ctrlc.Default.Run(ctx, func() error {
 		for _, pipe := range pipeline.BuildPipeline {
-			if err := middleware.Logging(
-				pipe.String(),
-				middleware.ErrHandler(pipe.Run),
-				middleware.DefaultInitialPadding,
+			if err := skip.Maybe(
+				pipe,
+				logging.Log(
+					pipe.String(),
+					errhandler.Handle(pipe.Run),
+					logging.DefaultInitialPadding,
+				),
 			)(ctx); err != nil {
 				return err
 			}
diff --git a/cmd/release.go b/cmd/release.go
index 261290b4..56b921b3 100644
--- a/cmd/release.go
+++ b/cmd/release.go
@@ -7,7 +7,9 @@ import (
 	"github.com/apex/log"
 	"github.com/caarlos0/ctrlc"
 	"github.com/fatih/color"
-	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/middleware/errhandler"
+	"github.com/goreleaser/goreleaser/internal/middleware/logging"
+	"github.com/goreleaser/goreleaser/internal/middleware/skip"
 	"github.com/goreleaser/goreleaser/internal/pipe/git"
 	"github.com/goreleaser/goreleaser/internal/pipeline"
 	"github.com/goreleaser/goreleaser/pkg/context"
@@ -101,10 +103,13 @@ func releaseProject(options releaseOpts) (*context.Context, error) {
 	setupReleaseContext(ctx, options)
 	return ctx, ctrlc.Default.Run(ctx, func() error {
 		for _, pipe := range pipeline.Pipeline {
-			if err := middleware.Logging(
-				pipe.String(),
-				middleware.ErrHandler(pipe.Run),
-				middleware.DefaultInitialPadding,
+			if err := skip.Maybe(
+				pipe,
+				logging.Log(
+					pipe.String(),
+					errhandler.Handle(pipe.Run),
+					logging.DefaultInitialPadding,
+				),
 			)(ctx); err != nil {
 				return err
 			}
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index f92fa777..ff9df69c 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -48,6 +48,10 @@ const (
 	Signature
 	// UploadableSourceArchive is the archive with the current commit source code.
 	UploadableSourceArchive
+	// BrewTap is an uploadable homebrew tap recipe file.
+	BrewTap
+	// ScoopManifest is an uploadable scoop manifest file.
+	ScoopManifest
 )
 
 func (t Type) String() string {
@@ -72,6 +76,10 @@ func (t Type) String() string {
 		return "Signature"
 	case UploadableSourceArchive:
 		return "Source"
+	case BrewTap:
+		return "Brew Tap"
+	case ScoopManifest:
+		return "Scoop Manifest"
 	default:
 		return "unknown"
 	}
@@ -126,8 +134,8 @@ func (a Artifact) Checksum(algorithm string) (string, error) {
 	default:
 		return "", fmt.Errorf("invalid algorithm: %s", algorithm)
 	}
-	_, err = io.Copy(h, file)
-	if err != nil {
+
+	if _, err := io.Copy(h, file); err != nil {
 		return "", fmt.Errorf("failed to checksum: %w", err)
 	}
 	return hex.EncodeToString(h.Sum(nil)), nil
diff --git a/internal/artifact/extras.go b/internal/artifact/extras.go
deleted file mode 100644
index c146142d..00000000
--- a/internal/artifact/extras.go
+++ /dev/null
@@ -1,1 +0,0 @@
-package artifact
diff --git a/internal/client/client.go b/internal/client/client.go
index c87768c7..0998bcaa 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -55,7 +56,7 @@ func New(ctx *context.Context) (Client, error) {
 	return nil, nil
 }
 
-func NewWithToken(ctx *context.Context, token string) (Client, error) {
+func newWithToken(ctx *context.Context, token string) (Client, error) {
 	if ctx.TokenType == context.TokenTypeGitHub {
 		return NewGitHub(ctx, token)
 	}
@@ -68,6 +69,18 @@ func NewWithToken(ctx *context.Context, token string) (Client, error) {
 	return nil, nil
 }
 
+func NewIfToken(ctx *context.Context, cli Client, token string) (Client, error) {
+	if token == "" {
+		return cli, nil
+	}
+	token, err := tmpl.New(ctx).ApplySingleEnvOnly(token)
+	if err != nil {
+		return nil, err
+	}
+	log.Debug("using custom token")
+	return newWithToken(ctx, token)
+}
+
 // ErrNoMilestoneFound is an error when no milestone is found.
 type ErrNoMilestoneFound struct {
 	Title string
diff --git a/internal/client/github.go b/internal/client/github.go
index d6da97c2..ee92dbc0 100644
--- a/internal/client/github.go
+++ b/internal/client/github.go
@@ -29,6 +29,7 @@ func NewGitHub(ctx *context.Context, token string) (Client, error) {
 	ts := oauth2.StaticTokenSource(
 		&oauth2.Token{AccessToken: token},
 	)
+
 	httpClient := oauth2.NewClient(ctx, ts)
 	base := httpClient.Transport.(*oauth2.Transport).Base
 	if base == nil || reflect.ValueOf(base).IsNil() {
diff --git a/internal/http/http.go b/internal/http/http.go
index 14382ee2..41f1bc0a 100644
--- a/internal/http/http.go
+++ b/internal/http/http.go
@@ -141,10 +141,6 @@ type ResponseChecker func(*h.Response) error
 
 // Upload does the actual uploading work.
 func Upload(ctx *context.Context, uploads []config.Upload, kind string, check ResponseChecker) error {
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
-
 	// Handle every configured upload
 	for _, upload := range uploads {
 		upload := upload
diff --git a/internal/middleware/doc.go b/internal/middleware/doc.go
deleted file mode 100644
index 9ab9c5f4..00000000
--- a/internal/middleware/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package middleware define middlewares for Actions.
-package middleware
diff --git a/internal/middleware/error.go b/internal/middleware/errhandler/error.go
similarity index 60%
rename from internal/middleware/error.go
rename to internal/middleware/errhandler/error.go
index f4d063cd..d4b7defa 100644
--- a/internal/middleware/error.go
+++ b/internal/middleware/errhandler/error.go
@@ -1,23 +1,20 @@
-package middleware
+package errhandler
 
 import (
 	"github.com/apex/log"
+	"github.com/goreleaser/goreleaser/internal/middleware"
 	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
-// ErrHandler handles an action error, ignoring and logging pipe skipped
+// Handle handles an action error, ignoring and logging pipe skipped
 // errors.
-func ErrHandler(action Action) Action {
+func Handle(action middleware.Action) middleware.Action {
 	return func(ctx *context.Context) error {
 		err := action(ctx)
 		if err == nil {
 			return nil
 		}
-		if pipe.IsExpectedSkip(err) {
-			log.WithError(err).Debug("pipe skipped")
-			return nil
-		}
 		if pipe.IsSkip(err) {
 			log.WithError(err).Warn("pipe skipped")
 			return nil
diff --git a/internal/middleware/logging.go b/internal/middleware/logging/logging.go
similarity index 82%
rename from internal/middleware/logging.go
rename to internal/middleware/logging/logging.go
index 8cf3d4e5..2ef23fcc 100644
--- a/internal/middleware/logging.go
+++ b/internal/middleware/logging/logging.go
@@ -1,9 +1,10 @@
-package middleware
+package logging
 
 import (
 	"github.com/apex/log"
 	"github.com/apex/log/handlers/cli"
 	"github.com/fatih/color"
+	"github.com/goreleaser/goreleaser/internal/middleware"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
@@ -16,13 +17,13 @@ const DefaultInitialPadding Padding = 3
 // ExtraPadding is the double of the DefaultInitialPadding.
 const ExtraPadding = DefaultInitialPadding * 2
 
-// Logging pretty prints the given action and its title.
+// Log pretty prints the given action and its title.
 // You can have different padding levels by providing different initial
 // paddings. The middleware will print the title in the given padding and the
 // action logs in padding+default padding.
 // The default padding in the log library is 3.
 // The middleware always resets to the default padding.
-func Logging(title string, next Action, padding Padding) Action {
+func Log(title string, next middleware.Action, padding Padding) middleware.Action {
 	return func(ctx *context.Context) error {
 		defer func() {
 			cli.Default.Padding = int(DefaultInitialPadding)
diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go
index de81de7d..71f08ada 100644
--- a/internal/middleware/middleware.go
+++ b/internal/middleware/middleware.go
@@ -1,3 +1,4 @@
+// Package middleware define middlewares for Actions.
 package middleware
 
 import "github.com/goreleaser/goreleaser/pkg/context"
diff --git a/internal/middleware/skip/skip.go b/internal/middleware/skip/skip.go
new file mode 100644
index 00000000..bd366f68
--- /dev/null
+++ b/internal/middleware/skip/skip.go
@@ -0,0 +1,27 @@
+// Package skip can skip an entire Action.
+package skip
+
+import (
+	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/pkg/context"
+)
+
+// Skipper defines a method to skip an entire Piper.
+type Skipper interface {
+	// Skip returns true if the Piper should be skipped.
+	Skip(ctx *context.Context) bool
+}
+
+// Maybe returns an action that skips immediately if the given p is a Skipper
+// and its Skip method returns true.
+func Maybe(skipper interface{}, next middleware.Action) middleware.Action {
+	if skipper, ok := skipper.(Skipper); ok {
+		return func(ctx *context.Context) error {
+			if skipper.Skip(ctx) {
+				return nil
+			}
+			return next(ctx)
+		}
+	}
+	return next
+}
diff --git a/internal/pipe/announce/announce.go b/internal/pipe/announce/announce.go
index 0806ec13..32dc8714 100644
--- a/internal/pipe/announce/announce.go
+++ b/internal/pipe/announce/announce.go
@@ -4,7 +4,9 @@ package announce
 import (
 	"fmt"
 
-	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/middleware/errhandler"
+	"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/reddit"
 	"github.com/goreleaser/goreleaser/internal/pipe/slack"
@@ -14,13 +16,6 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
-// Pipe that announces releases.
-type Pipe struct{}
-
-func (Pipe) String() string {
-	return "announcing"
-}
-
 // Announcer should be implemented by pipes that want to announce releases.
 type Announcer interface {
 	fmt.Stringer
@@ -39,13 +34,22 @@ var announcers = []Announcer{
 	twitter.Pipe{},
 }
 
+// Pipe that announces releases.
+type Pipe struct{}
+
+func (Pipe) String() string                 { return "announcing" }
+func (Pipe) Skip(ctx *context.Context) bool { return ctx.SkipAnnounce }
+
 // Run the pipe.
 func (Pipe) Run(ctx *context.Context) error {
 	for _, announcer := range announcers {
-		if err := middleware.Logging(
-			announcer.String(),
-			middleware.ErrHandler(announcer.Announce),
-			middleware.ExtraPadding,
+		if err := skip.Maybe(
+			announcer,
+			logging.Log(
+				announcer.String(),
+				errhandler.Handle(announcer.Announce),
+				logging.ExtraPadding,
+			),
 		)(ctx); err != nil {
 			return fmt.Errorf("%s: failed to announce release: %w", announcer.String(), err)
 		}
diff --git a/internal/pipe/artifactory/artifactory.go b/internal/pipe/artifactory/artifactory.go
index 71ada646..e3c5034b 100644
--- a/internal/pipe/artifactory/artifactory.go
+++ b/internal/pipe/artifactory/artifactory.go
@@ -15,10 +15,8 @@ import (
 // Pipe for Artifactory.
 type Pipe struct{}
 
-// String returns the description of the pipe.
-func (Pipe) String() string {
-	return "artifactory"
-}
+func (Pipe) String() string                 { return "artifactory" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Artifactories) == 0 }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -35,10 +33,6 @@ func (Pipe) Default(ctx *context.Context) error {
 //
 // Docs: https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-Example-DeployinganArtifact
 func (Pipe) Publish(ctx *context.Context) error {
-	if len(ctx.Config.Artifactories) == 0 {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	// Check requirements for every instance we have configured.
 	// If not fulfilled, we can skip this pipeline
 	for _, instance := range ctx.Config.Artifactories {
diff --git a/internal/pipe/before/before.go b/internal/pipe/before/before.go
index 89a7175b..72575772 100644
--- a/internal/pipe/before/before.go
+++ b/internal/pipe/before/before.go
@@ -18,10 +18,8 @@ import (
 // Pipe is a global hook pipe.
 type Pipe struct{}
 
-// String is the name of this pipe.
-func (Pipe) String() string {
-	return "running before hooks"
-}
+func (Pipe) String() string                 { return "running before hooks" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Before.Hooks) == 0 }
 
 // Run executes the hooks.
 func (Pipe) Run(ctx *context.Context) error {
diff --git a/internal/pipe/blob/blob.go b/internal/pipe/blob/blob.go
index ef46cdb5..c4643783 100644
--- a/internal/pipe/blob/blob.go
+++ b/internal/pipe/blob/blob.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/semerrgroup"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -14,9 +13,8 @@ import (
 type Pipe struct{}
 
 // String returns the description of the pipe.
-func (Pipe) String() string {
-	return "blobs"
-}
+func (Pipe) String() string                 { return "blobs" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Blobs) == 0 }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -35,10 +33,6 @@ func (Pipe) Default(ctx *context.Context) error {
 
 // Publish to specified blob bucket url.
 func (Pipe) Publish(ctx *context.Context) error {
-	if len(ctx.Config.Blobs) == 0 {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	g := semerrgroup.New(ctx.Parallelism)
 	for _, conf := range ctx.Config.Blobs {
 		conf := conf
diff --git a/internal/pipe/blob/upload.go b/internal/pipe/blob/upload.go
index 06ab7367..29a53803 100644
--- a/internal/pipe/blob/upload.go
+++ b/internal/pipe/blob/upload.go
@@ -23,7 +23,7 @@ import (
 	_ "gocloud.dev/blob/gcsblob"
 	_ "gocloud.dev/blob/s3blob"
 
-	// import the secrets packages we want to be able to open:.
+	// import the secrets packages we want to be able to be used.
 	_ "gocloud.dev/secrets/awskms"
 	_ "gocloud.dev/secrets/azurekeyvault"
 	_ "gocloud.dev/secrets/gcpkms"
@@ -87,7 +87,7 @@ func doUpload(ctx *context.Context, conf config.Blob) error {
 		filter = artifact.And(filter, artifact.ByIDs(conf.IDs...))
 	}
 
-	up := newUploader(ctx)
+	up := &productionUploader{}
 	if err := up.Open(ctx, bucketURL); err != nil {
 		return handleError(err, bucketURL)
 	}
@@ -160,13 +160,6 @@ func handleError(err error, url string) error {
 	}
 }
 
-func newUploader(ctx *context.Context) uploader {
-	if ctx.SkipPublish {
-		return &skipUploader{}
-	}
-	return &productionUploader{}
-}
-
 func getData(ctx *context.Context, conf config.Blob, path string) ([]byte, error) {
 	data, err := os.ReadFile(path)
 	if err != nil {
@@ -194,18 +187,6 @@ type uploader interface {
 	Upload(ctx *context.Context, path string, data []byte) error
 }
 
-// skipUploader is used when --skip-upload is set and will just log
-// things without really doing anything.
-type skipUploader struct{}
-
-func (u *skipUploader) Close() error                            { return nil }
-func (u *skipUploader) Open(_ *context.Context, _ string) error { return nil }
-
-func (u *skipUploader) Upload(_ *context.Context, path string, _ []byte) error {
-	log.WithField("path", path).Warn("upload skipped because skip-publish is set")
-	return nil
-}
-
 // productionUploader actually do upload to.
 type productionUploader struct {
 	bucket *blob.Bucket
diff --git a/internal/pipe/brew/brew.go b/internal/pipe/brew/brew.go
index 07edd23d..2e0c1212 100644
--- a/internal/pipe/brew/brew.go
+++ b/internal/pipe/brew/brew.go
@@ -20,6 +20,8 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
+const brewConfigExtra = "BrewConfig"
+
 // ErrNoArchivesFound happens when 0 archives are found.
 var ErrNoArchivesFound = errors.New("no linux/macos archives found")
 
@@ -33,17 +35,50 @@ type ErrTokenTypeNotImplementedForBrew struct {
 }
 
 func (e ErrTokenTypeNotImplementedForBrew) Error() string {
-	if e.TokenType != "" {
-		return fmt.Sprintf("token type %q not implemented for brew pipe", e.TokenType)
-	}
-	return "token type not implemented for brew pipe"
+	return fmt.Sprintf("token type %q not implemented for brew pipe", e.TokenType)
 }
 
 // Pipe for brew deployment.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "homebrew tap formula"
+func (Pipe) String() string                 { return "homebrew tap formula" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Brews) == 0 }
+
+func (Pipe) Default(ctx *context.Context) error {
+	for i := range ctx.Config.Brews {
+		brew := &ctx.Config.Brews[i]
+
+		if brew.Install == "" {
+			brew.Install = fmt.Sprintf(`bin.install "%s"`, ctx.Config.ProjectName)
+			log.Warnf("optimistically guessing `brew[%d].install` to be `%s`", i, brew.Install)
+		}
+		if brew.CommitAuthor.Name == "" {
+			brew.CommitAuthor.Name = "goreleaserbot"
+		}
+		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
+		}
+		if brew.Goarm == "" {
+			brew.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)
 }
 
 // Publish brew formula.
@@ -60,12 +95,22 @@ func (Pipe) Publish(ctx *context.Context) error {
 	return publishAll(ctx, cli)
 }
 
+func runAll(ctx *context.Context, cli client.Client) error {
+	for _, brew := range ctx.Config.Brews {
+		err := doRun(ctx, brew, cli)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 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.rb` file, which is useful for debugging.
 	skips := pipe.SkipMemento{}
-	for _, brew := range ctx.Config.Brews {
-		err := doRun(ctx, brew, cli)
+	for _, formula := range ctx.Artifacts.Filter(artifact.ByType(artifact.BrewTap)).List() {
+		err := doPublish(ctx, formula, cli)
 		if err != nil && pipe.IsSkip(err) {
 			skips.Remember(err)
 			continue
@@ -77,51 +122,45 @@ func publishAll(ctx *context.Context, cli client.Client) error {
 	return skips.Evaluate()
 }
 
-// Default sets the pipe defaults.
-func (Pipe) Default(ctx *context.Context) error {
-	for i := range ctx.Config.Brews {
-		brew := &ctx.Config.Brews[i]
+func doPublish(ctx *context.Context, formula *artifact.Artifact, cl client.Client) error {
+	brew := formula.Extra[brewConfigExtra].(config.Homebrew)
+	var err error
+	cl, err = client.NewIfToken(ctx, cl, brew.Tap.Token)
+	if err != nil {
+		return err
+	}
 
-		if brew.Install == "" {
-			brew.Install = fmt.Sprintf(`bin.install "%s"`, ctx.Config.ProjectName)
-			log.Warnf("optimistically guessing `brew[%d].install` to be `%s`", i, brew.Install)
-		}
-		if brew.CommitAuthor.Name == "" {
-			brew.CommitAuthor.Name = "goreleaserbot"
-		}
-		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
-		}
-		if brew.Goarm == "" {
-			brew.Goarm = "6"
-		}
+	if strings.TrimSpace(brew.SkipUpload) == "true" {
+		return pipe.Skip("brew.skip_upload is set")
 	}
 
-	return nil
+	if strings.TrimSpace(brew.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
+		return pipe.Skip("prerelease detected with 'auto' upload, skipping homebrew publish")
+	}
+
+	repo := client.RepoFromRef(brew.Tap)
+
+	gpath := buildFormulaPath(brew.Folder, formula.Name)
+	log.WithField("formula", gpath).
+		WithField("repo", repo.String()).
+		Info("pushing")
+
+	msg, err := tmpl.New(ctx).Apply(brew.CommitMessageTemplate)
+	if err != nil {
+		return err
+	}
+
+	content, err := os.ReadFile(formula.Path)
+	if err != nil {
+		return err
+	}
+
+	return cl.CreateFile(ctx, brew.CommitAuthor, repo, content, gpath, msg)
 }
 
 func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
 	if brew.Tap.Name == "" {
-		return pipe.ErrSkipDisabledPipe
-	}
-
-	if brew.Tap.Token != "" {
-		token, err := tmpl.New(ctx).ApplySingleEnvOnly(brew.Tap.Token)
-		if err != nil {
-			return err
-		}
-		log.Debug("using custom token to publish homebrew formula")
-		c, err := client.NewWithToken(ctx, token)
-		if err != nil {
-			return err
-		}
-		cl = c
+		return pipe.Skip("brew tap name is not set")
 	}
 
 	// TODO: properly cover this with tests
@@ -168,28 +207,16 @@ func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
 		return fmt.Errorf("failed to write brew formula: %w", err)
 	}
 
-	if strings.TrimSpace(brew.SkipUpload) == "true" {
-		return pipe.Skip("brew.skip_upload is set")
-	}
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
-	if strings.TrimSpace(brew.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
-		return pipe.Skip("prerelease detected with 'auto' upload, skipping homebrew publish")
-	}
-
-	repo := client.RepoFromRef(brew.Tap)
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name: filename,
+		Path: path,
+		Type: artifact.BrewTap,
+		Extra: map[string]interface{}{
+			brewConfigExtra: brew,
+		},
+	})
 
-	gpath := buildFormulaPath(brew.Folder, filename)
-	log.WithField("formula", gpath).
-		WithField("repo", repo.String()).
-		Info("pushing")
-
-	msg, err := tmpl.New(ctx).Apply(brew.CommitMessageTemplate)
-	if err != nil {
-		return err
-	}
-	return cl.CreateFile(ctx, brew.CommitAuthor, repo, []byte(content), gpath, msg)
+	return nil
 }
 
 func buildFormulaPath(folder, filename string) string {
diff --git a/internal/pipe/brew/testdata/TestRunPipe/custom_block.rb.golden b/internal/pipe/brew/testdata/TestFullPipe/custom_block.rb.golden
similarity index 100%
rename from internal/pipe/brew/testdata/TestRunPipe/custom_block.rb.golden
rename to internal/pipe/brew/testdata/TestFullPipe/custom_block.rb.golden
diff --git a/internal/pipe/brew/testdata/TestRunPipe/custom_download_strategy.rb.golden b/internal/pipe/brew/testdata/TestFullPipe/custom_download_strategy.rb.golden
similarity index 100%
rename from internal/pipe/brew/testdata/TestRunPipe/custom_download_strategy.rb.golden
rename to internal/pipe/brew/testdata/TestFullPipe/custom_download_strategy.rb.golden
diff --git a/internal/pipe/brew/testdata/TestRunPipe/custom_require.rb.golden b/internal/pipe/brew/testdata/TestFullPipe/custom_require.rb.golden
similarity index 100%
rename from internal/pipe/brew/testdata/TestRunPipe/custom_require.rb.golden
rename to internal/pipe/brew/testdata/TestFullPipe/custom_require.rb.golden
diff --git a/internal/pipe/brew/testdata/TestRunPipe/default.rb.golden b/internal/pipe/brew/testdata/TestFullPipe/default.rb.golden
similarity index 100%
rename from internal/pipe/brew/testdata/TestRunPipe/default.rb.golden
rename to internal/pipe/brew/testdata/TestFullPipe/default.rb.golden
diff --git a/internal/pipe/brew/testdata/TestRunPipe/default_gitlab.rb.golden b/internal/pipe/brew/testdata/TestFullPipe/default_gitlab.rb.golden
similarity index 100%
rename from internal/pipe/brew/testdata/TestRunPipe/default_gitlab.rb.golden
rename to internal/pipe/brew/testdata/TestFullPipe/default_gitlab.rb.golden
diff --git a/internal/pipe/build/build.go b/internal/pipe/build/build.go
index 9008acc6..9acfec4d 100644
--- a/internal/pipe/build/build.go
+++ b/internal/pipe/build/build.go
@@ -202,20 +202,14 @@ func buildOptionsForTarget(ctx *context.Context, build config.Build, target stri
 	if build.NoUniqueDistDir {
 		dir = ""
 	}
-	path, err := filepath.Abs(
-		filepath.Join(
-			ctx.Config.Dist,
-			dir,
-			name,
-		),
-	)
+	path, err := filepath.Abs(filepath.Join(ctx.Config.Dist, dir, name))
 	if err != nil {
 		return nil, err
 	}
 	buildOpts.Path = path
+	buildOpts.Name = name
 
 	log.WithField("binary", buildOpts.Path).Info("building")
-	buildOpts.Name = name
 	return &buildOpts, nil
 }
 
diff --git a/internal/pipe/changelog/changelog.go b/internal/pipe/changelog/changelog.go
index 6d5205ff..add01f25 100644
--- a/internal/pipe/changelog/changelog.go
+++ b/internal/pipe/changelog/changelog.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/internal/git"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -23,9 +22,8 @@ var ErrInvalidSortDirection = errors.New("invalid sort direction")
 // Pipe for checksums.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "generating changelog"
-}
+func (Pipe) String() string                 { return "generating changelog" }
+func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Changelog.Skip || ctx.Snapshot }
 
 // Run the pipe.
 func (Pipe) Run(ctx *context.Context) error {
@@ -35,12 +33,6 @@ func (Pipe) Run(ctx *context.Context) error {
 	}
 	ctx.ReleaseNotes = notes
 
-	if ctx.Config.Changelog.Skip {
-		return pipe.ErrSkipDisabledPipe
-	}
-	if ctx.Snapshot {
-		return pipe.Skip("not available for snapshots")
-	}
 	if ctx.ReleaseNotes != "" {
 		return nil
 	}
diff --git a/internal/pipe/checksums/checksums.go b/internal/pipe/checksums/checksums.go
index fc2c7a5f..d160c421 100644
--- a/internal/pipe/checksums/checksums.go
+++ b/internal/pipe/checksums/checksums.go
@@ -12,7 +12,6 @@ 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"
 	"github.com/goreleaser/goreleaser/pkg/context"
@@ -21,9 +20,8 @@ import (
 // Pipe for checksums.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "calculating checksums"
-}
+func (Pipe) String() string                 { return "calculating checksums" }
+func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Checksum.Disable }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -38,9 +36,6 @@ func (Pipe) Default(ctx *context.Context) error {
 
 // Run the pipe.
 func (Pipe) Run(ctx *context.Context) (err error) {
-	if ctx.Config.Checksum.Disable {
-		return pipe.ErrSkipDisabledPipe
-	}
 	filter := artifact.Or(
 		artifact.ByType(artifact.UploadableArchive),
 		artifact.ByType(artifact.UploadableBinary),
diff --git a/internal/pipe/custompublishers/custompublishers.go b/internal/pipe/custompublishers/custompublishers.go
index f962f274..16dae009 100644
--- a/internal/pipe/custompublishers/custompublishers.go
+++ b/internal/pipe/custompublishers/custompublishers.go
@@ -3,7 +3,6 @@ package custompublishers
 
 import (
 	"github.com/goreleaser/goreleaser/internal/exec"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
@@ -11,15 +10,10 @@ import (
 type Pipe struct{}
 
 // String returns the description of the pipe.
-func (Pipe) String() string {
-	return "custom publisher"
-}
+func (Pipe) String() string                 { return "custom publisher" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Publishers) == 0 }
 
 // Publish artifacts.
 func (Pipe) Publish(ctx *context.Context) error {
-	if len(ctx.Config.Publishers) == 0 {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	return exec.Execute(ctx, ctx.Config.Publishers)
 }
diff --git a/internal/pipe/defaults/defaults.go b/internal/pipe/defaults/defaults.go
index 4245c8de..9ab6d60f 100644
--- a/internal/pipe/defaults/defaults.go
+++ b/internal/pipe/defaults/defaults.go
@@ -7,7 +7,9 @@ import (
 	"strings"
 
 	"github.com/goreleaser/goreleaser/internal/client"
-	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/middleware/errhandler"
+	"github.com/goreleaser/goreleaser/internal/middleware/logging"
+	"github.com/goreleaser/goreleaser/internal/middleware/skip"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/goreleaser/goreleaser/pkg/defaults"
@@ -16,9 +18,7 @@ import (
 // Pipe that sets the defaults.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "setting defaults"
-}
+func (Pipe) String() string { return "setting defaults" }
 
 // Run the pipe.
 func (Pipe) Run(ctx *context.Context) error {
@@ -40,10 +40,13 @@ func (Pipe) Run(ctx *context.Context) error {
 		ctx.Config.GiteaURLs.Download = strings.ReplaceAll(apiURL, "/api/v1", "")
 	}
 	for _, defaulter := range defaults.Defaulters {
-		if err := middleware.Logging(
-			defaulter.String(),
-			middleware.ErrHandler(defaulter.Default),
-			middleware.ExtraPadding,
+		if err := skip.Maybe(
+			defaulter,
+			logging.Log(
+				defaulter.String(),
+				errhandler.Handle(defaulter.Default),
+				logging.ExtraPadding,
+			),
 		)(ctx); err != nil {
 			return err
 		}
diff --git a/internal/pipe/discord/discord.go b/internal/pipe/discord/discord.go
index daf9f634..5d2e1884 100644
--- a/internal/pipe/discord/discord.go
+++ b/internal/pipe/discord/discord.go
@@ -8,7 +8,6 @@ import (
 	"github.com/DisgoOrg/disgohook/api"
 	"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"
 )
@@ -22,7 +21,8 @@ const (
 
 type Pipe struct{}
 
-func (Pipe) String() string { return "discord" }
+func (Pipe) String() string                 { return "discord" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.Discord.Enabled }
 
 type Config struct {
 	WebhookID    string `env:"DISCORD_WEBHOOK_ID,notEmpty"`
@@ -46,13 +46,6 @@ func (p Pipe) Default(ctx *context.Context) error {
 }
 
 func (p Pipe) Announce(ctx *context.Context) error {
-	if ctx.SkipAnnounce {
-		return pipe.ErrSkipAnnounceEnabled
-	}
-	if !ctx.Config.Announce.Discord.Enabled {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	msg, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Discord.MessageTemplate)
 	if err != nil {
 		return fmt.Errorf("announce: failed to announce to discord: %w", err)
diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go
index 14566bae..00851646 100644
--- a/internal/pipe/docker/docker.go
+++ b/internal/pipe/docker/docker.go
@@ -1,7 +1,6 @@
 package docker
 
 import (
-	"errors"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -32,9 +31,8 @@ const (
 // Pipe for docker.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "docker images"
-}
+func (Pipe) String() string                 { return "docker images" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Dockers) == 0 }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -89,19 +87,8 @@ func validateImager(use string) error {
 	return fmt.Errorf("docker: invalid use: %s, valid options are %v", use, valid)
 }
 
-// Run the pipe.
-func (Pipe) Run(ctx *context.Context) error {
-	if len(ctx.Config.Dockers) == 0 || len(ctx.Config.Dockers[0].ImageTemplates) == 0 {
-		return pipe.ErrSkipDisabledPipe
-	}
-	return doRun(ctx)
-}
-
 // Publish the docker images.
 func (Pipe) Publish(ctx *context.Context) error {
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
 	images := ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableDockerImage)).List()
 	for _, image := range images {
 		if err := dockerPush(ctx, image); err != nil {
@@ -111,7 +98,8 @@ func (Pipe) Publish(ctx *context.Context) error {
 	return nil
 }
 
-func doRun(ctx *context.Context) error {
+// Run the pipe.
+func (Pipe) Run(ctx *context.Context) error {
 	g := semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism))
 	for _, docker := range ctx.Config.Dockers {
 		docker := docker
@@ -148,6 +136,10 @@ func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.A
 		return err
 	}
 
+	if len(images) == 0 {
+		return pipe.Skip("no image templates found")
+	}
+
 	log := log.WithField("image", images[0])
 	log.Debug("tempdir: " + tmp)
 
@@ -220,10 +212,6 @@ func processImageTemplates(ctx *context.Context, docker config.Docker) ([]string
 		images = append(images, image)
 	}
 
-	if len(images) == 0 {
-		return images, errors.New("no image templates found")
-	}
-
 	return images, nil
 }
 
diff --git a/internal/pipe/docker/manifest.go b/internal/pipe/docker/manifest.go
index 40af5697..c465cee4 100644
--- a/internal/pipe/docker/manifest.go
+++ b/internal/pipe/docker/manifest.go
@@ -19,9 +19,8 @@ import (
 // allowing to publish multi-arch docker images.
 type ManifestPipe struct{}
 
-func (ManifestPipe) String() string {
-	return "docker manifests"
-}
+func (ManifestPipe) String() string                 { return "docker manifests" }
+func (ManifestPipe) Skip(ctx *context.Context) bool { return len(ctx.Config.DockerManifests) == 0 }
 
 // Default sets the pipe defaults.
 func (ManifestPipe) Default(ctx *context.Context) error {
@@ -43,9 +42,6 @@ func (ManifestPipe) Default(ctx *context.Context) error {
 
 // Publish the docker manifests.
 func (ManifestPipe) Publish(ctx *context.Context) error {
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
 	g := semerrgroup.NewSkipAware(semerrgroup.New(1))
 	for _, manifest := range ctx.Config.DockerManifests {
 		manifest := manifest
diff --git a/internal/pipe/gomod/gomod.go b/internal/pipe/gomod/gomod.go
index e56e4194..ca3878c5 100644
--- a/internal/pipe/gomod/gomod.go
+++ b/internal/pipe/gomod/gomod.go
@@ -27,9 +27,7 @@ const (
 // Pipe for env.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "loading go mod information"
-}
+func (Pipe) String() string { return "loading go mod information" }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -53,7 +51,8 @@ func (Pipe) Run(ctx *context.Context) error {
 	ctx.ModulePath = result
 
 	if !ctx.Config.GoMod.Proxy {
-		return pipe.ErrSkipDisabledPipe
+		// TODO: check this on Skip?
+		return pipe.Skip("proxying not enabled")
 	}
 
 	if ctx.Snapshot {
diff --git a/internal/pipe/milestone/doc.go b/internal/pipe/milestone/doc.go
deleted file mode 100644
index f11f9f69..00000000
--- a/internal/pipe/milestone/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package milestone implements Pipe and manages VCS milestones.
-package milestone
diff --git a/internal/pipe/milestone/milestone.go b/internal/pipe/milestone/milestone.go
index 6acf17bb..b2233761 100644
--- a/internal/pipe/milestone/milestone.go
+++ b/internal/pipe/milestone/milestone.go
@@ -1,3 +1,4 @@
+// Package milestone implements Pipe and manages VCS milestones.
 package milestone
 
 import (
@@ -6,7 +7,6 @@ import (
 	"github.com/goreleaser/goreleaser/internal/git"
 	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
-	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
@@ -15,16 +15,11 @@ const defaultNameTemplate = "{{ .Tag }}"
 // Pipe for milestone.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "milestones"
-}
+func (Pipe) String() string                 { return "milestones" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Milestones) == 0 }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
-	if len(ctx.Config.Milestones) == 0 {
-		ctx.Config.Milestones = append(ctx.Config.Milestones, config.Milestone{})
-	}
-
 	for i := range ctx.Config.Milestones {
 		milestone := &ctx.Config.Milestones[i]
 
@@ -48,9 +43,6 @@ func (Pipe) Default(ctx *context.Context) error {
 
 // Publish the release.
 func (Pipe) Publish(ctx *context.Context) error {
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
 	c, err := client.New(ctx)
 	if err != nil {
 		return err
@@ -63,7 +55,7 @@ func doPublish(ctx *context.Context, vcsClient client.Client) error {
 		milestone := &ctx.Config.Milestones[i]
 
 		if !milestone.Close {
-			return pipe.ErrSkipDisabledPipe
+			return pipe.Skip("closing not enabled")
 		}
 
 		name, err := tmpl.New(ctx).Apply(milestone.NameTemplate)
diff --git a/internal/pipe/nfpm/nfpm.go b/internal/pipe/nfpm/nfpm.go
index f940395a..290ccc39 100644
--- a/internal/pipe/nfpm/nfpm.go
+++ b/internal/pipe/nfpm/nfpm.go
@@ -30,9 +30,8 @@ const defaultNameTemplate = "{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arc
 // Pipe for nfpm packaging.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "linux packages"
-}
+func (Pipe) String() string                 { return "linux packages" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.NFPMs) == 0 }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
diff --git a/internal/pipe/pipe.go b/internal/pipe/pipe.go
index 4001a0e2..77a735b4 100644
--- a/internal/pipe/pipe.go
+++ b/internal/pipe/pipe.go
@@ -25,31 +25,14 @@ var ErrSkipSignEnabled = Skip("artifact signing is disabled")
 // It means that the part of a Piper that validates some things was not run.
 var ErrSkipValidateEnabled = Skip("validation is disabled")
 
-// ErrSkipDisabledPipe happens when a pipe is skipped because it is not configured.
-var ErrSkipDisabledPipe = ErrSkip{
-	reason:   "pipe not configured/disabled",
-	expected: true,
-}
-
 // IsSkip returns true if the error is an ErrSkip.
 func IsSkip(err error) bool {
 	return errors.As(err, &ErrSkip{})
 }
 
-// IsExpectedSkip returns true if the given error is ErrSkip and if it is an
-// expected skip.
-func IsExpectedSkip(err error) bool {
-	skipErr := ErrSkip{}
-	if !errors.As(err, &skipErr) {
-		return false
-	}
-	return skipErr.expected
-}
-
 // ErrSkip occurs when a pipe is skipped for some reason.
 type ErrSkip struct {
-	reason   string
-	expected bool
+	reason string
 }
 
 // Error implements the error interface. returns the reason the pipe was skipped.
diff --git a/internal/pipe/publish/publish.go b/internal/pipe/publish/publish.go
index f2c36243..75b38a90 100644
--- a/internal/pipe/publish/publish.go
+++ b/internal/pipe/publish/publish.go
@@ -4,7 +4,9 @@ package publish
 import (
 	"fmt"
 
-	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/middleware/errhandler"
+	"github.com/goreleaser/goreleaser/internal/middleware/logging"
+	"github.com/goreleaser/goreleaser/internal/middleware/skip"
 	"github.com/goreleaser/goreleaser/internal/pipe/artifactory"
 	"github.com/goreleaser/goreleaser/internal/pipe/blob"
 	"github.com/goreleaser/goreleaser/internal/pipe/brew"
@@ -19,13 +21,6 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
-// Pipe that publishes artifacts.
-type Pipe struct{}
-
-func (Pipe) String() string {
-	return "publishing"
-}
-
 // Publisher should be implemented by pipes that want to publish artifacts.
 type Publisher interface {
 	fmt.Stringer
@@ -52,13 +47,21 @@ var publishers = []Publisher{
 	milestone.Pipe{},
 }
 
-// Run the pipe.
+// Pipe that publishes artifacts.
+type Pipe struct{}
+
+func (Pipe) String() string                 { return "publishing" }
+func (Pipe) Skip(ctx *context.Context) bool { return ctx.SkipPublish }
+
 func (Pipe) Run(ctx *context.Context) error {
 	for _, publisher := range publishers {
-		if err := middleware.Logging(
-			publisher.String(),
-			middleware.ErrHandler(publisher.Publish),
-			middleware.ExtraPadding,
+		if err := skip.Maybe(
+			publisher,
+			logging.Log(
+				publisher.String(),
+				errhandler.Handle(publisher.Publish),
+				logging.ExtraPadding,
+			),
 		)(ctx); err != nil {
 			return fmt.Errorf("%s: failed to publish artifacts: %w", publisher.String(), err)
 		}
diff --git a/internal/pipe/reddit/reddit.go b/internal/pipe/reddit/reddit.go
index 96c996df..8d95d392 100644
--- a/internal/pipe/reddit/reddit.go
+++ b/internal/pipe/reddit/reddit.go
@@ -5,7 +5,6 @@ import (
 
 	"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"
@@ -18,7 +17,8 @@ const (
 
 type Pipe struct{}
 
-func (Pipe) String() string { return "reddit" }
+func (Pipe) String() string                 { return "reddit" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.Reddit.Enabled }
 
 type Config struct {
 	Secret   string `env:"REDDIT_SECRET,notEmpty"`
@@ -38,13 +38,6 @@ func (Pipe) Default(ctx *context.Context) error {
 }
 
 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)
diff --git a/internal/pipe/release/release.go b/internal/pipe/release/release.go
index 63eab65b..d915c007 100644
--- a/internal/pipe/release/release.go
+++ b/internal/pipe/release/release.go
@@ -11,7 +11,6 @@ import (
 	"github.com/goreleaser/goreleaser/internal/client"
 	"github.com/goreleaser/goreleaser/internal/extrafiles"
 	"github.com/goreleaser/goreleaser/internal/git"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/semerrgroup"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -23,9 +22,8 @@ var ErrMultipleReleases = errors.New("multiple releases are defined. Only one is
 // Pipe for github release.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "github/gitlab/gitea releases"
-}
+func (Pipe) String() string                 { return "github/gitlab/gitea releases" }
+func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Release.Disable }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -101,9 +99,6 @@ func (Pipe) Default(ctx *context.Context) error {
 
 // Publish the release.
 func (Pipe) Publish(ctx *context.Context) error {
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
 	c, err := client.New(ctx)
 	if err != nil {
 		return err
@@ -112,9 +107,6 @@ func (Pipe) Publish(ctx *context.Context) error {
 }
 
 func doPublish(ctx *context.Context, client client.Client) error {
-	if ctx.Config.Release.Disable {
-		return pipe.ErrSkipDisabledPipe
-	}
 	log.WithField("tag", ctx.Git.CurrentTag).
 		WithField("repo", ctx.Config.Release.GitHub.String()).
 		Info("creating or updating release")
diff --git a/internal/pipe/scoop/scoop.go b/internal/pipe/scoop/scoop.go
index 31ce07c6..a637c38e 100644
--- a/internal/pipe/scoop/scoop.go
+++ b/internal/pipe/scoop/scoop.go
@@ -16,6 +16,7 @@ import (
 	"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"
 )
 
@@ -25,11 +26,21 @@ var ErrNoWindows = errors.New("scoop requires a windows build")
 // ErrTokenTypeNotImplementedForScoop indicates that a new token type was not implemented for this pipe.
 var ErrTokenTypeNotImplementedForScoop = errors.New("token type not implemented for scoop pipe")
 
-// Pipe for build.
+const scoopConfigExtra = "ScoopConfig"
+
+// Pipe that builds and publishes scoop manifests.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "scoop manifests"
+func (Pipe) String() string                 { return "scoop manifests" }
+func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Scoop.Bucket.Name == "" }
+
+// Run creates the scoop manifest locally.
+func (Pipe) Run(ctx *context.Context) error {
+	client, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+	return doRun(ctx, client)
 }
 
 // Publish scoop manifest.
@@ -38,7 +49,7 @@ func (Pipe) Publish(ctx *context.Context) error {
 	if err != nil {
 		return err
 	}
-	return doRun(ctx, client)
+	return doPublish(ctx, client)
 }
 
 // Default sets the pipe defaults.
@@ -60,22 +71,6 @@ func (Pipe) Default(ctx *context.Context) error {
 
 func doRun(ctx *context.Context, cl client.Client) error {
 	scoop := ctx.Config.Scoop
-	if scoop.Bucket.Name == "" {
-		return pipe.ErrSkipDisabledPipe
-	}
-
-	if scoop.Bucket.Token != "" {
-		token, err := tmpl.New(ctx).ApplySingleEnvOnly(scoop.Bucket.Token)
-		if err != nil {
-			return err
-		}
-		log.Debug("using custom token to publish scoop manifest")
-		c, err := client.NewWithToken(ctx, token)
-		if err != nil {
-			return err
-		}
-		cl = c
-	}
 
 	// TODO: multiple archives
 	if ctx.Config.Archives[0].Format == "binary" {
@@ -103,18 +98,41 @@ func doRun(ctx *context.Context, cl client.Client) error {
 		return err
 	}
 
-	distPath := filepath.Join(ctx.Config.Dist, filename)
-	log.WithField("manifest", distPath).Info("writing")
-	if err := os.WriteFile(distPath, content.Bytes(), 0o644); err != nil {
+	path := filepath.Join(ctx.Config.Dist, filename)
+	log.WithField("manifest", path).Info("writing")
+	if err := os.WriteFile(path, content.Bytes(), 0o644); err != nil {
 		return fmt.Errorf("failed to write scoop manifest: %w", err)
 	}
 
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name: filename,
+		Path: path,
+		Type: artifact.ScoopManifest,
+		Extra: map[string]interface{}{
+			scoopConfigExtra: scoop,
+		},
+	})
+	return nil
+}
+
+func doPublish(ctx *context.Context, cl client.Client) error {
+	manifests := ctx.Artifacts.Filter(artifact.ByType(artifact.ScoopManifest)).List()
+	if len(manifests) == 0 { // should never happen
+		return nil
+	}
+
+	manifest := manifests[0]
+	scoop := manifest.Extra[scoopConfigExtra].(config.Scoop)
+
+	var err error
+	cl, err = client.NewIfToken(ctx, cl, scoop.Bucket.Token)
+	if err != nil {
+		return err
+	}
+
 	if strings.TrimSpace(scoop.SkipUpload) == "true" {
 		return pipe.Skip("scoop.skip_upload is true")
 	}
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
 	if strings.TrimSpace(scoop.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
 		return pipe.Skip("release is prerelease")
 	}
@@ -130,13 +148,18 @@ func doRun(ctx *context.Context, cl client.Client) error {
 		return err
 	}
 
+	content, err := os.ReadFile(manifest.Path)
+	if err != nil {
+		return err
+	}
+
 	repo := client.RepoFromRef(scoop.Bucket)
 	return cl.CreateFile(
 		ctx,
 		scoop.CommitAuthor,
 		repo,
-		content.Bytes(),
-		path.Join(scoop.Folder, filename),
+		content,
+		path.Join(scoop.Folder, manifest.Name),
 		commitMessage,
 	)
 }
diff --git a/internal/pipe/sign/sign.go b/internal/pipe/sign/sign.go
index d5a58129..5848a483 100644
--- a/internal/pipe/sign/sign.go
+++ b/internal/pipe/sign/sign.go
@@ -24,9 +24,8 @@ import (
 // Pipe that signs common artifacts.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "signing artifacts"
-}
+func (Pipe) String() string                 { return "signing artifacts" }
+func (Pipe) Skip(ctx *context.Context) bool { return ctx.SkipSign || len(ctx.Config.Signs) == 0 }
 
 // Default sets the Pipes defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -55,10 +54,6 @@ func (Pipe) Default(ctx *context.Context) error {
 
 // Run executes the Pipe.
 func (Pipe) Run(ctx *context.Context) error {
-	if ctx.SkipSign {
-		return pipe.ErrSkipSignEnabled
-	}
-
 	g := semerrgroup.New(ctx.Parallelism)
 	for i := range ctx.Config.Signs {
 		cfg := ctx.Config.Signs[i]
@@ -89,7 +84,7 @@ func (Pipe) Run(ctx *context.Context) error {
 				filters = append(filters, artifact.ByType(artifact.UploadableBinary))
 			case "package":
 				filters = append(filters, artifact.ByType(artifact.LinuxPackage))
-			case "none":
+			case "none": // TODO(caarlos0): this is not very useful, lets remove it.
 				return pipe.ErrSkipSignEnabled
 			default:
 				return fmt.Errorf("invalid list of artifacts to sign: %s", cfg.Artifacts)
diff --git a/internal/pipe/sign/sign_docker.go b/internal/pipe/sign/sign_docker.go
index 5a629c78..48b8a767 100644
--- a/internal/pipe/sign/sign_docker.go
+++ b/internal/pipe/sign/sign_docker.go
@@ -13,8 +13,10 @@ import (
 // Pipe that signs docker images and manifests.
 type DockerPipe struct{}
 
-func (DockerPipe) String() string {
-	return "signing docker images"
+func (DockerPipe) String() string { return "signing docker images" }
+
+func (DockerPipe) Skip(ctx *context.Context) bool {
+	return ctx.SkipSign || len(ctx.Config.DockerSigns) == 0
 }
 
 // Default sets the Pipes defaults.
@@ -41,14 +43,6 @@ func (DockerPipe) Default(ctx *context.Context) error {
 
 // Publish signs and pushes the docker images signatures.
 func (DockerPipe) Publish(ctx *context.Context) error {
-	if ctx.SkipSign {
-		return pipe.ErrSkipSignEnabled
-	}
-
-	if ctx.SkipPublish {
-		return pipe.ErrSkipSignEnabled
-	}
-
 	g := semerrgroup.New(ctx.Parallelism)
 	for i := range ctx.Config.DockerSigns {
 		cfg := ctx.Config.DockerSigns[i]
@@ -64,7 +58,7 @@ func (DockerPipe) Publish(ctx *context.Context) error {
 					artifact.ByType(artifact.DockerImage),
 					artifact.ByType(artifact.DockerManifest),
 				))
-			case "none":
+			case "none": // TODO(caarlos0): remove this
 				return pipe.ErrSkipSignEnabled
 			default:
 				return fmt.Errorf("invalid list of artifacts to sign: %s", cfg.Artifacts)
diff --git a/internal/pipe/slack/slack.go b/internal/pipe/slack/slack.go
index 0653cbf5..4cf9e397 100644
--- a/internal/pipe/slack/slack.go
+++ b/internal/pipe/slack/slack.go
@@ -5,7 +5,6 @@ import (
 
 	"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"
@@ -18,7 +17,8 @@ const (
 
 type Pipe struct{}
 
-func (Pipe) String() string { return "slack" }
+func (Pipe) String() string                 { return "slack" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.Slack.Enabled }
 
 type Config struct {
 	Webhook string `env:"SLACK_WEBHOOK,notEmpty"`
@@ -35,13 +35,6 @@ func (Pipe) Default(ctx *context.Context) error {
 }
 
 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)
diff --git a/internal/pipe/smtp/smtp.go b/internal/pipe/smtp/smtp.go
index 69d74b39..e31f378a 100644
--- a/internal/pipe/smtp/smtp.go
+++ b/internal/pipe/smtp/smtp.go
@@ -6,7 +6,6 @@ import (
 
 	"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"
 	gomail "gopkg.in/mail.v2"
@@ -19,7 +18,8 @@ const (
 
 type Pipe struct{}
 
-func (Pipe) String() string { return "reddit" }
+func (Pipe) String() string                 { return "smtp" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.SMTP.Enabled }
 
 type Config struct {
 	Host     string `env:"SMTP_HOST,notEmpty"`
@@ -41,13 +41,6 @@ func (Pipe) Default(ctx *context.Context) error {
 }
 
 func (Pipe) Announce(ctx *context.Context) error {
-	if ctx.SkipAnnounce {
-		return pipe.ErrSkipAnnounceEnabled
-	}
-	if !ctx.Config.Announce.SMTP.Enabled {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	subject, err := tmpl.New(ctx).Apply(ctx.Config.Announce.SMTP.SubjectTemplate)
 	if err != nil {
 		return fmt.Errorf("announce: failed to announce to SMTP: %w", err)
diff --git a/internal/pipe/snapcraft/snapcraft.go b/internal/pipe/snapcraft/snapcraft.go
index f15952a3..4316dcb1 100644
--- a/internal/pipe/snapcraft/snapcraft.go
+++ b/internal/pipe/snapcraft/snapcraft.go
@@ -71,9 +71,8 @@ const defaultNameTemplate = "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arc
 // Pipe for snapcraft packaging.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "snapcraft packages"
-}
+func (Pipe) String() string                 { return "snapcraft packages" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Snapcrafts) == 0 }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
diff --git a/internal/pipe/snapshot/snapshot.go b/internal/pipe/snapshot/snapshot.go
index ee525ad8..10787c96 100644
--- a/internal/pipe/snapshot/snapshot.go
+++ b/internal/pipe/snapshot/snapshot.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 
 	"github.com/apex/log"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -13,9 +12,8 @@ import (
 // Pipe for checksums.
 type Pipe struct{}
 
-func (Pipe) String() string {
-	return "snapshotting"
-}
+func (Pipe) String() string                 { return "snapshotting" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Snapshot }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -26,9 +24,6 @@ func (Pipe) Default(ctx *context.Context) error {
 }
 
 func (Pipe) Run(ctx *context.Context) error {
-	if !ctx.Snapshot {
-		return pipe.ErrSkipDisabledPipe
-	}
 	name, err := tmpl.New(ctx).Apply(ctx.Config.Snapshot.NameTemplate)
 	if err != nil {
 		return fmt.Errorf("failed to generate snapshot name: %w", err)
diff --git a/internal/pipe/sourcearchive/source.go b/internal/pipe/sourcearchive/source.go
index 99fca210..ad0ee288 100644
--- a/internal/pipe/sourcearchive/source.go
+++ b/internal/pipe/sourcearchive/source.go
@@ -7,7 +7,6 @@ import (
 	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/git"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -19,12 +18,12 @@ func (Pipe) String() string {
 	return "creating source archive"
 }
 
+func (Pipe) Skip(ctx *context.Context) bool {
+	return !ctx.Config.Source.Enabled
+}
+
 // Run the pipe.
 func (Pipe) Run(ctx *context.Context) (err error) {
-	if !ctx.Config.Source.Enabled {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	name, err := tmpl.New(ctx).Apply(ctx.Config.Source.NameTemplate)
 	if err != nil {
 		return err
diff --git a/internal/pipe/teams/teams.go b/internal/pipe/teams/teams.go
index a448ce2e..039adb53 100644
--- a/internal/pipe/teams/teams.go
+++ b/internal/pipe/teams/teams.go
@@ -6,7 +6,6 @@ import (
 	"github.com/apex/log"
 	goteamsnotify "github.com/atc0005/go-teams-notify/v2"
 	"github.com/caarlos0/env/v6"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -20,7 +19,8 @@ const (
 
 type Pipe struct{}
 
-func (Pipe) String() string { return "teams" }
+func (Pipe) String() string                 { return "teams" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.Teams.Enabled }
 
 type Config struct {
 	Webhook string `env:"TEAMS_WEBHOOK,notEmpty"`
@@ -43,13 +43,6 @@ func (p Pipe) Default(ctx *context.Context) error {
 }
 
 func (p Pipe) Announce(ctx *context.Context) error {
-	if ctx.SkipAnnounce {
-		return pipe.ErrSkipAnnounceEnabled
-	}
-	if !ctx.Config.Announce.Teams.Enabled {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	title, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Teams.TitleTemplate)
 	if err != nil {
 		return fmt.Errorf("announce: failed to announce to teams: %w", err)
diff --git a/internal/pipe/twitter/twitter.go b/internal/pipe/twitter/twitter.go
index 523dcf29..4528c6fd 100644
--- a/internal/pipe/twitter/twitter.go
+++ b/internal/pipe/twitter/twitter.go
@@ -7,7 +7,6 @@ import (
 	"github.com/caarlos0/env/v6"
 	"github.com/dghubble/go-twitter/twitter"
 	"github.com/dghubble/oauth1"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -16,7 +15,8 @@ const defaultMessageTemplate = `{{ .ProjectName }} {{ .Tag }} is out! Check it o
 
 type Pipe struct{}
 
-func (Pipe) String() string { return "twitter" }
+func (Pipe) String() string                 { return "twitter" }
+func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.Twitter.Enabled }
 
 type Config struct {
 	ConsumerKey    string `env:"TWITTER_CONSUMER_KEY,notEmpty"`
@@ -33,13 +33,6 @@ func (Pipe) Default(ctx *context.Context) error {
 }
 
 func (Pipe) Announce(ctx *context.Context) error {
-	if ctx.SkipAnnounce {
-		return pipe.ErrSkipAnnounceEnabled
-	}
-	if !ctx.Config.Announce.Twitter.Enabled {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	msg, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Twitter.MessageTemplate)
 	if err != nil {
 		return fmt.Errorf("announce: failed to announce to twitter: %w", err)
diff --git a/internal/pipe/upload/upload.go b/internal/pipe/upload/upload.go
index fe457224..f0ea0671 100644
--- a/internal/pipe/upload/upload.go
+++ b/internal/pipe/upload/upload.go
@@ -14,9 +14,8 @@ import (
 type Pipe struct{}
 
 // String returns the description of the pipe.
-func (Pipe) String() string {
-	return "http upload"
-}
+func (Pipe) String() string                 { return "http upload" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Uploads) == 0 }
 
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
@@ -25,10 +24,6 @@ func (Pipe) Default(ctx *context.Context) error {
 
 // Publish artifacts.
 func (Pipe) Publish(ctx *context.Context) error {
-	if len(ctx.Config.Uploads) == 0 {
-		return pipe.ErrSkipDisabledPipe
-	}
-
 	// Check requirements for every instance we have configured.
 	// If not fulfilled, we can skip this pipeline
 	for _, instance := range ctx.Config.Uploads {
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index 66413eb7..f9021c55 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -7,6 +7,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/announce"
 	"github.com/goreleaser/goreleaser/internal/pipe/archive"
 	"github.com/goreleaser/goreleaser/internal/pipe/before"
+	"github.com/goreleaser/goreleaser/internal/pipe/brew"
 	"github.com/goreleaser/goreleaser/internal/pipe/build"
 	"github.com/goreleaser/goreleaser/internal/pipe/changelog"
 	"github.com/goreleaser/goreleaser/internal/pipe/checksums"
@@ -19,6 +20,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/gomod"
 	"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
 	"github.com/goreleaser/goreleaser/internal/pipe/publish"
+	"github.com/goreleaser/goreleaser/internal/pipe/scoop"
 	"github.com/goreleaser/goreleaser/internal/pipe/semver"
 	"github.com/goreleaser/goreleaser/internal/pipe/sign"
 	"github.com/goreleaser/goreleaser/internal/pipe/snapcraft"
@@ -59,6 +61,8 @@ var Pipeline = append(
 	sourcearchive.Pipe{}, // archive the source code using git-archive
 	nfpm.Pipe{},          // archive via fpm (deb, rpm) using "native" go impl
 	snapcraft.Pipe{},     // archive via snapcraft (snap)
+	brew.Pipe{},          // create brew tap
+	scoop.Pipe{},         // create scoop buckets
 	checksums.Pipe{},     // checksums of the files
 	sign.Pipe{},          // sign artifacts
 	docker.Pipe{},        // create and push docker images
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 058ee7f4..f2e3be7d 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -578,7 +578,7 @@ type Filters struct {
 type Changelog struct {
 	Filters Filters `yaml:",omitempty"`
 	Sort    string  `yaml:",omitempty"`
-	Skip    bool    `yaml:",omitempty"`
+	Skip    bool    `yaml:",omitempty"` // TODO(caarlos0): rename to Disable to match other pipes
 }
 
 // EnvFiles holds paths to files that contains environment variables


Test patch:
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
index 43a7b181..6d226226 100644
--- a/internal/artifact/artifact_test.go
+++ b/internal/artifact/artifact_test.go
@@ -306,6 +306,8 @@ func TestTypeToString(t *testing.T) {
 		Checksum,
 		Signature,
 		UploadableSourceArchive,
+		BrewTap,
+		ScoopManifest,
 	} {
 		t.Run(a.String(), func(t *testing.T) {
 			require.NotEqual(t, "unknown", a.String())
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index 1aa7356e..33448f90 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -58,3 +58,102 @@ func TestClientNewGitLab(t *testing.T) {
 	_, ok := client.(*gitlabClient)
 	require.True(t, ok)
 }
+
+func TestNewIfToken(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitLab,
+			Token:     "gitlabtoken",
+		}
+
+		client, err := New(ctx)
+		require.NoError(t, err)
+		_, ok := client.(*gitlabClient)
+		require.True(t, ok)
+
+		ctx = &context.Context{
+			Config: config.Project{
+				GiteaURLs: config.GiteaURLs{
+					API: "https://gitea.com/api/v1",
+				},
+			},
+			TokenType: context.TokenTypeGitea,
+			Token:     "giteatoken",
+			Env:       map[string]string{"VAR": "token"},
+		}
+
+		client, err = NewIfToken(ctx, client, "{{ .Env.VAR }}")
+		require.NoError(t, err)
+		_, ok = client.(*giteaClient)
+		require.True(t, ok)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitLab,
+			Token:     "gitlabtoken",
+		}
+
+		client, err := New(ctx)
+		require.NoError(t, err)
+
+		client, err = NewIfToken(ctx, client, "")
+		require.NoError(t, err)
+		_, ok := client.(*gitlabClient)
+		require.True(t, ok)
+	})
+
+	t.Run("invalid tmpl", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitLab,
+			Token:     "gitlabtoken",
+		}
+
+		_, err := NewIfToken(ctx, nil, "nope")
+		require.EqualError(t, err, `expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)`)
+	})
+}
+
+func TestNewWithToken(t *testing.T) {
+	t.Run("gitlab", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitLab,
+			Env:       map[string]string{"TK": "token"},
+		}
+
+		cli, err := newWithToken(ctx, "{{ .Env.TK }}")
+		require.NoError(t, err)
+
+		_, ok := cli.(*gitlabClient)
+		require.True(t, ok)
+	})
+
+	t.Run("gitea", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitea,
+			Env:       map[string]string{"TK": "token"},
+			Config: config.Project{
+				GiteaURLs: config.GiteaURLs{
+					API: "https://gitea.com/api/v1",
+				},
+			},
+		}
+
+		cli, err := newWithToken(ctx, "{{ .Env.TK }}")
+		require.NoError(t, err)
+
+		_, ok := cli.(*giteaClient)
+		require.True(t, ok)
+	})
+
+	t.Run("invalid", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenType("nope"),
+			Env:       map[string]string{"TK": "token"},
+		}
+
+		cli, err := newWithToken(ctx, "{{ .Env.TK }}")
+		require.NoError(t, err)
+		require.Nil(t, cli)
+	})
+}
diff --git a/internal/http/http_test.go b/internal/http/http_test.go
index aef15679..42e322df 100644
--- a/internal/http/http_test.go
+++ b/internal/http/http_test.go
@@ -485,15 +485,6 @@ func TestUpload(t *testing.T) {
 			},
 			checks(),
 		},
-		{
-			"skip-publishing", true, true, true, true,
-			func(s *httptest.Server) (*context.Context, config.Upload) {
-				c := *ctx
-				c.SkipPublish = true
-				return &c, config.Upload{}
-			},
-			checks(),
-		},
 		{
 			"checksumheader", true, true, false, false,
 			func(s *httptest.Server) (*context.Context, config.Upload) {
diff --git a/internal/middleware/errhandler/error_test.go b/internal/middleware/errhandler/error_test.go
new file mode 100644
index 00000000..0619ba72
--- /dev/null
+++ b/internal/middleware/errhandler/error_test.go
@@ -0,0 +1,30 @@
+package errhandler
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestError(t *testing.T) {
+	t.Run("no errors", func(t *testing.T) {
+		require.NoError(t, Handle(func(ctx *context.Context) error {
+			return nil
+		})(nil))
+	})
+
+	t.Run("pipe skipped", func(t *testing.T) {
+		require.NoError(t, Handle(func(ctx *context.Context) error {
+			return pipe.ErrSkipValidateEnabled
+		})(nil))
+	})
+
+	t.Run("some err", func(t *testing.T) {
+		require.Error(t, Handle(func(ctx *context.Context) error {
+			return fmt.Errorf("pipe errored")
+		})(nil))
+	})
+}
diff --git a/internal/middleware/error_test.go b/internal/middleware/error_test.go
deleted file mode 100644
index dbbc7d4c..00000000
--- a/internal/middleware/error_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package middleware
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/goreleaser/goreleaser/internal/pipe"
-	"github.com/stretchr/testify/require"
-)
-
-func TestError(t *testing.T) {
-	t.Run("no errors", func(t *testing.T) {
-		require.NoError(t, ErrHandler(mockAction(nil))(ctx))
-	})
-
-	t.Run("pipe skipped", func(t *testing.T) {
-		require.NoError(t, ErrHandler(mockAction(pipe.ErrSkipValidateEnabled))(ctx))
-	})
-
-	t.Run("pipe expected skipped", func(t *testing.T) {
-		require.NoError(t, ErrHandler(mockAction(pipe.ErrSkipDisabledPipe))(ctx))
-	})
-
-	t.Run("some err", func(t *testing.T) {
-		require.Error(t, ErrHandler(mockAction(fmt.Errorf("pipe errored")))(ctx))
-	})
-}
diff --git a/internal/middleware/logging/logging_test.go b/internal/middleware/logging/logging_test.go
new file mode 100644
index 00000000..cc691107
--- /dev/null
+++ b/internal/middleware/logging/logging_test.go
@@ -0,0 +1,14 @@
+package logging
+
+import (
+	"testing"
+
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestLogging(t *testing.T) {
+	require.NoError(t, Log("foo", func(ctx *context.Context) error {
+		return nil
+	}, DefaultInitialPadding)(nil))
+}
diff --git a/internal/middleware/logging_test.go b/internal/middleware/logging_test.go
deleted file mode 100644
index 8d9dfb51..00000000
--- a/internal/middleware/logging_test.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package middleware
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestLogging(t *testing.T) {
-	require.NoError(t, Logging("foo", mockAction(nil), DefaultInitialPadding)(ctx))
-}
diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go
deleted file mode 100644
index a47e763a..00000000
--- a/internal/middleware/middleware_test.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package middleware
-
-import "github.com/goreleaser/goreleaser/pkg/context"
-
-var ctx = &context.Context{}
-
-func mockAction(err error) Action {
-	return func(ctx *context.Context) error {
-		return err
-	}
-}
diff --git a/internal/middleware/skip/skip_test.go b/internal/middleware/skip/skip_test.go
new file mode 100644
index 00000000..9352eaaa
--- /dev/null
+++ b/internal/middleware/skip/skip_test.go
@@ -0,0 +1,36 @@
+package skip
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSkip(t *testing.T) {
+	fakeErr := fmt.Errorf("fake error")
+	action := func(_ *context.Context) error {
+		return fakeErr
+	}
+
+	t.Run("not a skipper", func(t *testing.T) {
+		require.EqualError(t, Maybe(action, action)(nil), fakeErr.Error())
+	})
+
+	t.Run("skip", func(t *testing.T) {
+		require.NoError(t, Maybe(skipper{true}, action)(nil))
+	})
+
+	t.Run("do not skip", func(t *testing.T) {
+		require.EqualError(t, Maybe(skipper{false}, action)(nil), fakeErr.Error())
+	})
+}
+
+type skipper struct {
+	skip bool
+}
+
+func (s skipper) Skip(_ *context.Context) bool {
+	return s.skip
+}
diff --git a/internal/pipe/announce/announce_test.go b/internal/pipe/announce/announce_test.go
index 9b752660..a1338be5 100644
--- a/internal/pipe/announce/announce_test.go
+++ b/internal/pipe/announce/announce_test.go
@@ -13,6 +13,29 @@ func TestDescription(t *testing.T) {
 }
 
 func TestAnnounce(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			Twitter: config.Twitter{
+				Enabled: true,
+			},
+		},
+	})
+	require.Error(t, Pipe{}.Run(ctx))
+}
+
+func TestAnnounceAllDisabled(t *testing.T) {
 	ctx := context.New(config.Project{})
 	require.NoError(t, Pipe{}.Run(ctx))
 }
+
+func TestSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		ctx := context.New(config.Project{})
+		ctx.SkipAnnounce = true
+		require.True(t, Pipe{}.Skip(ctx))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		require.False(t, Pipe{}.Skip(context.New(config.Project{})))
+	})
+}
diff --git a/internal/pipe/artifactory/artifactory_test.go b/internal/pipe/artifactory/artifactory_test.go
index f19cb47a..8d7e9d5d 100644
--- a/internal/pipe/artifactory/artifactory_test.go
+++ b/internal/pipe/artifactory/artifactory_test.go
@@ -10,7 +10,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/artifact"
-	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -568,31 +568,6 @@ func TestRunPipe_UnparsableTarget(t *testing.T) {
 	require.EqualError(t, Pipe{}.Publish(ctx), `artifactory: upload failed: parse "://artifacts.company.com/example-repo-local/mybin/darwin/amd64/mybin": missing protocol scheme`)
 }
 
-func TestRunPipe_SkipWhenPublishFalse(t *testing.T) {
-	ctx := context.New(config.Project{
-		Artifactories: []config.Upload{
-			{
-				Name:     "production",
-				Mode:     "binary",
-				Target:   "http://artifacts.company.com/example-repo-local/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}",
-				Username: "deployuser",
-			},
-		},
-		Archives: []config.Archive{
-			{},
-		},
-	})
-	ctx.Env = map[string]string{
-		"ARTIFACTORY_PRODUCTION_SECRET": "deployuser-secret",
-	}
-	ctx.SkipPublish = true
-
-	require.NoError(t, Pipe{}.Default(ctx))
-	err := Pipe{}.Publish(ctx)
-	require.True(t, pipe.IsSkip(err))
-	require.EqualError(t, err, pipe.ErrSkipPublishEnabled.Error())
-}
-
 func TestRunPipe_DirUpload(t *testing.T) {
 	folder := t.TempDir()
 	dist := filepath.Join(folder, "dist")
@@ -634,12 +609,6 @@ func TestDescription(t *testing.T) {
 	require.NotEmpty(t, Pipe{}.String())
 }
 
-func TestNoArtifactories(t *testing.T) {
-	ctx := context.New(config.Project{})
-	require.NoError(t, Pipe{}.Default(ctx))
-	require.True(t, pipe.IsSkip(Pipe{}.Publish(ctx)))
-}
-
 func TestArtifactoriesWithoutTarget(t *testing.T) {
 	ctx := &context.Context{
 		Env: map[string]string{
@@ -656,7 +625,7 @@ func TestArtifactoriesWithoutTarget(t *testing.T) {
 	}
 
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.True(t, pipe.IsSkip(Pipe{}.Publish(ctx)))
+	testlib.AssertSkipped(t, Pipe{}.Publish(ctx))
 }
 
 func TestArtifactoriesWithoutUsername(t *testing.T) {
@@ -675,7 +644,7 @@ func TestArtifactoriesWithoutUsername(t *testing.T) {
 	}
 
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.True(t, pipe.IsSkip(Pipe{}.Publish(ctx)))
+	testlib.AssertSkipped(t, Pipe{}.Publish(ctx))
 }
 
 func TestArtifactoriesWithoutName(t *testing.T) {
@@ -688,7 +657,7 @@ func TestArtifactoriesWithoutName(t *testing.T) {
 		},
 	})
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.True(t, pipe.IsSkip(Pipe{}.Publish(ctx)))
+	testlib.AssertSkipped(t, Pipe{}.Publish(ctx))
 }
 
 func TestArtifactoriesWithoutSecret(t *testing.T) {
@@ -702,7 +671,7 @@ func TestArtifactoriesWithoutSecret(t *testing.T) {
 		},
 	})
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.True(t, pipe.IsSkip(Pipe{}.Publish(ctx)))
+	testlib.AssertSkipped(t, Pipe{}.Publish(ctx))
 }
 
 func TestArtifactoriesWithInvalidMode(t *testing.T) {
@@ -772,3 +741,16 @@ func TestDefaultSet(t *testing.T) {
 	require.Equal(t, "custom", artifactory.Mode)
 	require.Equal(t, "foo", artifactory.ChecksumHeader)
 }
+
+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{
+			Artifactories: []config.Upload{{}},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
diff --git a/internal/pipe/before/before_test.go b/internal/pipe/before/before_test.go
index 878e25eb..c3fb8c5b 100644
--- a/internal/pipe/before/before_test.go
+++ b/internal/pipe/before/before_test.go
@@ -91,3 +91,18 @@ func TestInvalidTemplate(t *testing.T) {
 		},
 	)), `template: tmpl:1: unexpected "}" in operand`)
 }
+
+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{
+			Before: config.Before{
+				Hooks: []string{""},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
diff --git a/internal/pipe/blob/blob_minio_test.go b/internal/pipe/blob/blob_minio_test.go
index 1d85c12c..7b32e0a6 100644
--- a/internal/pipe/blob/blob_minio_test.go
+++ b/internal/pipe/blob/blob_minio_test.go
@@ -200,75 +200,6 @@ func TestMinioUploadInvalidCustomBucketID(t *testing.T) {
 	require.Error(t, Pipe{}.Publish(ctx))
 }
 
-func TestMinioUploadSkipPublish(t *testing.T) {
-	listen := randomListen(t)
-	folder := t.TempDir()
-	srcpath := filepath.Join(folder, "source.tar.gz")
-	tgzpath := filepath.Join(folder, "bin.tar.gz")
-	debpath := filepath.Join(folder, "bin.deb")
-	checkpath := filepath.Join(folder, "check.txt")
-	require.NoError(t, os.WriteFile(checkpath, []byte("fake checksums"), 0o744))
-	require.NoError(t, os.WriteFile(srcpath, []byte("fake\nsrc"), 0o744))
-	require.NoError(t, os.WriteFile(tgzpath, []byte("fake\ntargz"), 0o744))
-	require.NoError(t, os.WriteFile(debpath, []byte("fake\ndeb"), 0o744))
-	ctx := context.New(config.Project{
-		Dist:        folder,
-		ProjectName: "testupload",
-		Blobs: []config.Blob{
-			{
-				Provider: "s3",
-				Bucket:   "test",
-				Region:   "us-east",
-				Endpoint: "http://" + listen,
-				IDs:      []string{"foo", "bar"},
-			},
-		},
-	})
-	ctx.SkipPublish = true
-	ctx.Git = context.GitInfo{CurrentTag: "v1.2.0"}
-	ctx.Artifacts.Add(&artifact.Artifact{
-		Type: artifact.Checksum,
-		Name: "checksum.txt",
-		Path: checkpath,
-	})
-	ctx.Artifacts.Add(&artifact.Artifact{
-		Type: artifact.UploadableSourceArchive,
-		Name: "source.tar.gz",
-		Path: srcpath,
-		Extra: map[string]interface{}{
-			"Format": "tar.gz",
-		},
-	})
-	ctx.Artifacts.Add(&artifact.Artifact{
-		Type: artifact.UploadableArchive,
-		Name: "bin.tar.gz",
-		Path: tgzpath,
-		Extra: map[string]interface{}{
-			"ID": "foo",
-		},
-	})
-	ctx.Artifacts.Add(&artifact.Artifact{
-		Type: artifact.LinuxPackage,
-		Name: "bin.deb",
-		Path: debpath,
-		Extra: map[string]interface{}{
-			"ID": "bar",
-		},
-	})
-	name := "test_upload"
-	start(t, name, listen)
-	prepareEnv()
-	require.NoError(t, Pipe{}.Default(ctx))
-	require.NoError(t, Pipe{}.Publish(ctx))
-
-	require.NotContains(t, getFiles(t, ctx, ctx.Config.Blobs[0]), []string{
-		"testupload/v1.2.0/bin.deb",
-		"testupload/v1.2.0/bin.tar.gz",
-		"testupload/v1.2.0/checksum.txt",
-		"testupload/v1.2.0/source.tar.gz",
-	})
-}
-
 func randomListen(t *testing.T) string {
 	t.Helper()
 	listener, err := net.Listen("tcp", "127.0.0.1:0")
diff --git a/internal/pipe/blob/blob_test.go b/internal/pipe/blob/blob_test.go
index c94398ca..9421ad71 100644
--- a/internal/pipe/blob/blob_test.go
+++ b/internal/pipe/blob/blob_test.go
@@ -7,7 +7,6 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/artifact"
-	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -17,16 +16,10 @@ func TestDescription(t *testing.T) {
 	require.NotEmpty(t, Pipe{}.String())
 }
 
-func TestNoBlob(t *testing.T) {
-	testlib.AssertSkipped(t, Pipe{}.Publish(context.New(config.Project{})))
-}
-
 func TestDefaultsNoConfig(t *testing.T) {
 	errorString := "bucket or provider cannot be empty"
 	ctx := context.New(config.Project{
-		Blobs: []config.Blob{
-			{},
-		},
+		Blobs: []config.Blob{{}},
 	})
 	require.EqualError(t, Pipe{}.Default(ctx), errorString)
 }
@@ -306,6 +299,19 @@ func TestURL(t *testing.T) {
 	})
 }
 
+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{
+			Blobs: []config.Blob{{}},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
 func setEnv(env map[string]string) {
 	for k, v := range env {
 		os.Setenv(k, v)
diff --git a/internal/pipe/brew/brew_test.go b/internal/pipe/brew/brew_test.go
index e0d546c5..90e7f1e2 100644
--- a/internal/pipe/brew/brew_test.go
+++ b/internal/pipe/brew/brew_test.go
@@ -126,10 +126,10 @@ func TestSplit(t *testing.T) {
 	require.Equal(t, []string{}, parts)
 }
 
-func TestRunPipe(t *testing.T) {
+func TestFullPipe(t *testing.T) {
 	type testcase struct {
-		prepare       func(ctx *context.Context)
-		expectedError string
+		prepare              func(ctx *context.Context)
+		expectedPublishError string
 	}
 	for name, tt := range map[string]testcase{
 		"default": {
@@ -185,7 +185,7 @@ func TestRunPipe(t *testing.T) {
 				ctx.Config.Brews[0].Tap.Name = "test"
 				ctx.Config.Brews[0].CommitMessageTemplate = "{{ .Asdsa }"
 			},
-			expectedError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
 		},
 	} {
 		t.Run(name, func(t *testing.T) {
@@ -250,12 +250,13 @@ 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)
+			require.NoError(t, runAll(ctx, client))
+			if tt.expectedPublishError != "" {
+				require.EqualError(t, publishAll(ctx, client), tt.expectedPublishError)
 				return
 			}
 
-			require.NoError(t, doRun(ctx, ctx.Config.Brews[0], client))
+			require.NoError(t, publishAll(ctx, client))
 			require.True(t, client.CreatedFile)
 			golden.RequireEqualRb(t, []byte(client.Content))
 
@@ -313,7 +314,8 @@ func TestRunPipeNameTemplate(t *testing.T) {
 	client := &DummyClient{}
 	distFile := filepath.Join(folder, "foo_is_bar.rb")
 
-	require.NoError(t, doRun(ctx, ctx.Config.Brews[0], client))
+	require.NoError(t, runAll(ctx, client))
+	require.NoError(t, publishAll(ctx, client))
 	require.True(t, client.CreatedFile)
 	golden.RequireEqualRb(t, []byte(client.Content))
 	distBts, err := os.ReadFile(distFile)
@@ -389,6 +391,7 @@ func TestRunPipeMultipleBrewsWithSkip(t *testing.T) {
 	require.NoError(t, f.Close())
 
 	cli := &DummyClient{}
+	require.NoError(t, runAll(ctx, cli))
 	require.EqualError(t, publishAll(ctx, cli), `brew.skip_upload is set`)
 	require.True(t, cli.CreatedFile)
 
@@ -511,7 +514,8 @@ func TestRunPipeForMultipleArmVersions(t *testing.T) {
 			client := &DummyClient{}
 			distFile := filepath.Join(folder, name+".rb")
 
-			require.NoError(t, doRun(ctx, ctx.Config.Brews[0], client))
+			require.NoError(t, runAll(ctx, client))
+			require.NoError(t, publishAll(ctx, client))
 			require.True(t, client.CreatedFile)
 			golden.RequireEqualRb(t, []byte(client.Content))
 
@@ -537,7 +541,7 @@ func TestRunPipeNoBuilds(t *testing.T) {
 		},
 	}
 	client := &DummyClient{}
-	require.Equal(t, ErrNoArchivesFound, doRun(ctx, ctx.Config.Brews[0], client))
+	require.Equal(t, ErrNoArchivesFound, runAll(ctx, client))
 	require.False(t, client.CreatedFile)
 }
 
@@ -681,22 +685,13 @@ func TestRunPipeMultipleArchivesSameOsBuild(t *testing.T) {
 			})
 		}
 		client := &DummyClient{}
-		require.Equal(t, test.expectedError, doRun(ctx, ctx.Config.Brews[0], client))
+		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 TestRunPipeBrewNotSetup(t *testing.T) {
-	ctx := &context.Context{
-		Config: config.Project{},
-	}
-	client := &DummyClient{}
-	testlib.AssertSkipped(t, doRun(ctx, config.Homebrew{}, client))
-	require.False(t, client.CreatedFile)
-}
-
 func TestRunPipeBinaryRelease(t *testing.T) {
 	ctx := context.New(
 		config.Project{
@@ -718,7 +713,7 @@ func TestRunPipeBinaryRelease(t *testing.T) {
 		Type:   artifact.Binary,
 	})
 	client := &DummyClient{}
-	require.Equal(t, ErrNoArchivesFound, doRun(ctx, ctx.Config.Brews[0], client))
+	require.Equal(t, ErrNoArchivesFound, runAll(ctx, client))
 	require.False(t, client.CreatedFile)
 }
 
@@ -758,21 +753,21 @@ func TestRunPipeNoUpload(t *testing.T) {
 
 	assertNoPublish := func(t *testing.T) {
 		t.Helper()
-		testlib.AssertSkipped(t, doRun(ctx, ctx.Config.Brews[0], client))
+		require.NoError(t, runAll(ctx, client))
+		testlib.AssertSkipped(t, publishAll(ctx, client))
 		require.False(t, client.CreatedFile)
 	}
-	t.Run("skip upload", func(t *testing.T) {
-		ctx.Config.Release.Draft = false
+	t.Run("skip upload true", func(t *testing.T) {
 		ctx.Config.Brews[0].SkipUpload = "true"
-		ctx.SkipPublish = false
+		ctx.Semver.Prerelease = ""
 		assertNoPublish(t)
 	})
-	t.Run("skip publish", func(t *testing.T) {
-		ctx.Config.Release.Draft = false
-		ctx.Config.Brews[0].SkipUpload = "false"
-		ctx.SkipPublish = true
+	t.Run("skip upload auto", func(t *testing.T) {
+		ctx.Config.Brews[0].SkipUpload = "auto"
+		ctx.Semver.Prerelease = "beta1"
 		assertNoPublish(t)
 	})
+	// TODO: skip when ctx.Config.Release.Draft=true ?
 }
 
 func TestRunEmptyTokenType(t *testing.T) {
@@ -807,7 +802,7 @@ func TestRunEmptyTokenType(t *testing.T) {
 		},
 	})
 	client := &DummyClient{}
-	require.NoError(t, doRun(ctx, ctx.Config.Brews[0], client))
+	require.NoError(t, runAll(ctx, client))
 }
 
 func TestRunTokenTypeNotImplementedForBrew(t *testing.T) {
@@ -843,7 +838,7 @@ func TestRunTokenTypeNotImplementedForBrew(t *testing.T) {
 		},
 	})
 	client := &DummyClient{NotImplemented: true}
-	require.Equal(t, ErrTokenTypeNotImplementedForBrew{TokenType: "gitea"}, doRun(ctx, ctx.Config.Brews[0], client))
+	require.EqualError(t, runAll(ctx, client), `token type "gitea" not implemented for brew pipe`)
 }
 
 func TestDefault(t *testing.T) {
@@ -922,3 +917,27 @@ func (dc *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.Comm
 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{
+			Brews: []config.Homebrew{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
+func TestRunSkipNoName(t *testing.T) {
+	ctx := context.New(config.Project{
+		Brews: []config.Homebrew{{}},
+	})
+
+	client := &DummyClient{}
+	testlib.AssertSkipped(t, runAll(ctx, client))
+}
diff --git a/internal/pipe/changelog/changelog_test.go b/internal/pipe/changelog/changelog_test.go
index 39dad118..996ce15e 100644
--- a/internal/pipe/changelog/changelog_test.go
+++ b/internal/pipe/changelog/changelog_test.go
@@ -32,29 +32,12 @@ func TestTemplatedChangelogProvidedViaFlag(t *testing.T) {
 	require.Equal(t, "c0ff33 coffeee v0.0.1\n", ctx.ReleaseNotes)
 }
 
-func TestChangelogProvidedViaFlagAndSkipEnabled(t *testing.T) {
-	ctx := context.New(config.Project{
-		Changelog: config.Changelog{
-			Skip: true,
-		},
-	})
-	ctx.ReleaseNotesFile = "testdata/changes.md"
-	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
-	require.Equal(t, "c0ff33 coffeee\n", ctx.ReleaseNotes)
-}
-
 func TestChangelogProvidedViaFlagDoesntExist(t *testing.T) {
 	ctx := context.New(config.Project{})
 	ctx.ReleaseNotesFile = "testdata/changes.nope"
 	require.EqualError(t, Pipe{}.Run(ctx), "open testdata/changes.nope: no such file or directory")
 }
 
-func TestChangelogSkip(t *testing.T) {
-	ctx := context.New(config.Project{})
-	ctx.Config.Changelog.Skip = true
-	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
-}
-
 func TestReleaseHeaderProvidedViaFlagDoesntExist(t *testing.T) {
 	ctx := context.New(config.Project{})
 	ctx.ReleaseHeaderFile = "testdata/header.nope"
@@ -67,12 +50,6 @@ func TestReleaseFooterProvidedViaFlagDoesntExist(t *testing.T) {
 	require.EqualError(t, Pipe{}.Run(ctx), "open testdata/footer.nope: no such file or directory")
 }
 
-func TestSnapshot(t *testing.T) {
-	ctx := context.New(config.Project{})
-	ctx.Snapshot = true
-	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
-}
-
 func TestChangelog(t *testing.T) {
 	folder := testlib.Mktmp(t)
 	testlib.GitInit(t)
@@ -448,3 +425,25 @@ func TestChangeLogWithoutReleaseFooter(t *testing.T) {
 	require.Contains(t, ctx.ReleaseNotes, "## Changelog")
 	require.Equal(t, rune(ctx.ReleaseNotes[len(ctx.ReleaseNotes)-1]), '\n')
 }
+
+func TestSkip(t *testing.T) {
+	t.Run("skip on snapshot", func(t *testing.T) {
+		ctx := context.New(config.Project{})
+		ctx.Snapshot = true
+		require.True(t, Pipe{}.Skip(ctx))
+	})
+
+	t.Run("skip", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			Changelog: config.Changelog{
+				Skip: true,
+			},
+		})
+		require.True(t, Pipe{}.Skip(ctx))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		ctx := context.New(config.Project{})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
diff --git a/internal/pipe/checksums/checksums_test.go b/internal/pipe/checksums/checksums_test.go
index a901c5d4..41fee744 100644
--- a/internal/pipe/checksums/checksums_test.go
+++ b/internal/pipe/checksums/checksums_test.go
@@ -7,8 +7,6 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/artifact"
-	"github.com/goreleaser/goreleaser/internal/pipe"
-	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -103,21 +101,6 @@ func TestPipe(t *testing.T) {
 	}
 }
 
-func TestPipeSkipTrue(t *testing.T) {
-	folder := t.TempDir()
-	ctx := context.New(
-		config.Project{
-			Dist: folder,
-			Checksum: config.Checksum{
-				Disable: true,
-			},
-		},
-	)
-	err := Pipe{}.Run(ctx)
-	testlib.AssertSkipped(t, err)
-	require.EqualError(t, err, pipe.ErrSkipDisabledPipe.Error())
-}
-
 func TestPipeFileNotExist(t *testing.T) {
 	folder := t.TempDir()
 	ctx := context.New(
@@ -342,4 +325,19 @@ func TestExtraFilesNoMatch(t *testing.T) {
 	require.EqualError(t, Pipe{}.Run(ctx), `globbing failed for pattern ./nope/nope.txt: matching "./nope/nope.txt": file does not exist`)
 }
 
+func TestSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			Checksum: config.Checksum{
+				Disable: true,
+			},
+		})
+		require.True(t, Pipe{}.Skip(ctx))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		require.False(t, Pipe{}.Skip(context.New(config.Project{})))
+	})
+}
+
 // TODO: add tests for LinuxPackage and UploadableSourceArchive
diff --git a/internal/pipe/custompublishers/custompublishers_test.go b/internal/pipe/custompublishers/custompublishers_test.go
new file mode 100644
index 00000000..c22ffd8d
--- /dev/null
+++ b/internal/pipe/custompublishers/custompublishers_test.go
@@ -0,0 +1,38 @@
+package custompublishers
+
+import (
+	"testing"
+
+	"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 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{
+			Publishers: []config.Publisher{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
+func TestPublish(t *testing.T) {
+	require.NoError(t, Pipe{}.Publish(context.New(config.Project{
+		Publishers: []config.Publisher{
+			{
+				Cmd: "echo",
+			},
+		},
+	})))
+}
diff --git a/internal/pipe/discord/discord_test.go b/internal/pipe/discord/discord_test.go
index 592fdb97..97d564d3 100644
--- a/internal/pipe/discord/discord_test.go
+++ b/internal/pipe/discord/discord_test.go
@@ -3,7 +3,6 @@ package discord
 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"
@@ -19,17 +18,10 @@ func TestDefault(t *testing.T) {
 	require.Equal(t, ctx.Config.Announce.Discord.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{
 			Discord: config.Discord{
-				Enabled:         true,
 				MessageTemplate: "{{ .Foo }",
 			},
 		},
@@ -40,23 +32,26 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 func TestAnnounceMissingEnv(t *testing.T) {
 	ctx := context.New(config.Project{
 		Announce: config.Announce{
-			Discord: config.Discord{
-				Enabled: true,
-			},
+			Discord: config.Discord{},
 		},
 	})
 	require.NoError(t, Pipe{}.Default(ctx))
 	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to discord: env: environment variable "DISCORD_WEBHOOK_ID" should not be empty`)
 }
 
-func TestAnnounceSkipAnnounce(t *testing.T) {
-	ctx := context.New(config.Project{
-		Announce: config.Announce{
-			Discord: config.Discord{
-				Enabled: true,
+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{
+				Discord: config.Discord{
+					Enabled: true,
+				},
 			},
-		},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
 	})
-	ctx.SkipAnnounce = true
-	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
 }
diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go
index 7187fe5d..c120e358 100644
--- a/internal/pipe/docker/docker_test.go
+++ b/internal/pipe/docker/docker_test.go
@@ -1176,18 +1176,14 @@ func TestDescription(t *testing.T) {
 	require.NotEmpty(t, Pipe{}.String())
 }
 
-func TestNoDockers(t *testing.T) {
-	require.True(t, pipe.IsSkip(Pipe{}.Run(context.New(config.Project{}))))
-}
-
 func TestNoDockerWithoutImageName(t *testing.T) {
-	require.True(t, pipe.IsSkip(Pipe{}.Run(context.New(config.Project{
+	testlib.AssertSkipped(t, Pipe{}.Run(context.New(config.Project{
 		Dockers: []config.Docker{
 			{
 				Goos: "linux",
 			},
 		},
-	}))))
+	})))
 }
 
 func TestDefault(t *testing.T) {
@@ -1381,7 +1377,7 @@ func Test_processImageTemplates(t *testing.T) {
 			},
 		},
 	}
-	ctx.SkipPublish = true
+
 	ctx.Env = map[string]string{
 		"FOO": "123",
 	}
@@ -1410,3 +1406,31 @@ func Test_processImageTemplates(t *testing.T) {
 		"gcr.io/image:v1.0",
 	}, images)
 }
+
+func TestSkip(t *testing.T) {
+	t.Run("image", func(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{
+				Dockers: []config.Docker{{}},
+			})
+			require.False(t, Pipe{}.Skip(ctx))
+		})
+	})
+
+	t.Run("manifest", func(t *testing.T) {
+		t.Run("skip", func(t *testing.T) {
+			require.True(t, ManifestPipe{}.Skip(context.New(config.Project{})))
+		})
+
+		t.Run("dont skip", func(t *testing.T) {
+			ctx := context.New(config.Project{
+				DockerManifests: []config.DockerManifest{{}},
+			})
+			require.False(t, ManifestPipe{}.Skip(ctx))
+		})
+	})
+}
diff --git a/internal/pipe/milestone/milestone_test.go b/internal/pipe/milestone/milestone_test.go
index cfa8da7f..c4f7c12a 100644
--- a/internal/pipe/milestone/milestone_test.go
+++ b/internal/pipe/milestone/milestone_test.go
@@ -41,7 +41,9 @@ func TestDefaultWithRepoRemote(t *testing.T) {
 	testlib.GitInit(t)
 	testlib.GitRemoteAdd(t, "git@github.com:githubowner/githubrepo.git")
 
-	ctx := context.New(config.Project{})
+	ctx := context.New(config.Project{
+		Milestones: []config.Milestone{{}},
+	})
 	ctx.TokenType = context.TokenTypeGitHub
 	require.NoError(t, Pipe{}.Default(ctx))
 	require.Equal(t, "githubrepo", ctx.Config.Milestones[0].Repo.Name)
@@ -65,7 +67,9 @@ func TestDefaultWithNameTemplate(t *testing.T) {
 func TestDefaultWithoutGitRepo(t *testing.T) {
 	testlib.Mktmp(t)
 	ctx := &context.Context{
-		Config: config.Project{},
+		Config: config.Project{
+			Milestones: []config.Milestone{{}},
+		},
 	}
 	ctx.TokenType = context.TokenTypeGitHub
 	require.EqualError(t, Pipe{}.Default(ctx), "current folder is not a git repository")
@@ -75,7 +79,9 @@ func TestDefaultWithoutGitRepo(t *testing.T) {
 func TestDefaultWithoutGitRepoOrigin(t *testing.T) {
 	testlib.Mktmp(t)
 	ctx := &context.Context{
-		Config: config.Project{},
+		Config: config.Project{
+			Milestones: []config.Milestone{{}},
+		},
 	}
 	ctx.TokenType = context.TokenTypeGitHub
 	testlib.GitInit(t)
@@ -86,7 +92,9 @@ func TestDefaultWithoutGitRepoOrigin(t *testing.T) {
 func TestDefaultWithoutGitRepoSnapshot(t *testing.T) {
 	testlib.Mktmp(t)
 	ctx := &context.Context{
-		Config: config.Project{},
+		Config: config.Project{
+			Milestones: []config.Milestone{{}},
+		},
 	}
 	ctx.TokenType = context.TokenTypeGitHub
 	ctx.Snapshot = true
@@ -97,7 +105,7 @@ func TestDefaultWithoutGitRepoSnapshot(t *testing.T) {
 func TestDefaultWithoutNameTemplate(t *testing.T) {
 	ctx := &context.Context{
 		Config: config.Project{
-			Milestones: []config.Milestone{},
+			Milestones: []config.Milestone{{}},
 		},
 	}
 	require.NoError(t, Pipe{}.Default(ctx))
@@ -185,6 +193,21 @@ func TestPublishCloseFailOnError(t *testing.T) {
 	require.Equal(t, "", client.ClosedMilestone)
 }
 
+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{
+			Milestones: []config.Milestone{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
 type DummyClient struct {
 	ClosedMilestone      string
 	FailToCloseMilestone bool
diff --git a/internal/pipe/nfpm/nfpm_test.go b/internal/pipe/nfpm/nfpm_test.go
index 8cd5e7a7..0f186b1b 100644
--- a/internal/pipe/nfpm/nfpm_test.go
+++ b/internal/pipe/nfpm/nfpm_test.go
@@ -1123,6 +1123,21 @@ func TestBinDirTemplating(t *testing.T) {
 	}
 }
 
+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{
+			NFPMs: []config.NFPM{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
 func sources(contents files.Contents) []string {
 	result := make([]string, 0, len(contents))
 	for _, f := range contents {
diff --git a/internal/pipe/pipe_test.go b/internal/pipe/pipe_test.go
index c70cc214..69b0a027 100644
--- a/internal/pipe/pipe_test.go
+++ b/internal/pipe/pipe_test.go
@@ -16,15 +16,9 @@ func TestSkipPipe(t *testing.T) {
 
 func TestIsSkip(t *testing.T) {
 	require.True(t, IsSkip(Skip("whatever")))
-	require.True(t, IsSkip(ErrSkipDisabledPipe))
 	require.False(t, IsSkip(errors.New("nope")))
 }
 
-func TestIsExpectedSkip(t *testing.T) {
-	require.True(t, IsSkip(ErrSkipDisabledPipe))
-	require.True(t, IsSkip(Skip("nope")))
-}
-
 func TestSkipMemento(t *testing.T) {
 	m := SkipMemento{}
 	m.Remember(Skip("foo"))
diff --git a/internal/pipe/publish/publish_test.go b/internal/pipe/publish/publish_test.go
index 93a103c0..e077a8c1 100644
--- a/internal/pipe/publish/publish_test.go
+++ b/internal/pipe/publish/publish_test.go
@@ -21,3 +21,15 @@ func TestPublish(t *testing.T) {
 	}
 	require.NoError(t, Pipe{}.Run(ctx))
 }
+
+func TestSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		ctx := context.New(config.Project{})
+		ctx.SkipPublish = true
+		require.True(t, Pipe{}.Skip(ctx))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		require.False(t, Pipe{}.Skip(context.New(config.Project{})))
+	})
+}
diff --git a/internal/pipe/reddit/reddit_test.go b/internal/pipe/reddit/reddit_test.go
index 508fcab0..e1d76187 100644
--- a/internal/pipe/reddit/reddit_test.go
+++ b/internal/pipe/reddit/reddit_test.go
@@ -3,7 +3,6 @@ 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"
@@ -19,17 +18,10 @@ func TestDefault(t *testing.T) {
 	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 }",
 			},
 		},
@@ -41,7 +33,6 @@ func TestAnnounceInvalidTitleTemplate(t *testing.T) {
 	ctx := context.New(config.Project{
 		Announce: config.Announce{
 			Reddit: config.Reddit{
-				Enabled:       true,
 				TitleTemplate: "{{ .Foo }",
 			},
 		},
@@ -52,23 +43,26 @@ func TestAnnounceInvalidTitleTemplate(t *testing.T) {
 func TestAnnounceMissingEnv(t *testing.T) {
 	ctx := context.New(config.Project{
 		Announce: config.Announce{
-			Reddit: config.Reddit{
-				Enabled: true,
-			},
+			Reddit: config.Reddit{},
 		},
 	})
 	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,
+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{
+				Reddit: config.Reddit{
+					Enabled: true,
+				},
 			},
-		},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
 	})
-	ctx.SkipAnnounce = true
-	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
 }
diff --git a/internal/pipe/release/release_test.go b/internal/pipe/release/release_test.go
index 4b0558ab..e0b00e48 100644
--- a/internal/pipe/release/release_test.go
+++ b/internal/pipe/release/release_test.go
@@ -314,18 +314,6 @@ func TestRunPipeUploadRetry(t *testing.T) {
 	require.True(t, client.UploadedFile)
 }
 
-func TestPipeDisabled(t *testing.T) {
-	ctx := context.New(config.Project{
-		Release: config.Release{
-			Disable: true,
-		},
-	})
-	client := &DummyClient{}
-	testlib.AssertSkipped(t, doPublish(ctx, client))
-	require.False(t, client.CreatedRelease)
-	require.False(t, client.UploadedFile)
-}
-
 func TestDefault(t *testing.T) {
 	testlib.Mktmp(t)
 	testlib.GitInit(t)
@@ -521,6 +509,21 @@ func TestDefaultMultipleReleasesDefined(t *testing.T) {
 	require.EqualError(t, Pipe{}.Default(ctx), ErrMultipleReleases.Error())
 }
 
+func TestSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			Release: config.Release{
+				Disable: true,
+			},
+		})
+		require.True(t, Pipe{}.Skip(ctx))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		require.False(t, Pipe{}.Skip(context.New(config.Project{})))
+	})
+}
+
 type DummyClient struct {
 	FailToCreateRelease bool
 	FailToUpload        bool
diff --git a/internal/pipe/scoop/scoop_test.go b/internal/pipe/scoop/scoop_test.go
index 328f2f36..12bb5c6f 100644
--- a/internal/pipe/scoop/scoop_test.go
+++ b/internal/pipe/scoop/scoop_test.go
@@ -9,7 +9,6 @@ import (
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/client"
 	"github.com/goreleaser/goreleaser/internal/golden"
-	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
@@ -84,11 +83,12 @@ func Test_doRun(t *testing.T) {
 	}
 
 	tests := []struct {
-		name        string
-		args        args
-		artifacts   []*artifact.Artifact
-		assertError errChecker
-		assert      asserter
+		name               string
+		args               args
+		artifacts          []*artifact.Artifact
+		assertRunError     errChecker
+		assertPublishError errChecker
+		assert             asserter
 	}{
 		{
 			"valid public github",
@@ -133,6 +133,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldNotErr,
+			shouldNotErr,
 			func(t *testing.T, a args) {
 				t.Helper()
 				require.Equal(t, "scoops/run-pipe.json", a.client.Path)
@@ -196,6 +197,7 @@ func Test_doRun(t *testing.T) {
 				},
 			},
 			shouldNotErr,
+			shouldNotErr,
 			noAssertions,
 		},
 		{
@@ -241,6 +243,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldNotErr,
+			shouldNotErr,
 			func(t *testing.T, a args) {
 				t.Helper()
 				require.Equal(t, "run-pipe.json", a.client.Path)
@@ -298,6 +301,7 @@ func Test_doRun(t *testing.T) {
 				},
 			},
 			shouldNotErr,
+			shouldNotErr,
 			noAssertions,
 		},
 		{
@@ -353,6 +357,7 @@ func Test_doRun(t *testing.T) {
 				},
 			},
 			shouldNotErr,
+			shouldNotErr,
 			noAssertions,
 		},
 		{
@@ -396,6 +401,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr(ErrTokenTypeNotImplementedForScoop.Error()),
+			shouldNotErr,
 			noAssertions,
 		},
 		{
@@ -440,87 +446,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_linux_386.tar.gz", Goos: "linux", Goarch: "386"},
 			},
 			shouldErr("scoop requires a windows build"),
-			noAssertions,
-		},
-		{
-			"no scoop",
-			args{
-				&context.Context{
-					TokenType: context.TokenTypeGitHub,
-					Git: context.GitInfo{
-						CurrentTag: "v1.0.1",
-					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
-					Config: config.Project{
-						Builds: []config.Build{
-							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
-						},
-						Dist:        ".",
-						ProjectName: "run-pipe",
-						Archives: []config.Archive{
-							{Format: "tar.gz"},
-						},
-						Release: config.Release{
-							GitHub: config.Repo{
-								Owner: "test",
-								Name:  "test",
-							},
-						},
-					},
-				},
-				&DummyClient{},
-			},
-			[]*artifact.Artifact{
-				{Name: "foo_1.0.1_windows_amd64.tar.gz", Goos: "windows", Goarch: "amd64"},
-				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386"},
-			},
-			shouldErr(pipe.ErrSkipDisabledPipe.Error()),
-			noAssertions,
-		},
-		{
-			"no publish",
-			args{
-				&context.Context{
-					TokenType: context.TokenTypeGitHub,
-					Git: context.GitInfo{
-						CurrentTag: "v1.0.1",
-					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
-					Config: config.Project{
-						Builds: []config.Build{
-							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
-						},
-						Dist:        ".",
-						ProjectName: "run-pipe",
-						Archives: []config.Archive{
-							{Format: "tar.gz"},
-						},
-						Release: config.Release{
-							GitHub: config.Repo{
-								Owner: "test",
-								Name:  "test",
-							},
-						},
-						Scoop: config.Scoop{
-							Bucket: config.RepoRef{
-								Owner: "test",
-								Name:  "test",
-							},
-							Description: "A run pipe test formula",
-							Homepage:    "https://github.com/goreleaser",
-						},
-					},
-					SkipPublish: true,
-				},
-				&DummyClient{},
-			},
-			[]*artifact.Artifact{
-				{Name: "foo_1.0.1_windows_amd64.tar.gz", Goos: "windows", Goarch: "amd64", Path: file},
-				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
-			},
-			shouldErr(pipe.ErrSkipPublishEnabled.Error()),
+			shouldNotErr,
 			noAssertions,
 		},
 		{
@@ -561,6 +487,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_amd64.tar.gz", Goos: "windows", Goarch: "amd64", Path: file},
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
+			shouldNotErr,
 			shouldErr("release is marked as draft"),
 			noAssertions,
 		},
@@ -612,6 +539,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1-pre.1_windows_amd64.tar.gz", Goos: "windows", Goarch: "amd64", Path: file},
 				{Name: "foo_1.0.1-pre.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
+			shouldNotErr,
 			shouldErr("release is prerelease"),
 			noAssertions,
 		},
@@ -657,6 +585,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1-pre.1_windows_amd64.tar.gz", Goos: "windows", Goarch: "amd64", Path: file},
 				{Name: "foo_1.0.1-pre.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
+			shouldNotErr,
 			shouldErr("scoop.skip_upload is true"),
 			noAssertions,
 		},
@@ -698,6 +627,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_amd64.tar.gz", Goos: "windows", Goarch: "amd64", Path: file},
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
+			shouldNotErr,
 			shouldErr("release is disabled"),
 			noAssertions,
 		},
@@ -740,6 +670,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr("archive format is binary"),
+			shouldNotErr,
 			noAssertions,
 		},
 	}
@@ -751,7 +682,8 @@ func Test_doRun(t *testing.T) {
 			}
 			require.NoError(t, Pipe{}.Default(ctx))
 
-			tt.assertError(t, doRun(ctx, tt.args.client))
+			tt.assertRunError(t, doRun(ctx, tt.args.client))
+			tt.assertPublishError(t, doPublish(ctx, tt.args.client))
 			tt.assert(t, tt.args)
 		})
 	}
@@ -1052,29 +984,14 @@ func TestRunPipeScoopWithSkipUpload(t *testing.T) {
 	require.NoError(t, f.Close())
 
 	cli := &DummyClient{}
-	require.EqualError(t, doRun(ctx, cli), `scoop.skip_upload is true`)
+	require.NoError(t, doRun(ctx, cli))
+	require.EqualError(t, doPublish(ctx, cli), `scoop.skip_upload is true`)
 
 	distFile := filepath.Join(folder, ctx.Config.Scoop.Name+".json")
 	_, err = os.Stat(distFile)
 	require.NoError(t, err, "file should exist: "+distFile)
 }
 
-func TestRunPipeScoopWithSkipPublish(t *testing.T) {
-	folder := t.TempDir()
-	ctx, path := getScoopPipeSkipCtx(folder)
-	ctx.SkipPublish = true
-
-	f, err := os.Create(path)
-	require.NoError(t, err)
-	require.NoError(t, f.Close())
-
-	cli := &DummyClient{}
-	require.EqualError(t, doRun(ctx, cli), pipe.ErrSkipPublishEnabled.Error())
-
-	distFile := filepath.Join(folder, ctx.Config.Scoop.Name+".json")
-	require.FileExists(t, distFile)
-}
-
 func TestWrapInDirectory(t *testing.T) {
 	folder := t.TempDir()
 	file := filepath.Join(folder, "archive")
@@ -1146,6 +1063,23 @@ func TestWrapInDirectory(t *testing.T) {
 	golden.RequireEqualJSON(t, out.Bytes())
 }
 
+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{
+			Scoop: config.Scoop{
+				Bucket: config.RepoRef{
+					Name: "a",
+				},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
 type DummyClient struct {
 	CreatedFile    bool
 	Content        string
diff --git a/internal/pipe/sign/sign_docker_test.go b/internal/pipe/sign/sign_docker_test.go
index 42e41ef6..4425a0e4 100644
--- a/internal/pipe/sign/sign_docker_test.go
+++ b/internal/pipe/sign/sign_docker_test.go
@@ -37,20 +37,6 @@ func TestDockerSignDisabled(t *testing.T) {
 	require.EqualError(t, err, "artifact signing is disabled")
 }
 
-func TestDockerSignSkipped(t *testing.T) {
-	ctx := context.New(config.Project{})
-	ctx.SkipSign = true
-	err := DockerPipe{}.Publish(ctx)
-	require.EqualError(t, err, "artifact signing is disabled")
-}
-
-func TestDockerSignSkipPublish(t *testing.T) {
-	ctx := context.New(config.Project{})
-	ctx.SkipPublish = true
-	err := DockerPipe{}.Publish(ctx)
-	require.EqualError(t, err, "artifact signing is disabled")
-}
-
 func TestDockerSignInvalidArtifacts(t *testing.T) {
 	ctx := context.New(config.Project{})
 	ctx.Config.DockerSigns = []config.Sign{
@@ -187,3 +173,24 @@ func TestDockerSignArtifacts(t *testing.T) {
 		})
 	}
 }
+
+func TestDockerSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		require.True(t, DockerPipe{}.Skip(context.New(config.Project{})))
+	})
+
+	t.Run("skip sign", func(t *testing.T) {
+		ctx := context.New(config.Project{})
+		ctx.SkipSign = true
+		require.True(t, DockerPipe{}.Skip(ctx))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			DockerSigns: []config.Sign{
+				{},
+			},
+		})
+		require.False(t, DockerPipe{}.Skip(ctx))
+	})
+}
diff --git a/internal/pipe/sign/sign_test.go b/internal/pipe/sign/sign_test.go
index a4bbacd8..e44fc818 100644
--- a/internal/pipe/sign/sign_test.go
+++ b/internal/pipe/sign/sign_test.go
@@ -70,13 +70,6 @@ func TestSignDisabled(t *testing.T) {
 	require.EqualError(t, err, "artifact signing is disabled")
 }
 
-func TestSignSkipped(t *testing.T) {
-	ctx := context.New(config.Project{})
-	ctx.SkipSign = true
-	err := Pipe{}.Run(ctx)
-	require.EqualError(t, err, "artifact signing is disabled")
-}
-
 func TestSignInvalidArtifacts(t *testing.T) {
 	ctx := context.New(config.Project{})
 	ctx.Config.Signs = []config.Sign{
@@ -630,3 +623,24 @@ func TestSeveralSignsWithTheSameID(t *testing.T) {
 	}
 	require.EqualError(t, Pipe{}.Default(ctx), "found 2 signs with the ID 'a', please fix your config")
 }
+
+func TestSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		require.True(t, Pipe{}.Skip(context.New(config.Project{})))
+	})
+
+	t.Run("skip sign", func(t *testing.T) {
+		ctx := context.New(config.Project{})
+		ctx.SkipSign = true
+		require.True(t, Pipe{}.Skip(ctx))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			Signs: []config.Sign{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
diff --git a/internal/pipe/slack/slack_test.go b/internal/pipe/slack/slack_test.go
index c95691b4..d76af601 100644
--- a/internal/pipe/slack/slack_test.go
+++ b/internal/pipe/slack/slack_test.go
@@ -3,7 +3,6 @@ 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"
@@ -19,17 +18,10 @@ func TestDefault(t *testing.T) {
 	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 }",
 			},
 		},
@@ -40,23 +32,26 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 func TestAnnounceMissingEnv(t *testing.T) {
 	ctx := context.New(config.Project{
 		Announce: config.Announce{
-			Slack: config.Slack{
-				Enabled: true,
-			},
+			Slack: config.Slack{},
 		},
 	})
 	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,
+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{
+				Slack: config.Slack{
+					Enabled: true,
+				},
 			},
-		},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
 	})
-	ctx.SkipAnnounce = true
-	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
 }
diff --git a/internal/pipe/smtp/smtp_test.go b/internal/pipe/smtp/smtp_test.go
new file mode 100644
index 00000000..010d9673
--- /dev/null
+++ b/internal/pipe/smtp/smtp_test.go
@@ -0,0 +1,43 @@
+package smtp
+
+import (
+	"testing"
+
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestStringer(t *testing.T) {
+	require.NotEmpty(t, Pipe{}.String())
+}
+
+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{
+				SMTP: config.SMTP{
+					Enabled: true,
+				},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
+func TestDefault(t *testing.T) {
+	ctx := context.New(config.Project{
+		Announce: config.Announce{
+			SMTP: config.SMTP{
+				Enabled: true,
+			},
+		},
+	})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Equal(t, defaultBodyTemplate, ctx.Config.Announce.SMTP.BodyTemplate)
+	require.Equal(t, defaultSubjectTemplate, ctx.Config.Announce.SMTP.SubjectTemplate)
+}
diff --git a/internal/pipe/snapcraft/snapcraft_test.go b/internal/pipe/snapcraft/snapcraft_test.go
index ce21d00d..b6a48d50 100644
--- a/internal/pipe/snapcraft/snapcraft_test.go
+++ b/internal/pipe/snapcraft/snapcraft_test.go
@@ -538,6 +538,21 @@ func Test_isValidArch(t *testing.T) {
 	}
 }
 
+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{
+			Snapcrafts: []config.Snapcraft{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
 func requireEqualFiles(tb testing.TB, a, b string) {
 	tb.Helper()
 	eq, err := gio.EqualFiles(a, b)
diff --git a/internal/pipe/snapshot/snapshot_test.go b/internal/pipe/snapshot/snapshot_test.go
index 5ba6fb5c..ac5c6f07 100644
--- a/internal/pipe/snapshot/snapshot_test.go
+++ b/internal/pipe/snapshot/snapshot_test.go
@@ -3,7 +3,6 @@ package snapshot
 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"
@@ -41,7 +40,6 @@ func TestSnapshotInvalidNametemplate(t *testing.T) {
 			NameTemplate: "{{.ShortCommit}{{{sss}}}",
 		},
 	})
-	ctx.Snapshot = true
 	require.EqualError(t, Pipe{}.Run(ctx), `failed to generate snapshot name: template: tmpl:1: unexpected "}" in operand`)
 }
 
@@ -51,12 +49,29 @@ func TestSnapshotEmptyFinalName(t *testing.T) {
 			NameTemplate: "{{ .Commit }}",
 		},
 	})
-	ctx.Snapshot = true
 	ctx.Git.CurrentTag = "v1.2.3"
 	require.EqualError(t, Pipe{}.Run(ctx), "empty snapshot name")
 }
 
-func TestNotASnapshot(t *testing.T) {
-	ctx := context.New(config.Project{})
-	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
+func TestSnapshot(t *testing.T) {
+	ctx := context.New(config.Project{
+		Snapshot: config.Snapshot{
+			NameTemplate: "{{ incpatch .Tag }}",
+		},
+	})
+	ctx.Git.CurrentTag = "v1.2.3"
+	require.NoError(t, Pipe{}.Run(ctx))
+	require.Equal(t, "v1.2.4", ctx.Version)
+}
+
+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{})
+		ctx.Snapshot = true
+		require.False(t, Pipe{}.Skip(ctx))
+	})
 }
diff --git a/internal/pipe/sourcearchive/source_test.go b/internal/pipe/sourcearchive/source_test.go
index 9d05252b..82bbbecc 100644
--- a/internal/pipe/sourcearchive/source_test.go
+++ b/internal/pipe/sourcearchive/source_test.go
@@ -75,9 +75,20 @@ func TestInvalidNameTemplate(t *testing.T) {
 }
 
 func TestDisabled(t *testing.T) {
-	testlib.AssertSkipped(t, Pipe{}.Run(context.New(config.Project{})))
+	require.True(t, Pipe{}.Skip(context.New(config.Project{})))
 }
 
-func TestString(t *testing.T) {
-	require.NotEmpty(t, Pipe{}.String())
+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{
+			Source: config.Source{
+				Enabled: true,
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
 }
diff --git a/internal/pipe/teams/teams_test.go b/internal/pipe/teams/teams_test.go
index 9276d394..ad3bfefe 100644
--- a/internal/pipe/teams/teams_test.go
+++ b/internal/pipe/teams/teams_test.go
@@ -3,7 +3,6 @@ package teams
 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"
@@ -19,12 +18,6 @@ func TestDefault(t *testing.T) {
 	require.Equal(t, ctx.Config.Announce.Teams.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{
@@ -49,14 +42,19 @@ func TestAnnounceMissingEnv(t *testing.T) {
 	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to teams: env: environment variable "TEAMS_WEBHOOK" should not be empty`)
 }
 
-func TestAnnounceSkipAnnounce(t *testing.T) {
-	ctx := context.New(config.Project{
-		Announce: config.Announce{
-			Teams: config.Teams{
-				Enabled: true,
+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{
+				Teams: config.Teams{
+					Enabled: true,
+				},
 			},
-		},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
 	})
-	ctx.SkipAnnounce = true
-	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
 }
diff --git a/internal/pipe/twitter/twitter_test.go b/internal/pipe/twitter/twitter_test.go
index f1db1ec8..ee910b8b 100644
--- a/internal/pipe/twitter/twitter_test.go
+++ b/internal/pipe/twitter/twitter_test.go
@@ -3,7 +3,6 @@ package twitter
 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"
@@ -19,17 +18,10 @@ func TestDefault(t *testing.T) {
 	require.Equal(t, ctx.Config.Announce.Twitter.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{
 			Twitter: config.Twitter{
-				Enabled:         true,
 				MessageTemplate: "{{ .Foo }",
 			},
 		},
@@ -40,23 +32,26 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 func TestAnnounceMissingEnv(t *testing.T) {
 	ctx := context.New(config.Project{
 		Announce: config.Announce{
-			Twitter: config.Twitter{
-				Enabled: true,
-			},
+			Twitter: config.Twitter{},
 		},
 	})
 	require.NoError(t, Pipe{}.Default(ctx))
 	require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to twitter: env: environment variable "TWITTER_CONSUMER_KEY" should not be empty`)
 }
 
-func TestAnnounceSkipAnnounce(t *testing.T) {
-	ctx := context.New(config.Project{
-		Announce: config.Announce{
-			Twitter: config.Twitter{
-				Enabled: true,
+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{
+				Twitter: config.Twitter{
+					Enabled: true,
+				},
 			},
-		},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
 	})
-	ctx.SkipAnnounce = true
-	testlib.AssertSkipped(t, Pipe{}.Announce(ctx))
 }
diff --git a/internal/pipe/upload/upload_test.go b/internal/pipe/upload/upload_test.go
index 6d762dac..24bb0567 100644
--- a/internal/pipe/upload/upload_test.go
+++ b/internal/pipe/upload/upload_test.go
@@ -543,30 +543,6 @@ func TestRunPipe_UnparsableTarget(t *testing.T) {
 	require.EqualError(t, Pipe{}.Publish(ctx), `upload: upload failed: parse "://artifacts.company.com/example-repo-local/mybin/darwin/amd64/mybin": missing protocol scheme`)
 }
 
-func TestRunPipe_SkipWhenPublishFalse(t *testing.T) {
-	ctx := context.New(config.Project{
-		Uploads: []config.Upload{
-			{
-				Name:     "production",
-				Mode:     "binary",
-				Target:   "http://artifacts.company.com/example-repo-local/{{ .ProjectName }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}",
-				Username: "deployuser",
-			},
-		},
-		Archives: []config.Archive{
-			{},
-		},
-	})
-	ctx.Env = map[string]string{
-		"UPLOAD_PRODUCTION_SECRET": "deployuser-secret",
-	}
-	ctx.SkipPublish = true
-
-	err := Pipe{}.Publish(ctx)
-	require.True(t, pipe.IsSkip(err))
-	require.EqualError(t, err, pipe.ErrSkipPublishEnabled.Error())
-}
-
 func TestRunPipe_DirUpload(t *testing.T) {
 	folder := t.TempDir()
 	dist := filepath.Join(folder, "dist")
@@ -608,10 +584,6 @@ func TestDescription(t *testing.T) {
 	require.NotEmpty(t, Pipe{}.String())
 }
 
-func TestNoPuts(t *testing.T) {
-	require.True(t, pipe.IsSkip(Pipe{}.Publish(context.New(config.Project{}))))
-}
-
 func TestPutsWithoutTarget(t *testing.T) {
 	ctx := &context.Context{
 		Env: map[string]string{
@@ -741,3 +713,18 @@ func TestDefaultSet(t *testing.T) {
 	require.Equal(t, "custom", upload.Mode)
 	require.Equal(t, h.MethodPost, upload.Method)
 }
+
+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{
+			Uploads: []config.Upload{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}