Repository: goreleaser/goreleaser
Pull request #2747 (2021-12-07 20:14:54)

feat: improve universal binaries on krew/brew/gofish
only use universal binaries on brew/krew/gofish if they replace single-arch ones, otherwise, use the platform-specific ones.

closes https://github.com/goreleaser/goreleaser/issues/2641

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2747?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2747](https://codecov.io/gh/goreleaser/goreleaser/pull/2747?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (17bac6d) into [main](https://codecov.io/gh/goreleaser/goreleaser/commit/5025a363ff707e0832bb5981d7249dbf794db455?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (5025a36) will **increase** coverage by `0.00%`.
> The diff coverage is `90.00%`.

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

```diff
@@           Coverage Diff           @@
##             main    #2747   +/-   ##
=======================================
  Coverage   84.93%   84.93%           
=======================================
  Files         105      105           
  Lines        7977     7992   +15     
=======================================
+ Hits         6775     6788   +13     
- Misses        984      986    +2     
  Partials      218      218           
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2747?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/2747/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvYXJ0aWZhY3QvYXJ0aWZhY3QuZ28=) | `97.69% <50.00%> (-0.90%)` | :arrow_down: |
| [internal/pipe/archive/archive.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2747/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9hcmNoaXZlL2FyY2hpdmUuZ28=) | `94.44% <100.00%> (+0.04%)` | :arrow_up: |
| [internal/pipe/brew/brew.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2747/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9icmV3L2JyZXcuZ28=) | `84.84% <100.00%> (+0.05%)` | :arrow_up: |
| [internal/pipe/gofish/gofish.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2747/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9nb2Zpc2gvZ29maXNoLmdv) | `77.72% <100.00%> (+0.09%)` | :arrow_up: |
| [internal/pipe/krew/krew.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2747/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9rcmV3L2tyZXcuZ28=) | `85.65% <100.00%> (+0.06%)` | :arrow_up: |
| [internal/pipe/universalbinary/universalbinary.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2747/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS91bml2ZXJzYWxiaW5hcnkvdW5pdmVyc2FsYmluYXJ5Lmdv) | `84.41% <100.00%> (+0.63%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2747?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/2747?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [5025a36...17bac6d](https://codecov.io/gh/goreleaser/goreleaser/pull/2747?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/.goreleaser.yaml b/.goreleaser.yaml
index b33b3c7c..ea62be7d 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -34,7 +34,7 @@ builds:
     - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser
 
 universal_binaries:
-- replace: true
+- replace: false
 
 checksum:
   name_template: 'checksums.txt'
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index 521aa8ef..19784e0c 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -24,7 +24,7 @@ type Type int
 
 const (
 	// UploadableArchive a tar.gz/zip archive to be uploaded.
-	UploadableArchive Type = iota
+	UploadableArchive Type = iota + 1
 	// UploadableBinary is a binary file to be uploaded.
 	UploadableBinary
 	// UploadableFile is any file that can be uploaded.
@@ -113,6 +113,7 @@ const (
 	ExtraWrappedIn = "WrappedIn"
 	ExtraBinaries  = "Binaries"
 	ExtraRefresh   = "Refresh"
+	ExtraReplaces  = "Replaces"
 )
 
 // Extras represents the extra fields in an artifact.
@@ -143,6 +144,10 @@ type Artifact struct {
 	Extra  Extras `json:"extra,omitempty"`
 }
 
+func (a Artifact) String() string {
+	return a.Name
+}
+
 // 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{} {
@@ -287,6 +292,13 @@ func (artifacts *Artifacts) Remove(filter Filter) error {
 // function.
 type Filter func(a *Artifact) bool
 
+// OnlyReplacingUnibins removes universal binaries that did not replace the single-arch ones.
+//
+// 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)
+}
+
 // ByGoos is a predefined filter that filters by the given goos.
 func ByGoos(s string) Filter {
 	return func(a *Artifact) bool {
diff --git a/internal/pipe/archive/archive.go b/internal/pipe/archive/archive.go
index b3afb11c..77e45c13 100644
--- a/internal/pipe/archive/archive.go
+++ b/internal/pipe/archive/archive.go
@@ -198,6 +198,7 @@ func create(ctx *context.Context, arch config.Archive, binaries []*artifact.Arti
 			artifact.ExtraFormat:    arch.Format,
 			artifact.ExtraWrappedIn: wrap,
 			artifact.ExtraBinaries:  bins,
+			artifact.ExtraReplaces:  binaries[0].Extra[artifact.ExtraReplaces],
 		},
 	})
 	return nil
@@ -235,10 +236,11 @@ func skip(ctx *context.Context, archive config.Archive, binaries []*artifact.Art
 			Goarm:  binary.Goarm,
 			Gomips: binary.Gomips,
 			Extra: map[string]interface{}{
-				artifact.ExtraBuilds: []*artifact.Artifact{binary},
-				artifact.ExtraID:     archive.ID,
-				artifact.ExtraFormat: archive.Format,
-				artifact.ExtraBinary: binary.Name,
+				artifact.ExtraBuilds:   []*artifact.Artifact{binary},
+				artifact.ExtraID:       archive.ID,
+				artifact.ExtraFormat:   archive.Format,
+				artifact.ExtraBinary:   binary.Name,
+				artifact.ExtraReplaces: binaries[0].Extra[artifact.ExtraReplaces],
 			},
 		})
 	}
diff --git a/internal/pipe/brew/brew.go b/internal/pipe/brew/brew.go
index d030345f..4c5e4f3c 100644
--- a/internal/pipe/brew/brew.go
+++ b/internal/pipe/brew/brew.go
@@ -170,6 +170,7 @@ func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
 			),
 			artifact.ByType(artifact.UploadableBinary),
 		),
