Repository: goreleaser/goreleaser
Pull request #3540 (2022-11-09 14:02:35)

feat: add digest to artifacts info of published docker images
Extract the digest (sha256) of docker images from the `docker push` command for dockers published to the docker registry.
Outputting the digest is required to avoid a race condition when referencing the image, where the image tag is being modified before the reference is done.
See this [blog post](https://github.com/goreleaser/goreleaser/issues/3496) for more info.
This PR fixes https://github.com/goreleaser/goreleaser/issues/3496.

Note that the 'publish' pipe now must run before the 'metadata' pipe, so that the information extracted during the 'publish' pipe would appear in the metadata.
Previously, the published docker images metadata wasn't printed (because of the order). It made sense because the content of the published image was just a subset of the local one.
Now that it is printed to the metadata, it should have a different name to avoid confusion.
As I mentioned, it wasn't printed before - so there shouldn't be any backward-compatibility issues.

---
Local tests:
```
go test -v .
=== RUN   TestVersion
=== RUN   TestVersion/only_version
=== RUN   TestVersion/version_and_date
=== RUN   TestVersion/version,_date,_built_by
=== RUN   TestVersion/all_empty
=== RUN   TestVersion/complete
--- PASS: TestVersion (0.00s)
    --- PASS: TestVersion/only_version (0.00s)
    --- PASS: TestVersion/version_and_date (0.00s)
    --- PASS: TestVersion/version,_date,_built_by (0.00s)
    --- PASS: TestVersion/all_empty (0.00s)
    --- PASS: TestVersion/complete (0.00s)
PASS
ok      github.com/goreleaser/goreleaser        0.764s
```

Output example:
```
  {
    "name": "gallegit/hello-world:latest",
    "path": "gallegit/hello-world:latest",
    "goos": "linux",
    "goarch": "amd64",
    "internal_type": 10,
    "type": "Published Docker Image",
    "extra": {
      "digest": "sha256:c3f7dd196a046dc061236d3c6ae1e2946269e90da30b0a959240ca799750e632"
    }
  }
```

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/3540?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#3540](https://codecov.io/gh/goreleaser/goreleaser/pull/3540?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (0886ff1) into [main](https://codecov.io/gh/goreleaser/goreleaser/commit/8794290e147b1cb208cb8b9f8bf6bde278a7ed79?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (8794290) will **increase** coverage by `0.00%`.
> The diff coverage is `85.71%`.

```diff
@@           Coverage Diff           @@
##             main    #3540   +/-   ##
=======================================
  Coverage   84.27%   84.28%           
=======================================
  Files         114      114           
  Lines        9338     9374   +36     
=======================================
+ Hits         7870     7901   +31     
- Misses       1191     1195    +4     
- Partials      277      278    +1     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/3540?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/3540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvYXJ0aWZhY3QvYXJ0aWZhY3QuZ28=) | `87.36% <0.00%> (-0.64%)` | :arrow_down: |
| [internal/pipe/docker/api\_docker.go](https://codecov.io/gh/goreleaser/goreleaser/pull/3540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9kb2NrZXIvYXBpX2RvY2tlci5nbw==) | `88.23% <70.00%> (-5.10%)` | :arrow_down: |
| [internal/pipe/docker/api.go](https://codecov.io/gh/goreleaser/goreleaser/pull/3540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9kb2NrZXIvYXBpLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/pipe/docker/docker.go](https://codecov.io/gh/goreleaser/goreleaser/pull/3540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9kb2NrZXIvZG9ja2VyLmdv) | `91.32% <100.00%> (+0.13%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

caarlos0: hey @gal-legit ! this looks awesome!

can you cherry-pick this commit? https://github.com/goreleaser/goreleaser/commit/d389030f5c89949a2d3b102eb9d9c7610a6fc0f2

so we ensure this is not broken at some point :) 
gal-legit: @caarlos0 done. thank for the feedback! :)

Patch:
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index b351339b..abd707f6 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -86,8 +86,10 @@ func (t Type) String() string {
 		return "Binary"
 	case LinuxPackage:
 		return "Linux Package"
-	case PublishableDockerImage, DockerImage:
+	case PublishableDockerImage:
 		return "Docker Image"
+	case DockerImage:
+		return "Published Docker Image"
 	case DockerManifest:
 		return "Docker Manifest"
 	case PublishableSnapcraft, Snapcraft:
diff --git a/internal/pipe/docker/api.go b/internal/pipe/docker/api.go
index ca5f29c8..4a210471 100644
--- a/internal/pipe/docker/api.go
+++ b/internal/pipe/docker/api.go
@@ -34,7 +34,7 @@ func registerImager(use string, impl imager) {
 // imager is something that can build and push docker images.
 type imager interface {
 	Build(ctx *context.Context, root string, images, flags []string) error
-	Push(ctx *context.Context, image string, flags []string) error
+	Push(ctx *context.Context, image string, flags []string) (digest string, err error)
 }
 
 // manifester is something that can create and push docker manifests.
@@ -66,3 +66,31 @@ func runCommand(ctx *context.Context, dir, binary string, args ...string) error
 	}
 	return nil
 }
