Repository: goreleaser/goreleaser
Pull request #2297 (2021-06-09 20:13:13)

feat: use project id
<!--

Hi, thanks for contributing!

Please make sure you read our CONTRIBUTING guide.

Also, add tests and the respective documentation changes as well.

-->


<!-- If applied, this commit will... -->
Add support for using Gitlab's internal project id instead of the owner/name slug, necessary to be able to create releases on nested subgroups in Gitlab CE Hosted (See #1799)
It will also add templating support for the gitlab->name field, so that the project ID can be passed in using environment variables (e.g: name: "{{.Env.CI_PROJECT_ID}}" in a gitlab runner)
...

<!-- Why is this change being made? -->
The current version of goreleaser is unable to create a gitlab release in a project that resides in a nested subgroup/group.

...

<!-- # Provide links to any relevant tickets, URLs or other resources -->
#1799 #1801 
...


vercel[bot]: Someone is attempting to deploy a commit to the **Goreleaser** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=Goreleaser&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22113a19d3eb8a2c8a9c5216103a907b5e4a33cff3%22%7D%2C%22id%22%3A%22QmaCqLijZM55z5eGD9vSb9BMFYCyuevoYGfC8S7WpUGvCT%22%2C%22org%22%3A%22goreleaser%22%2C%22prId%22%3A2297%2C%22repo%22%3A%22goreleaser%22%7D).


caarlos0: hey @jessequinn , thanks for the PR!