+		artifact.OnlyReplacingUnibins,
 	}
 	if len(brew.IDs) > 0 {
 		filters = append(filters, artifact.ByIDs(brew.IDs...))
diff --git a/internal/pipe/gofish/gofish.go b/internal/pipe/gofish/gofish.go
index f0efc3d4..aaf0dea9 100644
--- a/internal/pipe/gofish/gofish.go
+++ b/internal/pipe/gofish/gofish.go
@@ -100,6 +100,7 @@ func doRun(ctx *context.Context, goFish config.GoFish, cl client.Client) error {
 			artifact.ByType(artifact.UploadableArchive),
 			artifact.ByType(artifact.UploadableBinary),
 		),
+		artifact.OnlyReplacingUnibins,
 	}
 	if len(goFish.IDs) > 0 {
 		filters = append(filters, artifact.ByIDs(goFish.IDs...))
diff --git a/internal/pipe/krew/krew.go b/internal/pipe/krew/krew.go
index cfebd835..e0f3c649 100644
--- a/internal/pipe/krew/krew.go
+++ b/internal/pipe/krew/krew.go
@@ -105,6 +105,7 @@ func doRun(ctx *context.Context, krew config.Krew, cl client.Client) error {
 			),
 		),
 		artifact.ByType(artifact.UploadableArchive),
+		artifact.OnlyReplacingUnibins,
 	}
 	if len(krew.IDs) > 0 {
 		filters = append(filters, artifact.ByIDs(krew.IDs...))
diff --git a/internal/pipe/universalbinary/universalbinary.go b/internal/pipe/universalbinary/universalbinary.go
index cc67fc26..b7638f05 100644
--- a/internal/pipe/universalbinary/universalbinary.go
+++ b/internal/pipe/universalbinary/universalbinary.go
@@ -206,13 +206,19 @@ func makeUniversalBinary(ctx *context.Context, unibin config.UniversalBinary) er
 		return fmt.Errorf("failed to close file: %w", err)
 	}
 
