Repository: goreleaser/goreleaser (Go)

Issue:
Allow building docker manifests having empty template values

### Discussed in https://github.com/orgs/goreleaser/discussions/5822

<div type='discussions-op-text'>

<sup>Originally posted by **zerospiel** June 10, 2025</sup>
Hi all,

I am trying to build a new configuration for my CI, and I'd like to keep everything within a single config file instead of producing boilerplates. Having that said, I am struggling to implement something close to a conditional expression within the `docker_manifests` package section, having the following (simplified/trimmed) `.goreleaser.yaml`:

```yaml
builds:
  - id: amd64
    env:
      - CGO_ENABLED=0
      - GO111MODULE=on
    goos:
      - linux
    goarch:
      - amd64
  - id: arm_and_arm64
    env:
      - CGO_ENABLED=0
      - GO111MODULE=on
    goos:
      - linux
    goarch:
      - arm
      - arm64
    goarm:
      - "7"
    builder: go
    # Here is the conditional skip
    skip: >-
      {{ if isEnvSet "SKIP_ARM" }}{{ true }}{{ else }}false{{ end }}

dockers:
  - id: linux-amd64
    goos: linux
    goarch: amd64
    image_templates:
      - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-amd64"
    skip_push: false
    dockerfile: "goreleaser.dockerfile"
    use: buildx
    ids:
      - amd64
    build_flag_templates:
      - --platform=linux/amd64
  - id: linux-arm64
    goos: linux
    goarch: arm64
    image_templates:
      - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-arm64v8"
    skip_push: false
    dockerfile: "goreleaser.dockerfile"
    use: buildx
    ids:
      - arm_and_arm64
    build_flag_templates:
      - --platform=linux/arm64/v8

docker_manifests:
  - name_template: '{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}'
    image_templates:
      - '{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-amd64'
      # Here is my attempt to skip these images from being added to the manifest
      - >- 
        {{ if isEnvSet "SKIP_ARM" }}{{""}}{{ else }}{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-arm64v8{{ end }}
      - >- 
        {{ if isEnvSet "SKIP_ARM" }}{{""}}{{ else }}{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Version }}-armv7{{ end }}
    skip_push: false
    use: docker
```

So, here at the last lines, I have a list with `if`s, what I want to achieve is to skip creating a manifest from non-existent digests (because the images from `dockers` will be skipped due to set `ids` section, which is done on purpose), but unfortunately, the current implementation [does not skip empty image names](https://github.com/goreleaser/goreleaser/blob/3e58f11fdaadeaac2061fb3296105a71e1efa3a0/internal/pipe/docker/manifest.go#L146) after evaluating a template name.

Hence, my question is: am I doing it wrong, or is it worth adding an extra `if` statement to the line of code mentioned above?

Would appreciate any tips!</div>


Discussion:

Patch:
diff --git a/internal/pipe/docker/manifest.go b/internal/pipe/docker/manifest.go
index f4f0f0204a8..7ce3c5d6196 100644
--- a/internal/pipe/docker/manifest.go
+++ b/internal/pipe/docker/manifest.go
@@ -143,7 +143,10 @@ func manifestImages(ctx *context.Context, manifest config.DockerManifest) ([]str
 		if err != nil {
 			return []string{}, err
 		}
-		imgs = append(imgs, withDigest(str, artifacts))
+
+		if str != "" {
+			imgs = append(imgs, withDigest(str, artifacts))
+		}
 	}
 	if strings.TrimSpace(strings.Join(manifest.ImageTemplates, "")) == "" {
 		return imgs, pipe.Skip("manifest has no images")


Test patch:
diff --git a/internal/pipe/docker/manifest_test.go b/internal/pipe/docker/manifest_test.go
index 2483628fec4..adafede3421 100644
--- a/internal/pipe/docker/manifest_test.go
+++ b/internal/pipe/docker/manifest_test.go
@@ -1,8 +1,13 @@
 package docker
 
 import (
+	"slices"
+	"strings"
 	"testing"
 
+	"github.com/goreleaser/goreleaser/v2/internal/artifact"
+	"github.com/goreleaser/goreleaser/v2/pkg/config"
+	"github.com/goreleaser/goreleaser/v2/pkg/context"
 	"github.com/stretchr/testify/require"
 )
 
@@ -26,3 +31,121 @@ func TestValidateManifester(t *testing.T) {
 		})
 	}
 }
+
+func Test_manifestImages(t *testing.T) {
+	const someImage = "repo/image:tag"
+
+	tests := []struct {
+		name        string
+		artifacts   []*artifact.Artifact
+		templates   []string
+		want        []string
+		errContains string
+		wantErr     bool
+	}{
+		{
+			name:        "no templates",
+			want:        []string{},
+			wantErr:     true,
+			errContains: "manifest has no images",
+		},
+		{
+			name:        "empty template string",
+			templates:   []string{""},
+			want:        []string{},
+			wantErr:     true,
+			errContains: "manifest has no images",
+		},
+		{
+			name: "single image with digest",
+			artifacts: []*artifact.Artifact{
+				{
+					Type:  artifact.DockerImage,
+					Name:  someImage,
+					Extra: map[string]any{artifact.ExtraDigest: "sha256:123"},
+				},
+			},
+			templates: []string{someImage},
+			want:      []string{someImage + "@sha256:123"},
+		},
+		{
+			name: "single image without digest",
+			artifacts: []*artifact.Artifact{
+				{
+					Type:  artifact.DockerImage,
+					Name:  someImage,
+					Extra: map[string]any{},
+				},
+			},
+			templates: []string{someImage},
+			want:      []string{someImage},
+		},
+		{
+			name: "template with no matching artifact",
+			artifacts: []*artifact.Artifact{
+				{
+					Type:  artifact.DockerImage,
+					Name:  "other/image:tag",
+					Extra: map[string]any{artifact.ExtraDigest: "sha"},
+				},
+			},
+			templates: []string{someImage},
+			want:      []string{someImage},
+		},
+		{
+			name: "multiple templates with some empty",
+			artifacts: []*artifact.Artifact{
+				{
+					Type:  artifact.DockerImage,
+					Name:  "a:1",
+					Extra: map[string]any{artifact.ExtraDigest: "d1"},
+				},
+				{
+					Type:  artifact.DockerImage,
+					Name:  "b:2",
+					Extra: map[string]any{},
+				},
+			},
+			templates: []string{
+				"a:1",
+				`{{ if contains "aa" "a" }}{{""}}{{ else }}{{ "fail" }}{{ end }}`,
+				"b:2",
+				"",
+			},
+			want: []string{"a:1@d1", "b:2"},
+		},
+		{
+			name:        "incorrect template",
+			templates:   []string{"{{ if eq (INCORRECT true) }}"},
+			wantErr:     true,
+			errContains: "template: failed to apply",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := context.New(config.Project{})
+			for _, art := range tt.artifacts {
+				ctx.Artifacts.Add(art)
+			}
+
+			manifest := config.DockerManifest{
+				ImageTemplates: tt.templates,
+			}
+
+			got, err := manifestImages(ctx, manifest)
+
+			if tt.wantErr {
+				require.Error(t, err)
+				if !strings.Contains(err.Error(), tt.errContains) {
+					t.Fatalf("expected error to contain %q, got: %v", tt.errContains, err)
+				}
+			} else {
+				require.NoError(t, err)
+				if !slices.Equal(got, tt.want) {
+					t.Fatalf("unexpected output: want: %v, got: %v", tt.want, got)
+				}
+			}
+		})
+	}
+}