diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go index b351339b389..abd707f6122 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 ca5f29c8fc2..4a210471f9b 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 fe791e65a8c..26e4f7a63c1 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 17f39143284..7f868af3bfb 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/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go index eb5dace1d8c..76b0dcdf896 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 + }) }) } } diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 10f61ed4721..78a247b2208 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{}, )