+
+func runCommandWithOutput(ctx *context.Context, dir, binary string, args ...string) ([]byte, error) {
+	fields := log.Fields{
+		"cmd": append([]string{binary}, args[0]),
+		"cwd": dir,
+	}
+
+	/* #nosec */
+	cmd := exec.CommandContext(ctx, binary, args...)
+	cmd.Dir = dir
+	cmd.Env = ctx.Env.Strings()
+
+	var b bytes.Buffer
+	w := gio.Safe(&b)
+	cmd.Stderr = io.MultiWriter(logext.NewWriter(fields, logext.Error), w)
+
+	log.WithFields(fields).WithField("args", args[1:]).Debug("running")
+	out, err := cmd.Output()
+	if out != nil {
+		// regardless of command success, always print stdout for backward-compatibility with runCommand()
+		_, _ = io.MultiWriter(logext.NewWriter(fields, logext.Error), w).Write(out)
+	}
+	if err != nil {
+		return nil, fmt.Errorf("%w: %s", err, b.String())
+	}
+
+	return out, nil
+}
diff --git a/internal/pipe/docker/api_docker.go b/internal/pipe/docker/api_docker.go
index fe791e65..26e4f7a6 100644
--- a/internal/pipe/docker/api_docker.go
+++ b/internal/pipe/docker/api_docker.go
@@ -2,6 +2,7 @@ package docker
 
 import (
 	"fmt"
+	"regexp"
 
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
@@ -43,11 +44,21 @@ type dockerImager struct {
 	buildx bool
 }
 
-func (i dockerImager) Push(ctx *context.Context, image string, flags []string) error {
-	if err := runCommand(ctx, ".", "docker", "push", image); err != nil {
-		return fmt.Errorf("failed to push %s: %w", image, err)
+var dockerDigestPattern = regexp.MustCompile("sha256:[a-z0-9]{64}")
+
+func (i dockerImager) Push(ctx *context.Context, image string, flags []string) (digest string, err error) {
+	outBytes, err := runCommandWithOutput(ctx, ".", "docker", "push", image)
+	if err != nil {
+		return "", fmt.Errorf("failed to push %s: %w", image, err)
 	}
-	return nil
+
+	out := string(outBytes)
+	digest = dockerDigestPattern.FindString(out)
+	if digest == "" {
+		return "", fmt.Errorf("failed to find docker digest in docker push output: %v", out)
+	}
+
+	return digest, nil
 }
 
 func (i dockerImager) Build(ctx *context.Context, root string, images, flags []string) error {
diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go
index 17f39143..7f868af3 100644
--- a/internal/pipe/docker/docker.go
+++ b/internal/pipe/docker/docker.go
@@ -20,6 +20,7 @@ import (
 
 const (
 	dockerConfigExtra = "DockerConfig"
+	dockerDigestExtra = "digest"
 
 	useBuildx = "buildx"
 	useDocker = "docker"
@@ -248,7 +249,8 @@ func dockerPush(ctx *context.Context, image *artifact.Artifact) error {
 		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 {
+	digest, err := imagers[docker.Use].Push(ctx, image.Name, docker.PushFlags)
+	if err != nil {
 		return err
 	}
 
@@ -264,6 +266,8 @@ func dockerPush(ctx *context.Context, image *artifact.Artifact) error {
 	if docker.ID != "" {
 		art.Extra[artifact.ExtraID] = docker.ID
 	}
+	art.Extra[dockerDigestExtra] = digest
+
 	ctx.Artifacts.Add(art)
 	return nil
 }
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index 10f61ed4..78a247b2 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -111,10 +111,10 @@ var Pipeline = append(
 	chocolatey.Pipe{},
 	// create and push docker images
 	docker.Pipe{},
-	// creates a metadata.json and an artifacts.json files in the dist folder
-	metadata.Pipe{},
 	// publishes artifacts
 	publish.Pipe{},
+	// creates a metadata.json and an artifacts.json files in the dist folder
+	metadata.Pipe{},
 	// announce releases
 	announce.Pipe{},
 )


Test patch:
diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go
index eb5dace1..76b0dcdf 100644
--- a/internal/pipe/docker/docker_test.go
+++ b/internal/pipe/docker/docker_test.go
@@ -1091,6 +1091,13 @@ func TestRunPipe(t *testing.T) {
 					// t.Log("removing docker image", img)
 					require.NoError(t, rmi(img), "could not delete image %s", img)
 				}
+
+				_ = ctx.Artifacts.Filter(artifact.ByType(artifact.DockerImage)).Visit(func(a *artifact.Artifact) error {
+					digest, err := artifact.Extra[string](*a, dockerDigestExtra)
+					require.NoError(t, err)
+					require.NotEmpty(t, digest)
+					return nil
+				})
 			})
 		}
 	}