+	extra := map[string]interface{}{}
+	for k, v := range binaries[0].Extra {
+		extra[k] = v
+	}
+	extra[artifact.ExtraReplaces] = unibin.Replace
+
 	ctx.Artifacts.Add(&artifact.Artifact{
 		Type:   artifact.UniversalBinary,
 		Name:   name,
 		Path:   path,
 		Goos:   "darwin",
 		Goarch: "all",
-		Extra:  binaries[0].Extra,
+		Extra:  extra,
 	})
 
 	return nil


Test patch:
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
index 827b5e78..56cacd79 100644
--- a/internal/artifact/artifact_test.go
+++ b/internal/artifact/artifact_test.go
@@ -69,6 +69,22 @@ func TestFilter(t *testing.T) {
 			Name: "checkzumm",
 			Type: Checksum,
 		},
+		{
+			Name:   "unibin-replaces",
+			Goos:   "darwin",
+			Goarch: "all",
+			Extra: map[string]interface{}{
+				ExtraReplaces: true,
+			},
+		},
+		{
+			Name:   "unibin-noreplace",
+			Goos:   "darwin",
+			Goarch: "all",
+			Extra: map[string]interface{}{
+				ExtraReplaces: false,
+			},
+		},
 	}
 	artifacts := New()
 	for _, a := range data {
@@ -76,7 +92,7 @@ func TestFilter(t *testing.T) {
 	}
 
 	require.Len(t, artifacts.Filter(ByGoos("linux")).items, 1)
-	require.Len(t, artifacts.Filter(ByGoos("darwin")).items, 0)
+	require.Len(t, artifacts.Filter(ByGoos("darwin")).items, 2)
 
 	require.Len(t, artifacts.Filter(ByGoarch("amd64")).items, 1)
 	require.Len(t, artifacts.Filter(ByGoarch("386")).items, 0)
@@ -87,7 +103,10 @@ func TestFilter(t *testing.T) {
 	require.Len(t, artifacts.Filter(ByType(Checksum)).items, 2)
 	require.Len(t, artifacts.Filter(ByType(Binary)).items, 0)
 
-	require.Len(t, artifacts.Filter(nil).items, 5)
+	require.Len(t, artifacts.Filter(OnlyReplacingUnibins).items, 6)
+	require.Len(t, artifacts.Filter(And(OnlyReplacingUnibins, ByGoos("darwin"))).items, 1)
+
+	require.Len(t, artifacts.Filter(nil).items, 7)
 
 	require.Len(t, artifacts.Filter(
 		And(
@@ -501,6 +520,13 @@ func TestMarshalJSON(t *testing.T) {
 			ExtraID: "adsad",
 		},
 	})
+	artifacts.Add(&Artifact{
+		Name: "foo",
+		Type: UploadableArchive,
+		Extra: map[string]interface{}{
+			ExtraID: "adsad",
+		},
+	})
 	artifacts.Add(&Artifact{
 		Name: "foo",
 		Type: Checksum,
diff --git a/internal/artifact/testdata/TestMarshalJSON.json.golden b/internal/artifact/testdata/TestMarshalJSON.json.golden
index a0091ac8..4f1fa483 100644
--- a/internal/artifact/testdata/TestMarshalJSON.json.golden
+++ b/internal/artifact/testdata/TestMarshalJSON.json.golden
@@ -1,1 +1,1 @@
-[{"name":"foo","type":"Binary","extra":{"ID":"adsad"}},{"name":"foo","type":"Checksum","extra":{"Refresh":"func() error"}}]
\ No newline at end of file
+[{"name":"foo","type":"Binary","extra":{"ID":"adsad"}},{"name":"foo","type":"Archive","extra":{"ID":"adsad"}},{"name":"foo","type":"Checksum","extra":{"Refresh":"func() error"}}]
\ No newline at end of file
diff --git a/internal/pipe/archive/archive_test.go b/internal/pipe/archive/archive_test.go
index 16da35e9..6cc628c8 100644
--- a/internal/pipe/archive/archive_test.go
+++ b/internal/pipe/archive/archive_test.go
@@ -80,10 +80,11 @@ func TestRunPipe(t *testing.T) {
 				Goarch: "all",
 				Name:   "bin/mybin",
 				Path:   filepath.Join(dist, "darwinall", "bin", "mybin"),
-				Type:   artifact.Binary,
+				Type:   artifact.UniversalBinary,
 				Extra: map[string]interface{}{
-					artifact.ExtraBinary: "bin/mybin",
-					artifact.ExtraID:     "default",
+					artifact.ExtraBinary:   "bin/mybin",
+					artifact.ExtraID:       "default",
+					artifact.ExtraReplaces: true,
 				},
 			}
 			darwinBuild := &artifact.Artifact{
@@ -368,10 +369,11 @@ func TestRunPipeBinary(t *testing.T) {
 		Goarch: "all",
 		Name:   "myunibin",
 		Path:   filepath.Join(dist, "darwinamd64", "mybin"),
-		Type:   artifact.Binary,
+		Type:   artifact.UniversalBinary,
 		Extra: map[string]interface{}{
-			artifact.ExtraBinary: "myunibin",
-			artifact.ExtraID:     "default",
+			artifact.ExtraBinary:   "myunibin",
+			artifact.ExtraID:       "default",
+			artifact.ExtraReplaces: true,
 		},
 	})
 	ctx.Artifacts.Add(&artifact.Artifact{
@@ -397,6 +399,7 @@ func TestRunPipeBinary(t *testing.T) {
 		artifact.ByGoos("darwin"),
 		artifact.ByGoarch("all"),
 	)).List()[0]
+	require.True(t, darwinUniversal.ExtraOr(artifact.ExtraReplaces, false).(bool))
 	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, ""))
diff --git a/internal/pipe/brew/brew_test.go b/internal/pipe/brew/brew_test.go
index c8b60ef0..dfe6e7b7 100644
--- a/internal/pipe/brew/brew_test.go
+++ b/internal/pipe/brew/brew_test.go
@@ -1040,6 +1040,87 @@ func TestRunPipeUniversalBinary(t *testing.T) {
 			artifact.ExtraID:       "unibin",
 			artifact.ExtraFormat:   "tar.gz",
 			artifact.ExtraBinaries: []string{"unibin"},
+			artifact.ExtraReplaces: true,
+		},
+	})
+
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	client := client.NewMock()
+	distFile := filepath.Join(folder, "unibin.rb")
+
+	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)
+	require.NoError(t, err)
+	require.Equal(t, client.Content, string(distBts))
+}
+
+func TestRunPipeUniversalBinaryNotReplacing(t *testing.T) {
+	folder := t.TempDir()
+	ctx := &context.Context{
+		Git: context.GitInfo{
+			CurrentTag: "v1.0.1",
+		},
+		Version:   "1.0.1",
+		Artifacts: artifact.New(),
+		Config: config.Project{
+			Dist:        folder,
+			ProjectName: "unibin",
+			Brews: []config.Homebrew{
+				{
+					Name: "unibin",
+					Tap: config.RepoRef{
+						Owner: "unibin",
+						Name:  "bar",
+					},
+					IDs: []string{
+						"unibin",
+					},
+					Install: `bin.install "unibin"`,
+				},
+			},
+		},
+	}
+	path := filepath.Join(folder, "bin.tar.gz")
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin_amd64.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+		},
+	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin_arm64.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "arm64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+		},
+	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "all",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+			artifact.ExtraReplaces: false,
 		},
 	})
 
