Repository: goreleaser/goreleaser
Pull request #3191 (2022-06-24 01:18:35)

refactor: improve handling of extra fields in artifacts
**Backporting from pro**

In the end of the process we store the artifact list as `dist/artifacts.json`. If we want to load this back into a goreleaser context and use them, some pipes that store "complex" data (e.g. their og config) in the `Extra` field of the artifact will fail, as they will be parsed back from json as `map[string]interface{}`.

With this, we can use the new `artifact.Extra`, which will try to cast, and if it fails, convert back to json and re-unmarshal into the correct type.

This is needed for #3174 in the pro distribution and is being backported from it to the OSS distribution to avoid merge conflicts.


Patch:
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/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 25c414f6..5dd7d992 100644
--- a/internal/pipe/docker/docker.go
+++ b/internal/pipe/docker/docker.go
@@ -242,7 +242,11 @@ 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)
 	}
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/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/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) {