can you run against https://github.com/goreleaser/acceptance-tests ?
codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2297?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2297](https://codecov.io/gh/goreleaser/goreleaser/pull/2297?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (b44f5b5) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/e4373447181af89a10217ffb2134a64e610eec8d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (e437344) will **decrease** coverage by `0.40%`.
> The diff coverage is `27.02%`.

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

```diff
@@            Coverage Diff             @@
##           master    #2297      +/-   ##
==========================================
- Coverage   82.69%   82.29%   -0.41%     
==========================================
  Files          79       79              
  Lines        4305     4331      +26     
==========================================
+ Hits         3560     3564       +4     
- Misses        605      625      +20     
- Partials      140      142       +2     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2297?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/client/gitlab.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2297/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvY2xpZW50L2dpdGxhYi5nbw==) | `8.53% <27.02%> (+0.80%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2297?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/2297?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [e437344...b44f5b5](https://codecov.io/gh/goreleaser/goreleaser/pull/2297?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).

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/goreleaser/goreleaser/HRN86EYyaepytPN1vXVffCktYQaN](https://vercel.com/goreleaser/goreleaser/HRN86EYyaepytPN1vXVffCktYQaN)  
✅ Preview: *In Progress*


jessequinn: hi @caarlos0 i ran it against the Gitea portion of acceptance tests:

<img width="1012" alt="Screen Shot 2021-06-17 at 05 08 58" src="https://user-images.githubusercontent.com/5218671/122357758-641b0300-cf2a-11eb-989e-6a0716a5b62a.png">
caarlos0: I think you'll need to run the gitlab tests as well 🤔
jessequinn: im trying but gitlab is just a disaster. 502 nonstop.
caarlos0: maybe @mavogel can help?
jessequinn: i had to use my linux dell server. osx doesnt work with the gitlab home directory. path issues. anyhow, i fixed the error.
jessequinn: i pushed a fix. the problem was a missing /
jessequinn: <img width="635" alt="Screen Shot 2021-06-17 at 10 59 48" src="https://user-images.githubusercontent.com/5218671/122411449-25ea0780-cf5b-11eb-8c5a-1af5608b3800.png">


Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index af231775..c915e3ac 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -39,7 +39,7 @@ dockers:
   - 'goreleaser/goreleaser:{{ .Tag }}-amd64'
   - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-amd64'
   dockerfile: Dockerfile
-  use_buildx: true
+  use: buildx
   build_flag_templates:
   - "--pull"
   - "--label=org.opencontainers.image.created={{.Date}}"
@@ -54,7 +54,7 @@ dockers:
   - 'goreleaser/goreleaser:{{ .Tag }}-arm64'
   - 'ghcr.io/goreleaser/goreleaser:{{ .Tag }}-arm64'
   dockerfile: Dockerfile
-  use_buildx: true
+  use: buildx
   build_flag_templates:
   - "--pull"
   - "--label=org.opencontainers.image.created={{.Date}}"
diff --git a/internal/client/gitlab.go b/internal/client/gitlab.go
index b664e054..183f8d0f 100644
--- a/internal/client/gitlab.go
+++ b/internal/client/gitlab.go
@@ -191,11 +191,18 @@ func (c *gitlabClient) CreateRelease(ctx *context.Context, body string) (release
 	if err != nil {
 		return "", err
 	}
-
-	projectID := ctx.Config.Release.GitLab.Owner + "/" + ctx.Config.Release.GitLab.Name
+	gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
+	if err != nil {
+		return "", err
+	}
+	projectID := gitlabName
+	if ctx.Config.Release.GitLab.Owner != "" {
+		projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID
+	}
 	log.WithFields(log.Fields{
-		"owner": ctx.Config.Release.GitLab.Owner,
-		"name":  ctx.Config.Release.GitLab.Name,
+		"owner":     ctx.Config.Release.GitLab.Owner,
+		"name":      gitlabName,
+		"projectID": projectID,
 	}).Debug("projectID")
 
 	name := title
@@ -258,12 +265,26 @@ func (c *gitlabClient) CreateRelease(ctx *context.Context, body string) (release
 }
 
 func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
-	return fmt.Sprintf(
-		"%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
-		ctx.Config.GitLabURLs.Download,
-		ctx.Config.Release.GitLab.Owner,
-		ctx.Config.Release.GitLab.Name,
-	), nil
+	var urlTemplate string
+	gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
+	if err != nil {
+		return "", err
+	}
+	if ctx.Config.Release.GitLab.Owner != "" {
+		urlTemplate = fmt.Sprintf(
+			"%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
+			ctx.Config.GitLabURLs.Download,
+			ctx.Config.Release.GitLab.Owner,
+			gitlabName,
+		)
+	} else {
+		urlTemplate = fmt.Sprintf(
+			"%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
+			ctx.Config.GitLabURLs.Download,
+			gitlabName,
+		)
+	}
+	return urlTemplate, nil
 }
 
 // Upload uploads a file into a release repository.
@@ -273,7 +294,16 @@ func (c *gitlabClient) Upload(
 	artifact *artifact.Artifact,
 	file *os.File,
 ) error {
-	projectID := ctx.Config.Release.GitLab.Owner + "/" + ctx.Config.Release.GitLab.Name
+	// create new template and apply name field
+	gitlabName, err := tmpl.New(ctx).Apply(ctx.Config.Release.GitLab.Name)
+	if err != nil {
+		return err
+	}
+	projectID := gitlabName
+	// check if owner is empty
+	if ctx.Config.Release.GitLab.Owner != "" {
+		projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID
+	}
 
 	log.WithField("file", file.Name()).Debug("uploading file")
 	projectFile, _, err := c.client.Projects.UploadFile(
@@ -290,9 +320,15 @@ func (c *gitlabClient) Upload(
 		"url":  projectFile.URL,
 	}).Debug("uploaded file")
 
+	// search for project details based on projectID
+	projectDetails, _, err := c.client.Projects.GetProject(projectID, nil)
+	if err != nil {
+		return err
+	}
+
 	gitlabBaseURL := ctx.Config.GitLabURLs.Download
 	// projectFile.URL from upload: /uploads/<hash>/filename.txt
-	linkURL := gitlabBaseURL + "/" + projectID + projectFile.URL
+	linkURL := gitlabBaseURL + "/" + projectDetails.PathWithNamespace + projectFile.URL
 	name := artifact.Name
 	filename := "/" + name
 	releaseLink, _, err := c.client.ReleaseLinks.CreateReleaseLink(
diff --git a/internal/pipe/build/build.go b/internal/pipe/build/build.go
index eb7d87aa..e9da2e0f 100644
--- a/internal/pipe/build/build.go
+++ b/internal/pipe/build/build.go
@@ -202,7 +202,7 @@ func buildOptionsForTarget(ctx *context.Context, build config.Build, target stri
 	}
 
 	log.WithField("binary", path).Info("building")
-	buildOpts.Name = filepath.Base(name)
+	buildOpts.Name = name
 	buildOpts.Path = path
 	return &buildOpts, nil
 }
diff --git a/internal/pipe/docker/api.go b/internal/pipe/docker/api.go
new file mode 100644
index 00000000..0c25f8a9
--- /dev/null
+++ b/internal/pipe/docker/api.go
@@ -0,0 +1,43 @@
+package docker
+
+import (
+	"context"
+	"os/exec"
+
+	"github.com/apex/log"
+	"github.com/goreleaser/goreleaser/pkg/config"
+)
+
+// 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) error
+}
+
+// manifester is something that can create and push docker manifests.
+type manifester interface {
+	Create(ctx context.Context, manifest string, images, flags []string) error
+	Push(ctx context.Context, manifest string, flags []string) error
+}
+
+func newManifester(manifest config.DockerManifest) manifester {
+	return dockerManifester{}
+}
+
+func newImager(docker config.Docker) imager {
+	return dockerImager{
+		buildx: docker.Use == useBuildx,
+	}
+}
+
+// nolint: unparam
+func runCommand(ctx context.Context, dir, binary string, args ...string) error {
+	/* #nosec */
+	cmd := exec.CommandContext(ctx, binary, args...)
+	cmd.Dir = dir
+	log := log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir)
+	log.Debug("running")
+	out, err := cmd.CombinedOutput()
+	log.Debug(string(out))
+	return err
+}
diff --git a/internal/pipe/docker/api_docker.go b/internal/pipe/docker/api_docker.go
new file mode 100644
index 00000000..40b9df17
--- /dev/null
+++ b/internal/pipe/docker/api_docker.go
@@ -0,0 +1,60 @@
+package docker
+
+import (
+	"context"
+	"fmt"
+)
+
+type dockerManifester struct{}
+
+func (m dockerManifester) Create(ctx context.Context, manifest string, images, flags []string) error {
+	_ = runCommand(ctx, ".", "docker", "manifest", "rm", manifest)
+
+	args := []string{"manifest", "create", manifest}
+	args = append(args, images...)
+	args = append(args, flags...)
+
+	if err := runCommand(ctx, ".", "docker", args...); err != nil {
+		return fmt.Errorf("failed to create %s: %w", manifest, err)
+	}
+	return nil
+}
+
+func (m dockerManifester) Push(ctx context.Context, manifest string, flags []string) error {
+	args := []string{"manifest", "push", manifest}
+	args = append(args, flags...)
+	if err := runCommand(ctx, ".", "docker", args...); err != nil {
+		return fmt.Errorf("failed to push %s: %w", manifest, err)
+	}
+	return nil
+}
+
+type dockerImager struct {
+	buildx bool
+}
+
+func (i dockerImager) Push(ctx context.Context, image string) error {
+	if err := runCommand(ctx, ".", "docker", "push", image); err != nil {
+		return fmt.Errorf("failed to push %s: %w", image, err)
+	}
+	return nil
+}
+
+func (i dockerImager) Build(ctx context.Context, root string, images, flags []string) error {
+	if err := runCommand(ctx, root, "docker", i.buildCommand(images, flags)...); err != nil {
+		return fmt.Errorf("failed to build %s: %w", images[0], err)
+	}
+	return nil
+}
+
+func (i dockerImager) buildCommand(images, flags []string) []string {
+	base := []string{"build", "."}
+	if i.buildx {
+		base = []string{"buildx", "build", ".", "--load"}
+	}
+	for _, image := range images {
+		base = append(base, "-t", image)
+	}
+	base = append(base, flags...)
+	return base
+}
diff --git a/internal/pipe/docker/docker.go b/internal/pipe/docker/docker.go
index df2cf3a1..a52d663c 100644
--- a/internal/pipe/docker/docker.go
+++ b/internal/pipe/docker/docker.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strings"
 
@@ -19,8 +18,14 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
-// ErrNoDocker is shown when docker cannot be found in $PATH.
-var ErrNoDocker = errors.New("docker not present in $PATH")
+const (
+	dockerConfigExtra = "DockerConfig"
+
+	useBuildx = "buildx"
+	useDocker = "docker"
+)
+
+var validDockerUses = []string{useBuildx, useDocker}
 
 // Pipe for docker.
 type Pipe struct{}
@@ -50,6 +55,18 @@ func (Pipe) Default(ctx *context.Context) error {
 			deprecate.Notice(ctx, "docker.builds")
 			docker.IDs = append(docker.IDs, docker.Builds...)
 		}
+		if docker.Buildx {
+			deprecate.Notice(ctx, "docker.use_buildx")
+			if docker.Use == "" {
+				docker.Use = useBuildx
+			}
+		}
+		if docker.Use == "" {
+			docker.Use = useDocker
+		}
+		if !isValidUse(docker.Use) {
+			return fmt.Errorf("docker: invalid use: %s, valid options are %v", docker.Use, validDockerUses)
+		}
 		for _, f := range docker.Files {
 			if f == "." || strings.HasPrefix(f, ctx.Config.Dist) {
 				return fmt.Errorf("invalid docker.files: can't be . or inside dist folder: %s", f)
@@ -59,15 +76,20 @@ func (Pipe) Default(ctx *context.Context) error {
 	return nil
 }
 
+func isValidUse(use string) bool {
+	for _, s := range validDockerUses {
+		if s == use {
+			return true
+		}
+	}
+	return false
+}
+
 // Run the pipe.
 func (Pipe) Run(ctx *context.Context) error {
 	if len(ctx.Config.Dockers) == 0 || len(ctx.Config.Dockers[0].ImageTemplates) == 0 {
 		return pipe.ErrSkipDisabledPipe
 	}
-	_, err := exec.LookPath("docker")
-	if err != nil {
-		return ErrNoDocker
-	}
 	return doRun(ctx)
 }
 
@@ -145,7 +167,7 @@ func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.A
 		return err
 	}
 
-	if err := dockerBuild(ctx, tmp, images, buildFlags, docker.Buildx); err != nil {
+	if err := newImager(docker).Build(ctx, tmp, images, buildFlags); err != nil {
 		return err
 	}
 
@@ -166,6 +188,9 @@ func process(ctx *context.Context, docker config.Docker, artifacts []*artifact.A
 			Goarch: docker.Goarch,
 			Goos:   docker.Goos,
 			Goarm:  docker.Goarm,
+			Extra: map[string]interface{}{
+				dockerConfigExtra: docker,
+			},
 		})
 	}
 	return nil
@@ -229,42 +254,12 @@ func link(src, dest string) error {
 	})
 }
 