diff --git a/internal/pipe/brew/testdata/TestRunPipeUniversalBinaryNotReplacing.rb.golden b/internal/pipe/brew/testdata/TestRunPipeUniversalBinaryNotReplacing.rb.golden
new file mode 100644
index 00000000..563173ca
--- /dev/null
+++ b/internal/pipe/brew/testdata/TestRunPipeUniversalBinaryNotReplacing.rb.golden
@@ -0,0 +1,29 @@
+# typed: false
+# frozen_string_literal: true
+
+# This file was generated by GoReleaser. DO NOT EDIT.
+class Unibin < Formula
+  desc ""
+  homepage ""
+  version "1.0.1"
+  depends_on :macos
+
+  on_macos do
+    if Hardware::CPU.intel?
+      url "https://dummyhost/download/v1.0.1/bin_amd64.tar.gz"
+      sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+
+      def install
+        bin.install "unibin"
+      end
+    end
+    if Hardware::CPU.arm?
+      url "https://dummyhost/download/v1.0.1/bin_arm64.tar.gz"
+      sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+
+      def install
+        bin.install "unibin"
+      end
+    end
+  end
+end
diff --git a/internal/pipe/gofish/gofish_test.go b/internal/pipe/gofish/gofish_test.go
index 02e59873..2f3c52df 100644
--- a/internal/pipe/gofish/gofish_test.go
+++ b/internal/pipe/gofish/gofish_test.go
@@ -277,6 +277,86 @@ func TestRunPipeUniversalBinary(t *testing.T) {
 			artifact.ExtraID:       "unibin",
 			artifact.ExtraFormat:   "tar.gz",
 			artifact.ExtraBinaries: []string{"unibin"},
+			artifact.ExtraReplaces: true,
+		},
+	})
+
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	client := client.NewMock()
+	distFile := filepath.Join(folder, "unibin.lua")
+
+	require.NoError(t, runAll(ctx, client))
+	require.NoError(t, publishAll(ctx, client))
+	require.True(t, client.CreatedFile)
+	golden.RequireEqualLua(t, []byte(client.Content))
+	distBts, err := os.ReadFile(distFile)
+	require.NoError(t, err)
+	require.Equal(t, client.Content, string(distBts))
+}
+
+func TestRunPipeUniversalBinaryNotReplacing(t *testing.T) {
+	folder := t.TempDir()
+	ctx := &context.Context{
+		Git: context.GitInfo{
+			CurrentTag: "v1.0.1",
+		},
+		Version:   "1.0.1",
+		Artifacts: artifact.New(),
+		Config: config.Project{
+			Dist:        folder,
+			ProjectName: "unibin",
+			Rigs: []config.GoFish{
+				{
+					Name: "unibin",
+					Rig: config.RepoRef{
+						Owner: "unibin",
+						Name:  "bar",
+					},
+					IDs: []string{
+						"unibin",
+					},
+				},
+			},
+		},
+	}
+	path := filepath.Join(folder, "bin.tar.gz")
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "unibin_amd64.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+		},
+	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "unibin_arm64.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "arm64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+		},
+	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "unibin.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "all",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+			artifact.ExtraReplaces: false,
 		},
 	})
 
