Repository: goreleaser/goreleaser
Pull request #3185 (2022-06-23 03:00:54)

fix: blob upload eating errors
closes  #2652

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/3185?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#3185](https://codecov.io/gh/goreleaser/goreleaser/pull/3185?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (fe7e23d) into [main](https://codecov.io/gh/goreleaser/goreleaser/commit/8424c9210284d570c87f9748f3abd6a28b51d116?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (8424c92) will **decrease** coverage by `0.03%`.
> The diff coverage is `40.00%`.

```diff
@@            Coverage Diff             @@
##             main    #3185      +/-   ##
==========================================
- Coverage   84.41%   84.37%   -0.04%     
==========================================
  Files         113      113              
  Lines        9237     9236       -1     
==========================================
- Hits         7797     7793       -4     
- Misses       1167     1169       +2     
- Partials      273      274       +1     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/3185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/pipe/blob/upload.go](https://codecov.io/gh/goreleaser/goreleaser/pull/3185/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9ibG9iL3VwbG9hZC5nbw==) | `75.46% <40.00%> (-1.98%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/3185?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/3185?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [8424c92...fe7e23d](https://codecov.io/gh/goreleaser/goreleaser/pull/3185?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).


Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index f57e1f94..6c409ea6 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -16,6 +16,6 @@ jobs:
 
     steps:
     - uses: actions/checkout@b0e28b5ac45a892f91e7d036f8200cf5ed489415 # v3
-    - uses: github/codeql-action/init@d00e8c09a38ef8c1ca1091fc55ef490776d2de73 # v2
-    - uses: github/codeql-action/autobuild@d00e8c09a38ef8c1ca1091fc55ef490776d2de73 # v2
-    - uses: github/codeql-action/analyze@d00e8c09a38ef8c1ca1091fc55ef490776d2de73 # v2
+    - uses: github/codeql-action/init@41a4ada31ba866a7f1196b9602703a89edd69e22 # v2
+    - uses: github/codeql-action/autobuild@41a4ada31ba866a7f1196b9602703a89edd69e22 # v2
+    - uses: github/codeql-action/analyze@41a4ada31ba866a7f1196b9602703a89edd69e22 # v2
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index 91c48a69..16ed3d0b 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -3,6 +3,7 @@ package artifact
 
 // nolint: gosec
 import (
+	"bytes"
 	"crypto/md5"
 	"crypto/sha1"
 	"crypto/sha256"
@@ -125,10 +126,10 @@ const (
 )
 
 // Extras represents the extra fields in an artifact.
-type Extras map[string]interface{}
+type Extras map[string]any
 
 func (e Extras) MarshalJSON() ([]byte, error) {
-	m := map[string]interface{}{}
+	m := map[string]any{}
 	for k, v := range e {
 		if k == ExtraRefresh {
 			// refresh is a func, so we can't serialize it.
@@ -157,13 +158,42 @@ func (a Artifact) String() string {
 	return a.Name
 }
 
+// Extra tries to get the extra field with the given name, returning either
+// its value, the default value for its type, or an error.
+//
+// If the extra value cannot be cast into the given type, it'll try to convert
+// it to JSON and unmarshal it into the correct type after.
+//
+// If that fails as well, it'll error.
+func Extra[T any](a Artifact, key string) (T, error) {
+	ex := a.Extra[key]
+	if ex == nil {
+		return *(new(T)), nil
+	}
+
+	t, ok := ex.(T)
+	if ok {
+		return t, nil
+	}
+
+	bts, err := json.Marshal(ex)
+	if err != nil {
+		return t, err
+	}
+
+	decoder := json.NewDecoder(bytes.NewReader(bts))
+	decoder.DisallowUnknownFields()
+	err = decoder.Decode(&t)
+	return t, err
+}
+
 // ExtraOr returns the Extra field with the given key or the or value specified
 // if it is nil.
-func (a Artifact) ExtraOr(key string, or interface{}) interface{} {
+func ExtraOr[T any](a Artifact, key string, or T) T {
 	if a.Extra[key] == nil {
 		return or
 	}
-	return a.Extra[key]
+	return a.Extra[key].(T)
 }
 
 // Checksum calculates the checksum of the artifact.
@@ -210,11 +240,7 @@ func (a Artifact) Refresh() error {
 	if a.Type != Checksum {
 		return nil
 	}
-	fn, ok := a.ExtraOr(ExtraRefresh, noRefresh).(func() error)
-	if !ok {
-		return nil
-	}
-	if err := fn(); err != nil {
+	if err := ExtraOr(a, ExtraRefresh, noRefresh)(); err != nil {
 		return fmt.Errorf("failed to refresh %q: %w", a.Name, err)
 	}
 	return nil
@@ -222,12 +248,12 @@ func (a Artifact) Refresh() error {
 
 // ID returns the artifact ID if it exists, empty otherwise.
 func (a Artifact) ID() string {
-	return a.ExtraOr(ExtraID, "").(string)
+	return ExtraOr(a, ExtraID, "")
 }
 
 // Format returns the artifact Format if it exists, empty otherwise.
 func (a Artifact) Format() string {
-	return a.ExtraOr(ExtraFormat, "").(string)
+	return ExtraOr(a, ExtraFormat, "")
 }
 
 // Artifacts is a list of artifacts.
@@ -318,7 +344,7 @@ type Filter func(a *Artifact) bool
 //
 // This is useful specially on homebrew et al, where you'll want to use only either the single-arch or the universal binaries.
 func OnlyReplacingUnibins(a *Artifact) bool {
-	return a.ExtraOr(ExtraReplaces, true).(bool)
+	return ExtraOr(*a, ExtraReplaces, true)
 }
 
 // ByGoos is a predefined filter that filters by the given goos.
@@ -389,7 +415,7 @@ func ByExt(exts ...string) Filter {
 	for _, ext := range exts {
 		ext := ext
 		filters = append(filters, func(a *Artifact) bool {
-			return a.ExtraOr(ExtraExt, "") == ext
+			return ExtraOr(*a, ExtraExt, "") == ext
 		})
 	}
 	return Or(filters...)
diff --git a/internal/pipe/archive/archive.go b/internal/pipe/archive/archive.go
index c87ff6ff..50199b23 100644
--- a/internal/pipe/archive/archive.go
+++ b/internal/pipe/archive/archive.go
@@ -236,7 +236,7 @@ func skip(ctx *context.Context, archive config.Archive, binaries []*artifact.Art
 		if err != nil {
 			return err
 		}
-		finalName := name + binary.ExtraOr(artifact.ExtraExt, "").(string)
+		finalName := name + artifact.ExtraOr(*binary, artifact.ExtraExt, "")
 		log.WithField("binary", binary.Name).
 			WithField("name", finalName).
 			Info("skip archiving")
diff --git a/internal/pipe/aur/aur.go b/internal/pipe/aur/aur.go
index 0abcf8e8..3dff67d8 100644
--- a/internal/pipe/aur/aur.go
+++ b/internal/pipe/aur/aur.go
@@ -137,10 +137,10 @@ func doRun(ctx *context.Context, aur config.AUR, cl client.Client) error {
 		switch art.Type {
 		case artifact.UploadableBinary:
 			name := art.Name
-			bin := art.ExtraOr(artifact.ExtraBinary, art.Name).(string)
+			bin := artifact.ExtraOr(*art, artifact.ExtraBinary, art.Name)
 			pkg = fmt.Sprintf(`install -Dm755 "./%s "${pkgdir}/usr/bin/%s"`, name, bin)
 		case artifact.UploadableArchive:
-			for _, bin := range art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string) {
+			for _, bin := range artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{}) {
 				pkg = fmt.Sprintf(`install -Dm755 "./%s" "${pkgdir}/usr/bin/%[1]s"`, bin)
 				break
 			}
@@ -318,7 +318,7 @@ func dataFor(ctx *context.Context, cfg config.AUR, cl client.Client, artifacts [
 			DownloadURL: url,
 			SHA256:      sum,
 			Arch:        toPkgBuildArch(art.Goarch + art.Goarm),
-			Format:      art.ExtraOr(artifact.ExtraFormat, "").(string),
+			Format:      artifact.ExtraOr(*art, artifact.ExtraFormat, ""),
 		}
 		result.ReleasePackages = append(result.ReleasePackages, releasePackage)
 		result.Arches = append(result.Arches, releasePackage.Arch)
@@ -353,7 +353,10 @@ func (Pipe) Publish(ctx *context.Context) error {
 }
 
 func doPublish(ctx *context.Context, pkgs []*artifact.Artifact) error {
-	cfg := pkgs[0].Extra[aurExtra].(config.AUR)
+	cfg, err := artifact.Extra[config.AUR](*pkgs[0], aurExtra)
+	if err != nil {
+		return err
+	}
 
 	if strings.TrimSpace(cfg.SkipUpload) == "true" {
 		return pipe.Skip("aur.skip_upload is set")
diff --git a/internal/pipe/blob/upload.go b/internal/pipe/blob/upload.go
index 445434a3..457a8511 100644
--- a/internal/pipe/blob/upload.go
+++ b/internal/pipe/blob/upload.go
@@ -232,11 +232,9 @@ func (u *productionUploader) Upload(ctx *context.Context, filepath string, data
 	if err != nil {
 		return err
 	}
-	defer func() {
-		if cerr := w.Close(); err == nil {
-			err = cerr
-		}
-	}()
-	_, err = w.Write(data)
-	return err
+	defer func() { _ = w.Close() }()
+	if _, err = w.Write(data); err != nil {
+		return err
+	}
+	return w.Close()
 }
diff --git a/internal/pipe/brew/brew.go b/internal/pipe/brew/brew.go
index 7bc32065..6a483525 100644
--- a/internal/pipe/brew/brew.go
+++ b/internal/pipe/brew/brew.go
@@ -108,8 +108,10 @@ func publishAll(ctx *context.Context, cli client.Client) error {
 }
 
 func doPublish(ctx *context.Context, formula *artifact.Artifact, cl client.Client) error {
-	brew := formula.Extra[brewConfigExtra].(config.Homebrew)
-	var err error
+	brew, err := artifact.Extra[config.Homebrew](*formula, brewConfigExtra)
+	if err != nil {
+		return err
+	}
 	cl, err = client.NewIfToken(ctx, cl, brew.Tap.Token)
 	if err != nil {
 		return err
@@ -295,10 +297,10 @@ func installs(cfg config.Homebrew, art *artifact.Artifact) []string {
 	switch art.Type {
 	case artifact.UploadableBinary:
 		name := art.Name
-		bin := art.ExtraOr(artifact.ExtraBinary, art.Name).(string)
+		bin := artifact.ExtraOr(*art, artifact.ExtraBinary, art.Name)
 		install[fmt.Sprintf("bin.install %q => %q", name, bin)] = true
 	case artifact.UploadableArchive:
-		for _, bin := range art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string) {
+		for _, bin := range artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{}) {
 			install[fmt.Sprintf("bin.install %q", bin)] = true
 		}
 	}
diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go
index c01c5bc6..5dd7d992 100644
--- a/internal/pipe/docker/docker.go
+++ b/internal/pipe/docker/docker.go
@@ -88,13 +88,18 @@ func validateImager(use string) error {
 
 // Publish the docker images.
 func (Pipe) Publish(ctx *context.Context) error {
+	skips := pipe.SkipMemento{}
 	images := ctx.Artifacts.Filter(artifact.ByType(artifact.PublishableDockerImage)).List()
 	for _, image := range images {
 		if err := dockerPush(ctx, image); err != nil {
+			if pipe.IsSkip(err) {
+				skips.Remember(err)
+				continue
+			}
 			return err
 		}
 	}
-	return nil
+	return skips.Evaluate()
 }
 
 // Run the pipe.
@@ -187,15 +192,6 @@ func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.A
 		return err
 	}
 
-	if strings.TrimSpace(docker.SkipPush) == "true" {
-		return pipe.Skip("docker.skip_push is set")
-	}
-	if ctx.SkipPublish {
-		return pipe.ErrSkipPublishEnabled
-	}
-	if strings.TrimSpace(docker.SkipPush) == "auto" && ctx.Semver.Prerelease != "" {
-		return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish")
-	}
 	for _, img := range images {
 		ctx.Artifacts.Add(&artifact.Artifact{
 			Type:   artifact.PublishableDockerImage,
@@ -245,10 +241,23 @@ func processBuildFlagTemplates(ctx *context.Context, docker config.Docker) ([]st
 
 func dockerPush(ctx *context.Context, image *artifact.Artifact) error {
 	log.WithField("image", image.Name).Info("pushing")
-	docker := image.Extra[dockerConfigExtra].(config.Docker)
+
+	docker, err := artifact.Extra[config.Docker](*image, dockerConfigExtra)
+	if err != nil {
+		return err
+	}
+
+	if strings.TrimSpace(docker.SkipPush) == "true" {
+		return pipe.Skip("docker.skip_push is set: " + image.Name)
+	}
+	if strings.TrimSpace(docker.SkipPush) == "auto" && ctx.Semver.Prerelease != "" {
+		return pipe.Skip("prerelease detected with 'auto' push, skipping docker publish: " + image.Name)
+	}
+
 	if err := imagers[docker.Use].Push(ctx, image.Name, docker.PushFlags); err != nil {
 		return err
 	}
+
 	art := &artifact.Artifact{
 		Type:   artifact.DockerImage,
 		Name:   image.Name,
diff --git a/internal/pipe/gofish/gofish.go b/internal/pipe/gofish/gofish.go
index d43c6742..03cfa6bd 100644
--- a/internal/pipe/gofish/gofish.go
+++ b/internal/pipe/gofish/gofish.go
@@ -229,7 +229,7 @@ func dataFor(ctx *context.Context, cfg config.GoFish, cl client.Client, artifact
 			}
 			switch art.Type {
 			case artifact.UploadableArchive:
-				for _, bin := range art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string) {
+				for _, bin := range artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{}) {
 					releasePackage.Binaries = append(releasePackage.Binaries, binary{
 						Name:   bin,
 						Target: bin,
@@ -238,7 +238,7 @@ func dataFor(ctx *context.Context, cfg config.GoFish, cl client.Client, artifact
 			case artifact.UploadableBinary:
 				releasePackage.Binaries = append(releasePackage.Binaries, binary{
 					Name:   art.Name,
-					Target: art.ExtraOr(artifact.ExtraBinary, art.Name).(string),
+					Target: artifact.ExtraOr(*art, artifact.ExtraBinary, art.Name),
 				})
 			}
 			result.ReleasePackages = append(result.ReleasePackages, releasePackage)
@@ -273,8 +273,11 @@ func publishAll(ctx *context.Context, cli client.Client) error {
 }
 
 func doPublish(ctx *context.Context, food *artifact.Artifact, cl client.Client) error {
-	rig := food.Extra[goFishConfigExtra].(config.GoFish)
-	var err error
+	rig, err := artifact.Extra[config.GoFish](*food, goFishConfigExtra)
+	if err != nil {
+		return err
+	}
+
 	cl, err = client.NewIfToken(ctx, cl, rig.Rig.Token)
 	if err != nil {
 		return err
diff --git a/internal/pipe/krew/krew.go b/internal/pipe/krew/krew.go
index ed3ebbc0..71f22f96 100644
--- a/internal/pipe/krew/krew.go
+++ b/internal/pipe/krew/krew.go
@@ -233,7 +233,7 @@ func manifestFor(ctx *context.Context, cfg config.Krew, cl client.Client, artifa
 		}
 
 		for _, arch := range goarch {
-			bins := art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string)
+			bins := artifact.ExtraOr(*art, artifact.ExtraBinaries, []string{})
 			if len(bins) != 1 {
 				return result, fmt.Errorf("krew: only one binary per archive allowed, got %d on %q", len(bins), art.Name)
 			}
@@ -283,8 +283,11 @@ func publishAll(ctx *context.Context, cli client.Client) error {
 }
 
 func doPublish(ctx *context.Context, manifest *artifact.Artifact, cl client.Client) error {
-	cfg := manifest.Extra[krewConfigExtra].(config.Krew)
-	var err error
+	cfg, err := artifact.Extra[config.Krew](*manifest, krewConfigExtra)
+	if err != nil {
+		return err
+	}
+
 	cl, err = client.NewIfToken(ctx, cl, cfg.Index.Token)
 	if err != nil {
 		return err
diff --git a/internal/pipe/scoop/scoop.go b/internal/pipe/scoop/scoop.go
index 2dcd9881..284d19aa 100644
--- a/internal/pipe/scoop/scoop.go
+++ b/internal/pipe/scoop/scoop.go
@@ -120,9 +120,12 @@ func doPublish(ctx *context.Context, cl client.Client) error {
 	}
 
 	manifest := manifests[0]
-	scoop := manifest.Extra[scoopConfigExtra].(config.Scoop)
 
-	var err error
+	scoop, err := artifact.Extra[config.Scoop](*manifest, scoopConfigExtra)
+	if err != nil {
+		return err
+	}
+
 	cl, err = client.NewIfToken(ctx, cl, scoop.Bucket.Token)
 	if err != nil {
 		return err
@@ -251,9 +254,14 @@ func dataFor(ctx *context.Context, cl client.Client, artifacts []*artifact.Artif
 			"sum":              sum,
 		}).Debug("scoop url templating")
 
+		binaries, err := binaries(*artifact)
+		if err != nil {
+			return manifest, err
+		}
+
 		manifest.Architecture[arch] = Resource{
 			URL:  url,
-			Bin:  binaries(artifact),
+			Bin:  binaries,
 			Hash: sum,
 		}
 	}
@@ -261,12 +269,16 @@ func dataFor(ctx *context.Context, cl client.Client, artifacts []*artifact.Artif
 	return manifest, nil
 }
 
-func binaries(a *artifact.Artifact) []string {
+func binaries(a artifact.Artifact) ([]string, error) {
 	// nolint: prealloc
 	var bins []string
-	wrap := a.ExtraOr(artifact.ExtraWrappedIn, "").(string)
-	for _, b := range a.ExtraOr(artifact.ExtraBuilds, []*artifact.Artifact{}).([]*artifact.Artifact) {
+	wrap := artifact.ExtraOr(a, artifact.ExtraWrappedIn, "")
+	builds, err := artifact.Extra[[]artifact.Artifact](a, artifact.ExtraBuilds)
+	if err != nil {
+		return nil, err
+	}
+	for _, b := range builds {
 		bins = append(bins, filepath.Join(wrap, b.Name))
 	}
-	return bins
+	return bins, nil
 }
diff --git a/internal/pipe/snapcraft/snapcraft.go b/internal/pipe/snapcraft/snapcraft.go
index 67a3beb1..3c44d2f4 100644
--- a/internal/pipe/snapcraft/snapcraft.go
+++ b/internal/pipe/snapcraft/snapcraft.go
@@ -418,7 +418,7 @@ const (
 
 func push(ctx *context.Context, snap *artifact.Artifact) error {
 	log := log.WithField("snap", snap.Name)
-	releases := snap.Extra[releasesExtra].([]string)
+	releases := artifact.ExtraOr(*snap, releasesExtra, []string{})
 	/* #nosec */
 	cmd := exec.CommandContext(ctx, "snapcraft", "upload", "--release="+strings.Join(releases, ","), snap.Path)
 	log.WithField("args", cmd.Args).Info("pushing snap")
diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go
index c41b569a..6000db49 100644
--- a/internal/tmpl/tmpl.go
+++ b/internal/tmpl/tmpl.go
@@ -139,16 +139,12 @@ func (t *Template) WithExtraFields(f Fields) *Template {
 
 // WithArtifact populates Fields from the artifact and replacements.
 func (t *Template) WithArtifact(a *artifact.Artifact, replacements map[string]string) *Template {
-	bin := a.Extra[binary]
-	if bin == nil {
-		bin = t.fields[projectName]
-	}
 	t.fields[osKey] = replace(replacements, a.Goos)
 	t.fields[arch] = replace(replacements, a.Goarch)
 	t.fields[arm] = replace(replacements, a.Goarm)
 	t.fields[mips] = replace(replacements, a.Gomips)
 	t.fields[amd64] = replace(replacements, a.Goamd64)
-	t.fields[binary] = bin.(string)
+	t.fields[binary] = artifact.ExtraOr(*a, binary, t.fields[projectName].(string))
 	t.fields[artifactName] = a.Name
 	t.fields[artifactPath] = a.Path
 	return t
diff --git a/www/docs/cmd/goreleaser.md b/www/docs/cmd/goreleaser.md
index ad6f6759..f22f4ee0 100644
--- a/www/docs/cmd/goreleaser.md
+++ b/www/docs/cmd/goreleaser.md
@@ -9,7 +9,9 @@ Its goal is to simplify the build, release and publish steps while providing var
 
 GoReleaser is built for CI tools, you only need to download and execute it in your build script. Of course, you can also install it locally if you wish.
 
-You can also customize your entire release process through a single .goreleaser.yaml file.
+You can customize your entire release process through a single .goreleaser.yaml file.
+
+Check out our website for more information, examples and documentation: https://goreleaser.com
 
 
 ## Options
diff --git a/www/docs/cmd/goreleaser_build.md b/www/docs/cmd/goreleaser_build.md
index 3156cbae..1ecf5443 100644
--- a/www/docs/cmd/goreleaser_build.md
+++ b/www/docs/cmd/goreleaser_build.md
@@ -27,6 +27,8 @@ goreleaser build [flags]
   -p, --parallelism int    Amount tasks to run concurrently (default: number of CPUs)
       --rm-dist            Remove the dist folder before building
       --single-target      Builds only for current GOOS and GOARCH
+      --skip-after         Skips global after hooks
+      --skip-before        Skips global before hooks
       --skip-post-hooks    Skips all post-build hooks
       --skip-validate      Skips several sanity checks
       --snapshot           Generate an unversioned snapshot build, skipping all validations
diff --git a/www/docs/cmd/goreleaser_release.md b/www/docs/cmd/goreleaser_release.md
index 8506c855..d2c46aa8 100644
--- a/www/docs/cmd/goreleaser_release.md
+++ b/www/docs/cmd/goreleaser_release.md
@@ -22,8 +22,11 @@ goreleaser release [flags]
       --release-notes string         Load custom release notes from a markdown file (will skip GoReleaser changelog generation)
       --release-notes-tmpl string    Load custom release notes from a templated markdown file (overrides --release-notes)
       --rm-dist                      Removes the dist folder
+      --skip-after                   Skips global after hooks
       --skip-announce                Skips announcing releases (implies --skip-validate)
-      --skip-publish                 Skips publishing artifacts
+      --skip-before                  Skips global before hooks
+      --skip-docker                  Skips Docker Images/Manifests builds
+      --skip-publish                 Skips publishing artifacts (implies --skip-announce)
       --skip-sbom                    Skips cataloging artifacts
       --skip-sign                    Skips signing artifacts
       --skip-validate                Skips git checks
diff --git a/www/docs/customization/archive.md b/www/docs/customization/archive.md
index 53700eff..83d1ee80 100644
--- a/www/docs/customization/archive.md
+++ b/www/docs/customization/archive.md
@@ -27,7 +27,7 @@ archives:
 
     # This will create an archive without any binaries, only the files are there.
     # The name template must not contain any references to `Os`, `Arch` and etc, since the archive will be meta.
-    # Defaul is false.
+    # Default is false.
     meta: true
 
     # Archive name template.
diff --git a/www/docs/errors/no-main.md b/www/docs/errors/no-main.md
index 1d4394fd..f64fe257 100644
--- a/www/docs/errors/no-main.md
+++ b/www/docs/errors/no-main.md
@@ -31,3 +31,7 @@ builds:
 ```
 
 For more info, check the [builds documentation](/customization/build/).
+
+## If you ran goreleaser outside the root of the project
+
+Run goreleaser in the root of the project.


Test patch:
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
index 0f4860c2..c3e3452c 100644
--- a/internal/artifact/artifact_test.go
+++ b/internal/artifact/artifact_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/golden"
+	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/sync/errgroup"
@@ -359,14 +360,58 @@ func TestInvalidAlgorithm(t *testing.T) {
 	require.Empty(t, sum)
 }
 
-func TestExtraOr(t *testing.T) {
-	a := &Artifact{
+func TestExtra(t *testing.T) {
+	a := Artifact{
 		Extra: map[string]interface{}{
 			"Foo": "foo",
+			"docker": config.Docker{
+				ID:  "id",
+				Use: "docker",
+			},
+			"fail-plz": config.Homebrew{
+				Plist: "aaaa",
+			},
+			"unsupported": func() {},
+			"binaries":    []string{"foo", "bar"},
 		},
 	}
-	require.Equal(t, "foo", a.ExtraOr("Foo", "bar"))
-	require.Equal(t, "bar", a.ExtraOr("Foobar", "bar"))
+
+	t.Run("string", func(t *testing.T) {
+		foo, err := Extra[string](a, "Foo")
+		require.NoError(t, err)
+		require.Equal(t, "foo", foo)
+		require.Equal(t, "foo", ExtraOr(a, "Foo", "bar"))
+	})
+
+	t.Run("missing field", func(t *testing.T) {
+		bar, err := Extra[string](a, "Foobar")
+		require.NoError(t, err)
+		require.Equal(t, "", bar)
+		require.Equal(t, "bar", ExtraOr(a, "Foobar", "bar"))
+	})
+
+	t.Run("complex", func(t *testing.T) {
+		docker, err := Extra[config.Docker](a, "docker")
+		require.NoError(t, err)
+		require.Equal(t, "id", docker.ID)
+	})
+
+	t.Run("array", func(t *testing.T) {
+		binaries, err := Extra[[]string](a, "binaries")
+		require.NoError(t, err)
+		require.Equal(t, []string{"foo", "bar"}, binaries)
+		require.Equal(t, []string{"foo", "bar"}, ExtraOr(a, "binaries", []string{}))
+	})
+
+	t.Run("unmarshal error", func(t *testing.T) {
+		_, err := Extra[config.Docker](a, "fail-plz")
+		require.EqualError(t, err, "json: unknown field \"Name\"")
+	})
+
+	t.Run("marshal error", func(t *testing.T) {
+		_, err := Extra[config.Docker](a, "unsupported")
+		require.EqualError(t, err, "json: unsupported type: func()")
+	})
 }
 
 func TestByIDs(t *testing.T) {
@@ -508,15 +553,6 @@ func TestRefresher(t *testing.T) {
 				},
 			},
 		})
-		artifacts.Add(&Artifact{
-			Name: "invalid",
-			Type: Checksum,
-			Extra: map[string]interface{}{
-				"Refresh": func() {
-					t.Fatalf("should not have been called")
-				},
-			},
-		})
 		artifacts.Add(&Artifact{
 			Name: "no refresh",
 			Type: Checksum,
diff --git a/internal/pipe/archive/archive_test.go b/internal/pipe/archive/archive_test.go
index fa3685f1..373d3e95 100644
--- a/internal/pipe/archive/archive_test.go
+++ b/internal/pipe/archive/archive_test.go
@@ -175,8 +175,8 @@ func TestRunPipe(t *testing.T) {
 					expectBin += ".exe"
 				}
 				require.Equal(t, "myid", arch.ID(), "all archives must have the archive ID set")
-				require.Equal(t, []string{expectBin}, arch.ExtraOr(artifact.ExtraBinaries, []string{}).([]string))
-				require.Equal(t, "", arch.ExtraOr(artifact.ExtraBinary, "").(string))
+				require.Equal(t, []string{expectBin}, artifact.ExtraOr(*arch, artifact.ExtraBinaries, []string{}))
+				require.Equal(t, "", artifact.ExtraOr(*arch, artifact.ExtraBinary, ""))
 			}
 			require.Len(t, archives, 7)
 			// TODO: should verify the artifact fields here too
@@ -426,14 +426,14 @@ func TestRunPipeBinary(t *testing.T) {
 		artifact.ByGoos("darwin"),
 		artifact.ByGoarch("all"),
 	)).List()[0]
-	require.True(t, darwinUniversal.ExtraOr(artifact.ExtraReplaces, false).(bool))
+	require.True(t, artifact.ExtraOr(*darwinUniversal, artifact.ExtraReplaces, false))
 	windows := binaries.Filter(artifact.ByGoos("windows")).List()[0]
 	require.Equal(t, "mybin_0.0.1_darwin_amd64", darwinThin.Name)
-	require.Equal(t, "mybin", darwinThin.ExtraOr(artifact.ExtraBinary, ""))
+	require.Equal(t, "mybin", artifact.ExtraOr(*darwinThin, artifact.ExtraBinary, ""))
 	require.Equal(t, "myunibin_0.0.1_darwin_all", darwinUniversal.Name)
-	require.Equal(t, "myunibin", darwinUniversal.ExtraOr(artifact.ExtraBinary, ""))
+	require.Equal(t, "myunibin", artifact.ExtraOr(*darwinUniversal, artifact.ExtraBinary, ""))
 	require.Equal(t, "mybin_0.0.1_windows_amd64.exe", windows.Name)
-	require.Equal(t, "mybin.exe", windows.ExtraOr(artifact.ExtraBinary, ""))
+	require.Equal(t, "mybin.exe", artifact.ExtraOr(*windows, artifact.ExtraBinary, ""))
 }
 
 func TestRunPipeDistRemoved(t *testing.T) {
@@ -659,7 +659,7 @@ func TestRunPipeWrap(t *testing.T) {
 
 	archives := ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableArchive)).List()
 	require.Len(t, archives, 1)
-	require.Equal(t, "foo_macOS", archives[0].ExtraOr(artifact.ExtraWrappedIn, ""))
+	require.Equal(t, "foo_macOS", artifact.ExtraOr(*archives[0], artifact.ExtraWrappedIn, ""))
 
 	// Check archive contents
 	f, err = os.Open(filepath.Join(dist, "foo.tar.gz"))
@@ -817,13 +817,13 @@ func TestBinaryOverride(t *testing.T) {
 			darwin := archives.Filter(artifact.ByGoos("darwin")).List()[0]
 			require.Equal(t, "foobar_0.0.1_darwin_amd64."+format, darwin.Name)
 			require.Equal(t, format, darwin.Format())
-			require.Empty(t, darwin.ExtraOr(artifact.ExtraWrappedIn, ""))
+			require.Empty(t, artifact.ExtraOr(*darwin, artifact.ExtraWrappedIn, ""))
 
 			archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableBinary))
 			windows := archives.Filter(artifact.ByGoos("windows")).List()[0]
 			require.Equal(t, "foobar_0.0.1_windows_amd64.exe", windows.Name)
-			require.Empty(t, windows.ExtraOr(artifact.ExtraWrappedIn, ""))
-			require.Equal(t, "mybin.exe", windows.ExtraOr(artifact.ExtraBinary, ""))
+			require.Empty(t, artifact.ExtraOr(*windows, artifact.ExtraWrappedIn, ""))
+			require.Equal(t, "mybin.exe", artifact.ExtraOr(*windows, artifact.ExtraBinary, ""))
 		})
 	}
 }
diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go
index ab656b9c..584bb622 100644
--- a/internal/pipe/docker/docker_test.go
+++ b/internal/pipe/docker/docker_test.go
@@ -655,8 +655,10 @@ func TestRunPipe(t *testing.T) {
 			expect: []string{
 				registry + "goreleaser/test_run_pipe:latest",
 			},
-			assertImageLabels: noLabels,
-			assertError:       testlib.AssertSkipped,
+			assertImageLabels:   noLabels,
+			assertError:         shouldNotErr,
+			pubAssertError:      testlib.AssertSkipped,
+			manifestAssertError: shouldNotErr,
 		},
 		"one_img_error_with_skip_push": {
 			dockers: []config.Docker{
@@ -839,8 +841,10 @@ func TestRunPipe(t *testing.T) {
 				registry + "goreleaser/mybin:v1.0.0-123",
 				registry + "goreleaser/mybin:latest",
 			},
-			assertImageLabels: noLabels,
-			assertError:       testlib.AssertSkipped,
+			assertImageLabels:   noLabels,
+			assertError:         shouldNotErr,
+			pubAssertError:      testlib.AssertSkipped,
+			manifestAssertError: shouldNotErr,
 		},
 		"no_permissions": {
 			dockers: []config.Docker{
diff --git a/internal/pipe/nfpm/nfpm_test.go b/internal/pipe/nfpm/nfpm_test.go
index 5abc644a..26c8b62f 100644
--- a/internal/pipe/nfpm/nfpm_test.go
+++ b/internal/pipe/nfpm/nfpm_test.go
@@ -242,7 +242,7 @@ func TestRunPipe(t *testing.T) {
 			"./testdata/folder",
 			"./testdata/testfile-" + pkg.Goarch + pkg.Goamd64 + pkg.Goarm + pkg.Gomips + ".txt",
 			binPath,
-		}, sources(pkg.ExtraOr(extraFiles, files.Contents{}).(files.Contents)))
+		}, sources(artifact.ExtraOr(*pkg, extraFiles, files.Contents{})))
 		require.ElementsMatch(t, []string{
 			"/var/log/foobar",
 			"/usr/share/testfile.txt",
@@ -253,7 +253,7 @@ func TestRunPipe(t *testing.T) {
 			"/etc/nope3_mybin.conf",
 			"/etc/folder",
 			"/usr/bin/subdir/mybin",
-		}, destinations(pkg.ExtraOr(extraFiles, files.Contents{}).(files.Contents)))
+		}, destinations(artifact.ExtraOr(*pkg, extraFiles, files.Contents{})))
 	}
 	require.Len(t, ctx.Config.NFPMs[0].Contents, 8, "should not modify the config file list")
 }
@@ -389,8 +389,8 @@ func TestRunPipeConventionalNameTemplate(t *testing.T) {
 			"foo_1.0.0_x86_64v4.apk",
 		}, pkg.Name, "package name is not expected")
 		require.Equal(t, "someid", pkg.ID())
-		require.ElementsMatch(t, []string{binPath}, sources(pkg.ExtraOr(extraFiles, files.Contents{}).(files.Contents)))
-		require.ElementsMatch(t, []string{"/usr/bin/subdir/mybin"}, destinations(pkg.ExtraOr(extraFiles, files.Contents{}).(files.Contents)))
+		require.ElementsMatch(t, []string{binPath}, sources(artifact.ExtraOr(*pkg, extraFiles, files.Contents{})))
+		require.ElementsMatch(t, []string{"/usr/bin/subdir/mybin"}, destinations(artifact.ExtraOr(*pkg, extraFiles, files.Contents{})))
 	}
 }
 
@@ -1207,7 +1207,7 @@ func TestMeta(t *testing.T) {
 			"/usr/share/testfile.txt",
 			"/etc/nope.conf",
 			"/etc/nope-rpm.conf",
-		}, destinations(pkg.ExtraOr(extraFiles, files.Contents{}).(files.Contents)))
+		}, destinations(artifact.ExtraOr(*pkg, extraFiles, files.Contents{})))
 	}
 
 	require.Len(t, ctx.Config.NFPMs[0].Contents, 4, "should not modify the config file list")
@@ -1355,7 +1355,7 @@ func TestBinDirTemplating(t *testing.T) {
 		// the final binary should contain the evaluated bindir (after template eval)
 		require.ElementsMatch(t, []string{
 			"/usr/lib/pro/nagios/plugins/subdir/mybin",
-		}, destinations(pkg.ExtraOr(extraFiles, files.Contents{}).(files.Contents)))
+		}, destinations(artifact.ExtraOr(*pkg, extraFiles, files.Contents{})))
 	}
 }
 
diff --git a/internal/pipe/universalbinary/universalbinary_test.go b/internal/pipe/universalbinary/universalbinary_test.go
index 719a1d7b..7961a207 100644
--- a/internal/pipe/universalbinary/universalbinary_test.go
+++ b/internal/pipe/universalbinary/universalbinary_test.go
@@ -255,7 +255,7 @@ func TestRun(t *testing.T) {
 		unis := ctx1.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List()
 		require.Len(t, unis, 1)
 		checkUniversalBinary(t, unis[0])
-		require.True(t, unis[0].Extra[artifact.ExtraReplaces].(bool))
+		require.True(t, artifact.ExtraOr(*unis[0], artifact.ExtraReplaces, false))
 	})
 
 	t.Run("keeping", func(t *testing.T) {
@@ -264,7 +264,7 @@ func TestRun(t *testing.T) {
 		unis := ctx2.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List()
 		require.Len(t, unis, 1)
 		checkUniversalBinary(t, unis[0])
-		require.False(t, unis[0].Extra[artifact.ExtraReplaces].(bool))
+		require.False(t, artifact.ExtraOr(*unis[0], artifact.ExtraReplaces, true))
 	})
 
 	t.Run("bad template", func(t *testing.T) {