-func dockerBuild(ctx *context.Context, root string, images, flags []string, buildx bool) error {
-	log.WithField("image", images[0]).WithField("buildx", buildx).Info("building docker image")
-	/* #nosec */
-	cmd := exec.CommandContext(ctx, "docker", buildCommand(buildx, images, flags)...)
-	cmd.Dir = root
-	log.WithField("cmd", cmd.Args).WithField("cwd", cmd.Dir).Debug("running")
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		return fmt.Errorf("failed to build docker image: %s: \n%s: %w", images[0], string(out), err)
-	}
-	log.Debugf("docker build output: \n%s", string(out))
-	return nil
-}
-
-func buildCommand(buildx bool, images, flags []string) []string {
-	base := []string{"build", "."}
-	if buildx {
-		base = []string{"buildx", "build", ".", "--load"}
-	}
-	for _, image := range images {
-		base = append(base, "-t", image)
-	}
-	base = append(base, flags...)
-	return base
-}
-
 func dockerPush(ctx *context.Context, image *artifact.Artifact) error {
 	log.WithField("image", image.Name).Info("pushing docker image")
-	/* #nosec */
-	cmd := exec.CommandContext(ctx, "docker", "push", image.Name)
-	log.WithField("cmd", cmd.Args).Debug("running")
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		return fmt.Errorf("failed to push docker image: \n%s: %w", string(out), err)
+	docker := image.Extra[dockerConfigExtra].(config.Docker)
+	if err := newImager(docker).Push(ctx, image.Name); err != nil {
+		return err
 	}
-	log.Debugf("docker push output: \n%s", string(out))
 	ctx.Artifacts.Add(&artifact.Artifact{
 		Type:   artifact.DockerImage,
 		Name:   image.Name,
diff --git a/internal/pipe/docker/manifest.go b/internal/pipe/docker/manifest.go
index 4387f65f..53a52b4d 100644
--- a/internal/pipe/docker/manifest.go
+++ b/internal/pipe/docker/manifest.go
@@ -1,8 +1,6 @@
 package docker
 
 import (
-	"fmt"
-	"os/exec"
 	"strings"
 
 	"github.com/apex/log"
@@ -34,21 +32,25 @@ func (ManifestPipe) Publish(ctx *context.Context) error {
 			if strings.TrimSpace(manifest.SkipPush) == "true" {
 				return pipe.Skip("docker_manifest.skip_push is set")
 			}
+
 			if strings.TrimSpace(manifest.SkipPush) == "auto" && ctx.Semver.Prerelease != "" {
 				return pipe.Skip("prerelease detected with 'auto' push, skipping docker manifest")
 			}
+
 			name, err := manifestName(ctx, manifest)
 			if err != nil {
 				return err
 			}
-			if err := dockerManifestRm(ctx, name); err != nil {
-				return err
-			}
+
 			images, err := manifestImages(ctx, manifest)
 			if err != nil {
 				return err
 			}
-			if err := dockerManifestCreate(ctx, name, images, manifest.CreateFlags); err != nil {
+
+			manifester := newManifester(manifest)
+
+			log.WithField("manifest", manifest).WithField("images", images).Info("creating docker manifest")
+			if err := manifester.Create(ctx, name, images, manifest.CreateFlags); err != nil {
 				return err
 			}
 			ctx.Artifacts.Add(&artifact.Artifact{
@@ -56,7 +58,9 @@ func (ManifestPipe) Publish(ctx *context.Context) error {
 				Name: name,
 				Path: name,
 			})
-			return dockerManifestPush(ctx, name, manifest.PushFlags)
+
+			log.WithField("manifest", manifest).Info("pushing docker manifest")
+			return manifester.Push(ctx, name, manifest.PushFlags)
 		})
 	}
 	return g.Wait()
@@ -87,51 +91,3 @@ func manifestImages(ctx *context.Context, manifest config.DockerManifest) ([]str
 	}
 	return imgs, nil
 }
-
-func dockerManifestRm(ctx *context.Context, manifest string) error {
-	log.WithField("manifest", manifest).Info("removing local docker manifest")
-	/* #nosec */
-	cmd := exec.CommandContext(ctx, "docker", "manifest", "rm", manifest)
-	log.WithField("cmd", cmd.Args).Debug("running")
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		if strings.HasPrefix(string(out), "No such manifest: ") {
-			// ignore "no such manifest" error, is the state we want in the end...
-			return nil
-		}
-		return fmt.Errorf("failed to remove local docker manifest: %s: \n%s: %w", manifest, string(out), err)
-	}
-	log.Debugf("docker manifest rm output: \n%s", string(out))
-	return nil
-}
-
-func dockerManifestCreate(ctx *context.Context, manifest string, images, flags []string) error {
-	log.WithField("manifest", manifest).WithField("images", images).Info("creating docker manifest")
-	args := []string{"manifest", "create", manifest}
-	args = append(args, images...)
-	args = append(args, flags...)
-	/* #nosec */
-	cmd := exec.CommandContext(ctx, "docker", args...)
-	log.WithField("cmd", cmd.Args).Debug("running")
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		return fmt.Errorf("failed to create docker manifest: %s: \n%s: %w", manifest, string(out), err)
-	}
-	log.Debugf("docker manifest output: \n%s", string(out))
-	return nil
-}
-
-func dockerManifestPush(ctx *context.Context, manifest string, flags []string) error {
-	log.WithField("manifest", manifest).Info("pushing docker manifest")
-	args := []string{"manifest", "push", manifest}
-	args = append(args, flags...)
-	/* #nosec */
-	cmd := exec.CommandContext(ctx, "docker", args...)
-	log.WithField("cmd", cmd.Args).Debug("running")
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		return fmt.Errorf("failed to push docker manifest: %s: \n%s: %w", manifest, string(out), err)
-	}
-	log.Debugf("docker manifest output: \n%s", string(out))
-	return nil
-}
diff --git a/internal/pipe/nfpm/nfpm.go b/internal/pipe/nfpm/nfpm.go
index 6abe7549..db4edcd4 100644
--- a/internal/pipe/nfpm/nfpm.go
+++ b/internal/pipe/nfpm/nfpm.go
@@ -220,7 +220,7 @@ func create(ctx *context.Context, fpm config.NFPM, format, arch string, binaries
 		log := log.WithField("package", name+"."+format).WithField("arch", arch)
 		for _, binary := range binaries {
 			src := binary.Path
-			dst := filepath.Join(fpm.Bindir, binary.Name)
+			dst := filepath.Join(fpm.Bindir, filepath.Base(binary.Name))
 			log.WithField("src", src).WithField("dst", dst).Debug("adding binary to package")
 			contents = append(contents, &files.Content{
 				Source:      filepath.ToSlash(src),
diff --git a/internal/pipe/scoop/scoop.go b/internal/pipe/scoop/scoop.go
index 921db7c5..681c4a2c 100644
--- a/internal/pipe/scoop/scoop.go
+++ b/internal/pipe/scoop/scoop.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"path"
 	"path/filepath"
 	"strings"
 
@@ -97,7 +98,7 @@ func doRun(ctx *context.Context, cl client.Client) error {
 		return ErrNoWindows
 	}
 
-	path := scoop.Name + ".json"
+	filename := scoop.Name + ".json"
 
 	data, err := dataFor(ctx, cl, archives)
 	if err != nil {
@@ -108,9 +109,9 @@ func doRun(ctx *context.Context, cl client.Client) error {
 		return err
 	}
 
-	filename := filepath.Join(ctx.Config.Dist, path)
-	log.WithField("manifest", filename).Info("writing")
-	if err := os.WriteFile(filename, content.Bytes(), 0o644); err != nil {
+	distPath := filepath.Join(ctx.Config.Dist, filename)
+	log.WithField("manifest", distPath).Info("writing")
+	if err := os.WriteFile(distPath, content.Bytes(), 0o644); err != nil {
 		return fmt.Errorf("failed to write scoop manifest: %w", err)
 	}
 
@@ -142,7 +143,7 @@ func doRun(ctx *context.Context, cl client.Client) error {
 		scoop.CommitAuthor,
 		repo,
 		content.Bytes(),
-		path,
+		path.Join(scoop.Folder, filename),
 		commitMessage,
 	)
 }
diff --git a/internal/pipe/snapcraft/snapcraft.go b/internal/pipe/snapcraft/snapcraft.go
index 2d8d33de..c34c0568 100644
--- a/internal/pipe/snapcraft/snapcraft.go
+++ b/internal/pipe/snapcraft/snapcraft.go
@@ -237,10 +237,10 @@ func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries [
 	if len(snap.Apps) == 0 {
 		name := snap.Name
 		if name == "" {
-			name = binaries[0].Name
+			name = filepath.Base(binaries[0].Name)
 		}
 		metadata.Apps[name] = AppMetadata{
-			Command: filepath.Base(binaries[0].Name),
+			Command: filepath.Base(filepath.Base(binaries[0].Name)),
 		}
 	}
 
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 5c80237a..a866a0fc 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -116,6 +116,7 @@ type Homebrew struct {
 type Scoop struct {
 	Name                  string       `yaml:",omitempty"`
 	Bucket                RepoRef      `yaml:",omitempty"`
+	Folder                string       `yaml:",omitempty"`
 	CommitAuthor          CommitAuthor `yaml:"commit_author,omitempty"`
 	CommitMessageTemplate string       `yaml:"commit_msg_template,omitempty"`
 	Homepage              string       `yaml:",omitempty"`
@@ -518,7 +519,8 @@ type Docker struct {
 	SkipPush           string   `yaml:"skip_push,omitempty"`
 	Files              []string `yaml:"extra_files,omitempty"`
 	BuildFlagTemplates []string `yaml:"build_flag_templates,omitempty"`
-	Buildx             bool     `yaml:"use_buildx,omitempty"`
+	Buildx             bool     `yaml:"use_buildx,omitempty"` // deprecated: use Use instead
+	Use                string   `yaml:"use,omitempty"`
 }
 
 // DockerManifest config.
diff --git a/www/docs/customization/docker.md b/www/docs/customization/docker.md
index 18495fd6..62ae26fa 100644
--- a/www/docs/customization/docker.md
+++ b/www/docs/customization/docker.md
@@ -82,11 +82,10 @@ dockers:
     # Path to the Dockerfile (from the project root).
     dockerfile: Dockerfile
 
-    # Whether to use `docker buildx build` instead of `docker build`.
-    # You probably want to set it to true when using flags like `--platform`.
-    # If true, will also add `--load` to the build flags.
-    # Defaults to false.
-    use_buildx: true
+    # Set the "backend" for the docker pipe.
+    # Valid options are: docker, buildx
+    # Defaults to docker.
+    use: docker
 
     # Template of the docker build flags.
     build_flag_templates:
diff --git a/www/docs/customization/release.md b/www/docs/customization/release.md
index 6639e9fe..e15b16b6 100644
--- a/www/docs/customization/release.md
+++ b/www/docs/customization/release.md
@@ -14,6 +14,9 @@ release:
   # Repo in which the release will be created.
   # Default is extracted from the origin remote URL or empty if its private hosted.
   # Note: it can only be one: either github, gitlab or gitea
+  # You can use Gitlab's internal project id by setting it directly or using an environment variable
+  # owner: ""
+  # name: "{{.Env.CI_PROJECT_ID}}"
   github:
     owner: user
     name: repo
diff --git a/www/docs/customization/scoop.md b/www/docs/customization/scoop.md
index a579cd82..b62a0633 100644
--- a/www/docs/customization/scoop.md
+++ b/www/docs/customization/scoop.md
@@ -24,6 +24,10 @@ scoop:
     # Optionally a token can be provided, if it differs from the token provided to GoReleaser
     token: "{{ .Env.SCOOP_BUCKET_GITHUB_TOKEN }}"
 
+  # Folder inside the repository to put the scoop.
+  # Default is the root folder.
+  folder: Scoops
+
   # Git author used to commit to the repository.
   # Defaults are shown.
   commit_author:
diff --git a/www/docs/deprecations.md b/www/docs/deprecations.md
index 3266d7b4..0674459e 100644
--- a/www/docs/deprecations.md
+++ b/www/docs/deprecations.md
@@ -15,6 +15,30 @@ goreleaser check
 
 ## Active deprecation notices
 
+
+### docker.use_buildx
+
+> since 2021-06-26 (v0.172.0)
+
+`use_buildx` is deprecated in favor of the more generalist `use`, since now it also allow other options in the future:
+
+Change this:
+
+=== "Before"
+    ```yaml
+    dockers:
+      -
+        use_buildx: true
+    ```
+
+=== "After"
+    ```yaml
+    dockers:
+      -
+        use: buildx
+    ```
+
+
 ### Skipping SemVer Validations
 
 > since 2021-02-28 (v0.158.0)


Test patch:
diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go
index d2eb7e89..8514dc1b 100644
--- a/internal/pipe/build/build_test.go
+++ b/internal/pipe/build/build_test.go
@@ -760,7 +760,7 @@ func TestBuildOptionsForTarget(t *testing.T) {
 				NoUniqueDistDir: true,
 			},
 			expectedOpts: &api.Options{
-				Name:   "testbinary_linux_amd64",
+				Name:   "distpath/linux/amd64/testbinary_linux_amd64",
 				Path:   filepath.Join(tmpDir, "distpath", "linux", "amd64", "testbinary_linux_amd64"),
 				Target: "linux_amd64",
 				Os:     "linux",
diff --git a/internal/pipe/docker/docker_test.go b/internal/pipe/docker/docker_test.go
index 35f474d7..121eafa9 100644
--- a/internal/pipe/docker/docker_test.go
+++ b/internal/pipe/docker/docker_test.go
@@ -344,7 +344,7 @@ func TestRunPipe(t *testing.T) {
 			expect:              []string{registry + "goreleaser/test_multiarch_fail:latest-arm64v8"},
 			assertError:         shouldNotErr,
 			pubAssertError:      shouldNotErr,
-			manifestAssertError: shouldErr("failed to create docker manifest: localhost:5000/goreleaser/test_multiarch_fail:test"),
+			manifestAssertError: shouldErr("failed to create localhost:5000/goreleaser/test_multiarch_fail:test"),
 			assertImageLabels:   noLabels,
 		},
 		"multiarch manifest template error": {
@@ -525,7 +525,7 @@ func TestRunPipe(t *testing.T) {
 			},
 			expect:              []string{},
 			assertImageLabels:   noLabels,
-			assertError:         shouldErr(`goreleaser/test_run_pipe_template_UPPERCASE:v1.0.0" for "-t, --tag" flag: invalid reference format: repository name must be lowercase`),
+			assertError:         shouldErr(`failed to build localhost:5000/goreleaser/test_run_pipe_template_UPPERCASE:v1.0.0`),
 			pubAssertError:      shouldNotErr,
 			manifestAssertError: shouldNotErr,
 		},
@@ -687,7 +687,7 @@ func TestRunPipe(t *testing.T) {
 				registry + "goreleaser/one_img_error_with_skip_push:true",
 			},
 			assertImageLabels: noLabels,
-			assertError:       shouldErr("failed to build docker image"),
+			assertError:       shouldErr("failed to build localhost:5000/goreleaser/one_img_error_with_skip_push:false"),
 		},
 		"valid_no_latest": {
 			dockers: []config.Docker{
@@ -745,7 +745,7 @@ func TestRunPipe(t *testing.T) {
 				},
 			},
 			assertImageLabels: noLabels,
-			assertError:       shouldErr("unknown flag: --bad-flag"),
+			assertError:       shouldErr("failed to build localhost:5000/goreleaser/test_build_args:latest"),
 		},
 		"bad_dockerfile": {
 			dockers: []config.Docker{
@@ -759,7 +759,7 @@ func TestRunPipe(t *testing.T) {
 				},
 			},
 			assertImageLabels: noLabels,
-			assertError:       shouldErr("pull access denied"),
+			assertError:       shouldErr("failed to build localhost:5000/goreleaser/bad_dockerfile:latest"),
 		},
 		"tag_template_error": {
 			dockers: []config.Docker{
@@ -860,7 +860,7 @@ func TestRunPipe(t *testing.T) {
 			},
 			assertImageLabels:   noLabels,
 			assertError:         shouldNotErr,
-			pubAssertError:      shouldErr(`requested access to the resource is denied`),
+			pubAssertError:      shouldErr(`failed to push docker.io/nope:latest`),
 			manifestAssertError: shouldNotErr,
 		},
 		"dockerfile_doesnt_exist": {
@@ -1107,7 +1107,10 @@ func TestBuildCommand(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			require.Equal(t, tt.expect, buildCommand(tt.buildx, images, tt.flags))
+			imager := dockerImager{
+				buildx: tt.buildx,
+			}
+			require.Equal(t, tt.expect, imager.buildCommand(images, tt.flags))
 		})
 	}
 }
@@ -1130,43 +1133,43 @@ func TestNoDockerWithoutImageName(t *testing.T) {
 	}))))
 }
 
-func TestDockerNotInPath(t *testing.T) {
-	path := os.Getenv("PATH")
-	defer func() {
-		require.NoError(t, os.Setenv("PATH", path))
-	}()
-	require.NoError(t, os.Setenv("PATH", ""))
+func TestDefault(t *testing.T) {
 	ctx := &context.Context{
-		Version: "1.0.0",
 		Config: config.Project{
 			Dockers: []config.Docker{
 				{
-					ImageTemplates: []string{"a/b"},
+					IDs:      []string{"aa"},
+					Builds:   []string{"foo"},
+					Binaries: []string{"aaa"},
+				},
+				{
+					Use: useBuildx,
 				},
 			},
 		},
 	}
-	require.EqualError(t, Pipe{}.Run(ctx), ErrNoDocker.Error())
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Len(t, ctx.Config.Dockers, 2)
+	docker := ctx.Config.Dockers[0]
+	require.Equal(t, "linux", docker.Goos)
+	require.Equal(t, "amd64", docker.Goarch)
+	require.Equal(t, []string{"aa", "foo"}, docker.IDs)
+	require.Equal(t, useDocker, docker.Use)
+	docker = ctx.Config.Dockers[1]
+	require.Equal(t, useBuildx, docker.Use)
 }
 
-func TestDefault(t *testing.T) {
+func TestDefaultInvalidUse(t *testing.T) {
 	ctx := &context.Context{
 		Config: config.Project{
 			Dockers: []config.Docker{
 				{
-					IDs:      []string{"aa"},
-					Builds:   []string{"foo"},
-					Binaries: []string{"aaa"},
+					Use: "something",
 				},
 			},
 		},
 	}
-	require.NoError(t, Pipe{}.Default(ctx))
-	require.Len(t, ctx.Config.Dockers, 1)
-	docker := ctx.Config.Dockers[0]
-	require.Equal(t, "linux", docker.Goos)
-	require.Equal(t, "amd64", docker.Goarch)
-	require.Equal(t, []string{"aa", "foo"}, docker.IDs)
+	require.EqualError(t, Pipe{}.Default(ctx), `docker: invalid use: something, valid options are [buildx docker]`)
 }
 
 func TestDefaultDockerfile(t *testing.T) {
diff --git a/internal/pipe/nfpm/nfpm_test.go b/internal/pipe/nfpm/nfpm_test.go
index 7a02ac6d..b924a49d 100644
--- a/internal/pipe/nfpm/nfpm_test.go
+++ b/internal/pipe/nfpm/nfpm_test.go
@@ -150,7 +150,7 @@ func TestRunPipe(t *testing.T) {
 	for _, goos := range []string{"linux", "darwin"} {
 		for _, goarch := range []string{"amd64", "386"} {
 			ctx.Artifacts.Add(&artifact.Artifact{
-				Name:   "mybin",
+				Name:   "subdir/mybin",
 				Path:   binPath,
 				Goarch: goarch,
 				Goos:   goos,
diff --git a/internal/pipe/scoop/scoop_test.go b/internal/pipe/scoop/scoop_test.go
index 7c604dec..e9758b80 100644
--- a/internal/pipe/scoop/scoop_test.go
+++ b/internal/pipe/scoop/scoop_test.go
@@ -61,6 +61,12 @@ func Test_doRun(t *testing.T) {
 	file := filepath.Join(folder, "archive")
 	require.NoError(t, os.WriteFile(file, []byte("lorem ipsum"), 0o644))
 
+	type args struct {
+		ctx    *context.Context
+		client *DummyClient
+	}
+
+	type asserter func(*testing.T, args)
 	type errChecker func(*testing.T, error)
 	shouldErr := func(msg string) errChecker {
 		return func(t *testing.T, err error) {
@@ -69,19 +75,20 @@ func Test_doRun(t *testing.T) {
 			require.EqualError(t, err, msg)
 		}
 	}
+	noAssertions := func(t *testing.T, _ args) {
+		t.Helper()
+	}
 	shouldNotErr := func(t *testing.T, err error) {
 		t.Helper()
 		require.NoError(t, err)
 	}
-	type args struct {
-		ctx    *context.Context
-		client client.Client
-	}
+
 	tests := []struct {
 		name        string
 		args        args
 		artifacts   []*artifact.Artifact
 		assertError errChecker
+		assert      asserter
 	}{
 		{
 			"valid public github",
@@ -113,6 +120,7 @@ func Test_doRun(t *testing.T) {
 								Owner: "test",
 								Name:  "test",
 							},
+							Folder:      "scoops",
 							Description: "A run pipe test formula",
 							Homepage:    "https://github.com/goreleaser",
 						},
@@ -125,6 +133,10 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldNotErr,
+			func(t *testing.T, a args) {
+				t.Helper()
+				require.Equal(t, "scoops/run-pipe.json", a.client.Path)
+			},
 		},
 		{
 			"wrap in directory",
@@ -184,6 +196,7 @@ func Test_doRun(t *testing.T) {
 				},
 			},
 			shouldNotErr,
+			noAssertions,
 		},
 		{
 			"valid enterprise github",
@@ -228,6 +241,10 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldNotErr,
+			func(t *testing.T, a args) {
+				t.Helper()
+				require.Equal(t, "run-pipe.json", a.client.Path)
+			},
 		},
 		{
 			"valid public gitlab",
@@ -281,6 +298,7 @@ func Test_doRun(t *testing.T) {
 				},
 			},
 			shouldNotErr,
+			noAssertions,
 		},
 		{
 			"valid enterprise gitlab",
@@ -335,6 +353,7 @@ func Test_doRun(t *testing.T) {
 				},
 			},
 			shouldNotErr,
+			noAssertions,
 		},
 		{
 			"token type not implemented for pipe",
@@ -377,6 +396,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr(ErrTokenTypeNotImplementedForScoop.Error()),
+			noAssertions,
 		},
 		{
 			"no windows build",
@@ -420,6 +440,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_linux_386.tar.gz", Goos: "linux", Goarch: "386"},
 			},
 			shouldErr("scoop requires a windows build"),
+			noAssertions,
 		},
 		{
 			"no scoop",
@@ -455,6 +476,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386"},
 			},
 			shouldErr(pipe.ErrSkipDisabledPipe.Error()),
+			noAssertions,
 		},
 		{
 			"no publish",
@@ -499,6 +521,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr(pipe.ErrSkipPublishEnabled.Error()),
+			noAssertions,
 		},
 		{
 			"is draft",
@@ -539,6 +562,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr("release is marked as draft"),
+			noAssertions,
 		},
 		{
 			"is prerelease and skip upload set to auto",
@@ -589,6 +613,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1-pre.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr("release is prerelease"),
+			noAssertions,
 		},
 		{
 			"skip upload set to true",
@@ -633,6 +658,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1-pre.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr("scoop.skip_upload is true"),
+			noAssertions,
 		},
 		{
 			"release is disabled",
@@ -673,6 +699,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr("release is disabled"),
+			noAssertions,
 		},
 		{
 			"no archive",
@@ -713,6 +740,7 @@ func Test_doRun(t *testing.T) {
 				{Name: "foo_1.0.1_windows_386.tar.gz", Goos: "windows", Goarch: "386", Path: file},
 			},
 			shouldErr("archive format is binary"),
+			noAssertions,
 		},
 	}
 	for _, tt := range tests {
@@ -724,6 +752,7 @@ func Test_doRun(t *testing.T) {
 			require.NoError(t, Pipe{}.Default(ctx))
 
 			tt.assertError(t, doRun(ctx, tt.args.client))
+			tt.assert(t, tt.args)
 		})
 	}
 }
@@ -1096,6 +1125,7 @@ func TestWrapInDirectory(t *testing.T) {
 type DummyClient struct {
 	CreatedFile    bool
 	Content        string
+	Path           string
 	NotImplemented bool
 }
 
@@ -1117,6 +1147,7 @@ func (dc *DummyClient) ReleaseURLTemplate(ctx *context.Context) (string, error)
 func (dc *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo client.Repo, content []byte, path, msg string) (err error) {
 	dc.CreatedFile = true
 	dc.Content = string(content)
+	dc.Path = path
 	return
 }
 
diff --git a/internal/pipe/snapcraft/snapcraft_test.go b/internal/pipe/snapcraft/snapcraft_test.go
index c543b275..b568cbf4 100644
--- a/internal/pipe/snapcraft/snapcraft_test.go
+++ b/internal/pipe/snapcraft/snapcraft_test.go
@@ -426,7 +426,7 @@ func addBinaries(t *testing.T, ctx *context.Context, name, dist string) {
 			require.NoError(t, err)
 			require.NoError(t, f.Close())
 			ctx.Artifacts.Add(&artifact.Artifact{
-				Name:   name,
+				Name:   "subdir/" + name,
 				Path:   binPath,
 				Goarch: goarch,
 				Goos:   goos,