diff --git a/internal/pipe/gofish/testdata/TestRunPipeUniversalBinaryNotReplacing.lua.golden b/internal/pipe/gofish/testdata/TestRunPipeUniversalBinaryNotReplacing.lua.golden
new file mode 100644
index 00000000..48a1bf56
--- /dev/null
+++ b/internal/pipe/gofish/testdata/TestRunPipeUniversalBinaryNotReplacing.lua.golden
@@ -0,0 +1,38 @@
+local name = "unibin"
+local version = "1.0.1"
+
+food = {
+    name = name,
+    description = "",
+    license = "",
+    homepage = "",
+    version = version,
+    packages = {
+        {
+            os = "darwin",
+            arch = "amd64",
+            url = "https://dummyhost/download/v1.0.1/unibin_amd64.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = "unibin",
+                    installpath = "bin/unibin",
+                    executable = true
+                },
+            }
+        },
+        {
+            os = "darwin",
+            arch = "arm64",
+            url = "https://dummyhost/download/v1.0.1/unibin_arm64.tar.gz",
+            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+            resources = {
+                {
+                    path = "unibin",
+                    installpath = "bin/unibin",
+                    executable = true
+                },
+            }
+        },
+    }
+}
diff --git a/internal/pipe/krew/krew_test.go b/internal/pipe/krew/krew_test.go
index f2a7cad1..ad7b1c7d 100644
--- a/internal/pipe/krew/krew_test.go
+++ b/internal/pipe/krew/krew_test.go
@@ -336,6 +336,89 @@ func TestRunPipeUniversalBinary(t *testing.T) {
 			artifact.ExtraID:       "unibin",
 			artifact.ExtraFormat:   "tar.gz",
 			artifact.ExtraBinaries: []string{"unibin"},
+			artifact.ExtraReplaces: true,
+		},
+	})
+
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	client := client.NewMock()
+	distFile := filepath.Join(folder, manifestName(t)+".yaml")
+
+	require.NoError(t, runAll(ctx, client))
+	require.NoError(t, publishAll(ctx, client))
+	require.True(t, client.CreatedFile)
+	golden.RequireEqualNakedYaml(t, []byte(client.Content))
+	requireValidManifest(t)
+	distBts, err := os.ReadFile(distFile)
+	require.NoError(t, err)
+	require.Equal(t, client.Content, string(distBts))
+}
+
+func TestRunPipeUniversalBinaryNotReplacing(t *testing.T) {
+	folder := t.TempDir()
+	ctx := &context.Context{
+		Git: context.GitInfo{
+			CurrentTag: "v1.0.1",
+		},
+		Version:   "1.0.1",
+		Artifacts: artifact.New(),
+		Config: config.Project{
+			Dist:        folder,
+			ProjectName: "unibin",
+			Krews: []config.Krew{
+				{
+					Name:             manifestName(t),
+					Description:      "Some desc",
+					ShortDescription: "Short desc",
+					Index: config.RepoRef{
+						Owner: "unibin",
+						Name:  "bar",
+					},
+					IDs: []string{
+						"unibin",
+					},
+				},
+			},
+		},
+	}
+	path := filepath.Join(folder, "bin.tar.gz")
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "unibin_amd64.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+		},
+	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "unibin_amd64.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "arm64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+		},
+	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "unibin.tar.gz",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "all",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "unibin",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"unibin"},
+			artifact.ExtraReplaces: false,
 		},
 	})
 
diff --git a/internal/pipe/krew/testdata/TestRunPipeUniversalBinaryNotReplacing.yaml b/internal/pipe/krew/testdata/TestRunPipeUniversalBinaryNotReplacing.yaml
new file mode 100644
index 00000000..09c7ad9c
--- /dev/null
+++ b/internal/pipe/krew/testdata/TestRunPipeUniversalBinaryNotReplacing.yaml
@@ -0,0 +1,23 @@
+apiVersion: krew.googlecontainertools.github.com/v1alpha2
+kind: Plugin
+metadata:
+  name: TestRunPipeUniversalBinaryNotReplacing
+spec:
+  version: v1.0.1
+  platforms:
+  - bin: unibin
+    uri: https://dummyhost/download/v1.0.1/unibin_amd64.tar.gz
+    sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+    selector:
+      matchLabels:
+        os: darwin
+        arch: amd64
+  - bin: unibin
+    uri: https://dummyhost/download/v1.0.1/unibin_amd64.tar.gz
+    sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+    selector:
+      matchLabels:
+        os: darwin
+        arch: arm64
+  shortDescription: Short desc
+  description: Some desc
diff --git a/internal/pipe/scoop/scoop_test.go b/internal/pipe/scoop/scoop_test.go
index ea4f4bfb..103d5c97 100644
--- a/internal/pipe/scoop/scoop_test.go
+++ b/internal/pipe/scoop/scoop_test.go
@@ -85,7 +85,7 @@ func Test_doRun(t *testing.T) {
 	tests := []struct {
 		name               string
 		args               args
-		artifacts          []*artifact.Artifact
+		artifacts          []artifact.Artifact
 		assertRunError     errChecker
 		assertPublishError errChecker
 		assert             asserter
@@ -98,8 +98,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -128,7 +127,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]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},
 			},
@@ -147,8 +146,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -176,7 +174,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]artifact.Artifact{
 				{
 					Name:   "foo_1.0.1_windows_amd64.tar.gz",
 					Goos:   "windows",
@@ -208,8 +206,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						GitHubURLs: config.GitHubURLs{Download: "https://api.custom.github.enterprise.com"},
 						Builds: []config.Build{
@@ -238,7 +235,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]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},
 			},
@@ -257,8 +254,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -286,7 +282,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]artifact.Artifact{
 				{
 					Name:   "foo_1.0.1_windows_amd64.tar.gz",
 					Goos:   "windows",
@@ -312,8 +308,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						GitHubURLs: config.GitHubURLs{Download: "https://api.custom.gitlab.enterprise.com"},
 						Builds: []config.Build{
@@ -342,7 +337,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]artifact.Artifact{
 				{
 					Name:   "foo_1.0.1_windows_amd64.tar.gz",
 					Goos:   "windows",
@@ -368,8 +363,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test"},
@@ -397,7 +391,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]artifact.Artifact{
 				{Name: "foo_1.0.1_linux_amd64.tar.gz", Goos: "linux", Goarch: "amd64"},
 				{Name: "foo_1.0.1_linux_386.tar.gz", Goos: "linux", Goarch: "386"},
 			},
@@ -413,8 +407,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -439,7 +432,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]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},
 			},
@@ -461,8 +454,7 @@ func Test_doRun(t *testing.T) {
 						Patch:      1,
 						Prerelease: "-pre.1",
 					},
-					Version:   "1.0.1-pre.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1-pre.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -491,7 +483,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]artifact.Artifact{
 				{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},
 			},
@@ -507,8 +499,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -537,7 +528,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]artifact.Artifact{
 				{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},
 			},
@@ -553,8 +544,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -579,7 +569,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]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},
 			},
@@ -595,8 +585,7 @@ func Test_doRun(t *testing.T) {
 					Git: context.GitInfo{
 						CurrentTag: "v1.0.1",
 					},
-					Version:   "1.0.1",
-					Artifacts: artifact.New(),
+					Version: "1.0.1",
 					Config: config.Project{
 						Builds: []config.Build{
 							{Binary: "test", Goarch: []string{"amd64"}, Goos: []string{"windows"}},
@@ -621,7 +610,7 @@ func Test_doRun(t *testing.T) {
 				},
 				client.NewMock(),
 			},
-			[]*artifact.Artifact{
+			[]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},
 			},
@@ -633,8 +622,10 @@ func Test_doRun(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			ctx := tt.args.ctx
+			ctx.Artifacts = artifact.New()
 			for _, a := range tt.artifacts {
-				ctx.Artifacts.Add(a)
+				a.Type = artifact.UploadableArchive
+				ctx.Artifacts.Add(&a)
 			}
 			require.NoError(t, Pipe{}.Default(ctx))
 
diff --git a/internal/pipe/universalbinary/universalbinary_test.go b/internal/pipe/universalbinary/universalbinary_test.go
index e02abc63..17aca2df 100644
--- a/internal/pipe/universalbinary/universalbinary_test.go
+++ b/internal/pipe/universalbinary/universalbinary_test.go
@@ -206,15 +206,19 @@ func TestRun(t *testing.T) {
 	t.Run("replacing", func(t *testing.T) {
 		require.NoError(t, Pipe{}.Run(ctx1))
 		require.Len(t, ctx1.Artifacts.Filter(artifact.ByType(artifact.Binary)).List(), 0)
-		require.Len(t, ctx1.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List(), 1)
-		checkUniversalBinary(t, ctx1.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List()[0])
+		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))
 	})
 
 	t.Run("keeping", func(t *testing.T) {
 		require.NoError(t, Pipe{}.Run(ctx2))
 		require.Len(t, ctx2.Artifacts.Filter(artifact.ByType(artifact.Binary)).List(), 2)
-		require.Len(t, ctx2.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List(), 1)
-		checkUniversalBinary(t, ctx2.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List()[0])
+		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))
 	})
 
 	t.Run("bad template", func(t *testing.T) {