Repository: goreleaser/goreleaser
Pull request #2129 (2021-03-21 18:54:15)

feat: initial proxy build support
starting a prototype for a feature to build proxying from a go module, so builds are verifiable.

check https://github.com/goreleaser/goreleaser/issues/1354 for more info

@myitcv wdyt of this approach?

- [x] decide on the API 
- [x] add tests
- [x] handle snapshots (probably ignore the proxy if so)
- [x] add an example to docs

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/9fb6fftrWbsdouyGsrmkjo14jcYU](https://vercel.com/goreleaser/goreleaser/9fb6fftrWbsdouyGsrmkjo14jcYU)  
✅ Preview: [https://goreleaser-git-proxy-build-goreleaser.vercel.app](https://goreleaser-git-proxy-build-goreleaser.vercel.app)


codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2129?src=pr&el=h1) Report
> Merging [#2129](https://codecov.io/gh/goreleaser/goreleaser/pull/2129?src=pr&el=desc) (7e96e87) into [gomodulepath](https://codecov.io/gh/goreleaser/goreleaser/commit/13272ab289f9a456bad057fca7625e8b30ba24ff?el=desc) (13272ab) will **decrease** coverage by `0.56%`.
> The diff coverage is `22.22%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2129/graphs/tree.svg?width=650&height=150&src=pr&token=3bJvv6b7hN)](https://codecov.io/gh/goreleaser/goreleaser/pull/2129?src=pr&el=tree)

```diff
@@               Coverage Diff                @@
##           gomodulepath    #2129      +/-   ##
================================================
- Coverage         82.82%   82.26%   -0.57%     
================================================
  Files                75       75              
  Lines              4024     4054      +30     
================================================
+ Hits               3333     3335       +2     
- Misses              564      590      +26     
- Partials            127      129       +2     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2129?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2129/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `93.75% <ø> (ø)` | |
| [internal/pipe/build/build.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2129/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9idWlsZC9idWlsZC5nbw==) | `80.13% <22.22%> (-18.22%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2129?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2129?src=pr&el=footer). Last update [13272ab...7e96e87](https://codecov.io/gh/goreleaser/goreleaser/pull/2129?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

caarlos0: > But one overarching question I had is that I don't really have a strong sense from this PR what the configuration interface would be. i.e. if I want to use this mode, what settings would I require?

Basically, something like:

```yaml
builds:
  - id: foo
    proxy:
       path: '{{ .ModulePath }}'
       version: '{{ .Tag }}'
```

Should already work in most projects...

> Is there perhaps an integration test that will cover this?

there will be once we decide on the api, this pr is veeery wip :P 


myitcv: Awesome progress! 

> ```yaml
> builds:
>   - id: foo
>     proxy:
>        path: '{{ .ModulePath }}'
>        version: '{{ .Tag }}'
> ```

Understood. So `builds` is a list of binaries that get put into a release artefact? The use of `proxy` indicates the main package at `.ModulePath` (perhaps need to think about naming here too?) at version `.Tag` gets built, via the mechanism we have described, and included in the release artefact? Am I holding that roughly right?!

> there will be once we decide on the api, this pr is veeery wip :P

No problem at all, quite understand! I tend to think backwards in terms of what the user will see/do first. 

Semi-related tangent (not at all for this PR), but for integration tests have you considered using https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript? It's the setup that is used for all of the [`cmd/go` tests](https://github.com/golang/go/tree/master/src/cmd/go/testdata/script) and now also [`cmd/cue` tests](https://github.com/cuelang/cue/tree/master/cmd/cue/cmd/testdata/script). 



caarlos0: > Understood. So `builds` is a list of binaries that get put into a release artefact?

more or less, you can, for example, have multiple build definitions for the same main.go, but with different names/configs (e.g. CGO vs no-CGO, etc).

> perhaps need to think about naming here too?

yes, not sure about what a good name would be here...

> The use of `proxy` indicates the main package at `.ModulePath` (perhaps need to think about naming here too?) at version `.Tag` gets built, via the mechanism we have described, and included in the release artefact? Am I holding that roughly right?!

exactly right, yes



> Semi-related tangent (not at all for this PR), but for integration tests have you considered using https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript? It's the setup that is used for all of the [`cmd/go` tests](https://github.com/golang/go/tree/master/src/cmd/go/testdata/script) and now also [`cmd/cue` tests](https://github.com/cuelang/cue/tree/master/cmd/cue/cmd/testdata/script).

didn't know this, looks promising, will take a look! thanks for the tip :D 

myitcv: 
> for example, have multiple build definitions for the same main.go, but with different names/configs (e.g. CGO vs no-CGO, etc).

Thanks for confirming. My question in https://github.com/goreleaser/goreleaser/pull/2129#discussion_r598845932 was actually slightly broader. Is it possible to have binaries built from main packages from multiple modules in a release artefact?
caarlos0: > Thanks for confirming. My question in [#2129 (comment)](https://github.com/goreleaser/goreleaser/pull/2129#discussion_r598845932) was actually slightly broader. Is it possible to have binaries built from main packages from multiple modules in a release artefact?

in theory, the way this pr is, it is.

I'm not sure we should allow it though... I'm thinking we should probably force it to be current module at current tag 🤔
myitcv: > I'm not sure we should allow it though... I'm thinking we should probably force it to be current module at current tag 🤔

I'd tend to agree. If you make this a constraint initially, then you always have the option to relax it at a later stage.
caarlos0: Given that, I think we should move this to the root of the yaml...

something like:

```yaml
gomod:
  proxy: true
```

(with maybe a better name, open for suggestions).

thoughts?
myitcv: > Given that, I think we should move this to the root of the yaml...

That sounds reasonable - perhaps you could present a full config file as an example? I'm nowhere near as familiar with the surrounding context as you 😄  That way we (I) can see how it "sits" with everything else. 
myitcv: Very much a tangent, but this is a situation where having CUE to describe the config schema would be great: we could talk very concretely in terms of how that schema would change. 
caarlos0: > That sounds reasonable - perhaps you could present a full config file as an example? I'm nowhere near as familiar with the surrounding context as you 😄 That way we (I) can see how it "sits" with everything else.

Sure, using [goreleaser config](https://github.com/goreleaser/goreleaser/blob/master/.goreleaser.yml) itself as an example, the diff would be just:

```diff
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 82978913..b14ecdab 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -4,6 +4,8 @@ before:
   hooks:
     - go mod tidy
     - ./scripts/completions.sh
+gomod:
+  proxy: true
 builds:
 - env:
     - CGO_ENABLED=0
```

But it is rather simple, I think a more complex example (building something other than the "root" package) would be:

```yaml
gomod:
  proxy: true
builds:
- id: root
  goos: ['linux', 'darwin']
  goarch: ['arm64']
  binary: rootbin
- id: foo
  main: pkg/foo
  goos: ['linux', 'darwin']
  goarch: ['arm64']
  binary: foobin
# etc...
archives:
  - id: foobar
    ids: ['foo', 'root']
# etc
```



> Very much a tangent, but this is a situation where having CUE to describe the config schema would be great: we could talk very concretely in terms of how that schema would change.

yes, yaml has been proving to be a pain on that and also when defining "relationships", e.g. build id `foo` should be on archive `foobar`.
myitcv: > But it is rather simple, I think a more complex example (building something other than the "root" package) would be:

Great, thanks. That really helps. 

I'm assuming full import paths for `main` are supported today? Because given:

```
- id: foo
  main: pkg/foo
  goos: ['linux', 'darwin']
  goarch: ['arm64']
  binary: foobin
```

`pkg/foo` by itself is, strictly speaking, a package pattern that refers to the non-existent standard library package `pkg/foo`. 

`main` should, I think, be defined as a package pattern that must resolve to a main package. In non-proxy mode, that resolution would happen within the root of the project, in which case `./pkg/foo` would work. But in proxy mode that will need to be `example.com/proj/pkg/foo`, the full import path.

And I think we're saying that a restriction we want to enforce for now is that any `main` import path value in proxy mode must resolve to a package inside the project in question.
caarlos0: > `main` should, I think, be defined as a package pattern that must resolve to a main package. In non-proxy mode, that resolution would happen within the root of the project, in which case `./pkg/foo` would work. But in proxy mode that will need to be `example.com/proj/pkg/foo`, the full import path.

yeah, my idea was to, if proxying, prepend the module path... but it could be `./pkg/foo` and then we could join with them module path as well 🤔

that way, same config works for snapshots as well (when you build against master instead of a tag) 



> And I think we're saying that a restriction we want to enforce for now is that any `main` import path value in proxy mode must resolve to a package inside the project in question.

yes 🤔
caarlos0: pushed a few changes as discussed...
caarlos0: @myitcv pushed some tests and docs, feel free to take another look.

If all ok, will merge/release tomorrow.

also adding some contributors as reviewers to have more eyes into this :) 

Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 82978913..b14ecdab 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -4,6 +4,8 @@ before:
   hooks:
     - go mod tidy
     - ./scripts/completions.sh
+gomod:
+  proxy: true
 builds:
 - env:
     - CGO_ENABLED=0
diff --git a/internal/builders/golang/build.go b/internal/builders/golang/build.go
index 515c3be3..ce618d96 100644
--- a/internal/builders/golang/build.go
+++ b/internal/builders/golang/build.go
@@ -71,8 +71,10 @@ func (*Builder) WithDefaults(build config.Build) (config.Build, error) {
 
 // Build builds a golang build.
 func (*Builder) Build(ctx *context.Context, build config.Build, options api.Options) error {
-	if err := checkMain(build); err != nil {
-		return err
+	if !ctx.Config.GoMod.Proxy {
+		if err := checkMain(build); err != nil {
+			return err
+		}
 	}
 	target, err := newBuildTarget(options.Target)
 	if err != nil {
diff --git a/internal/pipe/gomod/gomod.go b/internal/pipe/gomod/gomod.go
index 8957d766..3b4cbb11 100644
--- a/internal/pipe/gomod/gomod.go
+++ b/internal/pipe/gomod/gomod.go
@@ -1,14 +1,29 @@
+// Package gomod provides go modules utilities, such as template variables and the ability to proxy the module from
+// proxy.golang.org.
 package gomod
 
 import (
+	"errors"
 	"fmt"
+	"io"
+	"os"
 	"os/exec"
+	"path"
+	"path/filepath"
 	"strings"
 
+	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
+	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
+const (
+	go115NotAGoModuleError = "go list -m: not using modules"
+	go116NotAGoModuleError = "command-line-arguments"
+)
+
 // Pipe for env.
 type Pipe struct{}
 
@@ -16,14 +31,17 @@ func (Pipe) String() string {
 	return "loading go mod information"
 }
 
-const (
-	go115NotAGoModuleError = "go list -m: not using modules"
-	go116NotAGoModuleError = "command-line-arguments"
-)
+// Default sets the pipe defaults.
+func (Pipe) Default(ctx *context.Context) error {
+	if ctx.Config.GoMod.GoBinary == "" {
+		ctx.Config.GoMod.GoBinary = "go"
+	}
+	return nil
+}
 
 // Run the pipe.
 func (Pipe) Run(ctx *context.Context) error {
-	out, err := exec.CommandContext(ctx, "go", "list", "-m").CombinedOutput()
+	out, err := exec.CommandContext(ctx, ctx.Config.GoMod.GoBinary, "list", "-m").CombinedOutput()
 	if err != nil {
 		return fmt.Errorf("failed to get module path: %w: %s", err, string(out))
 	}
@@ -35,5 +53,139 @@ func (Pipe) Run(ctx *context.Context) error {
 
 	ctx.ModulePath = result
 
+	if !ctx.Config.GoMod.Proxy {
+		return pipe.Skip("gomod.proxy is disabled")
+	}
+
+	if ctx.Snapshot {
+		return pipe.ErrSnapshotEnabled
+	}
+
+	return setupProxy(ctx)
+}
+
+func setupProxy(ctx *context.Context) error {
+	for i := range ctx.Config.Builds {
+		build := &ctx.Config.Builds[i]
+		if err := proxyBuild(ctx, build); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+const goModTpl = `
+module {{ .BuildID }}
+
+require {{ .ModulePath }} {{ .Tag }}
+`
+
+const mainGoTpl = `
+// +build main
+package main
+
+import _ "{{ .Main }}"
+`
+
+// ErrProxy happens when something goes wrong while proxying the current go module.
+type ErrProxy struct {
+	err     error
+	details string
+}
+
+func newErrProxy(err error) error {
+	return ErrProxy{
+		err: err,
+	}
+}
+
+func newDetailedErrProxy(err error, details string) error {
+	return ErrProxy{
+		err:     err,
+		details: details,
+	}
+}
+
+func (e ErrProxy) Error() string {
+	out := fmt.Sprintf("failed to proxy module: %v", e.err)
+	if e.details != "" {
+		return fmt.Sprintf("%s: %s", out, e.details)
+	}
+	return out
+}
+
+func (e ErrProxy) Unwrap() error {
+	return e.err
+}
+
+func proxyBuild(ctx *context.Context, build *config.Build) error {
+	mainPackage := path.Join(ctx.ModulePath, build.Main)
+	template := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
+		"Main":    mainPackage,
+		"BuildID": build.ID,
+	})
+
+	log.Infof("proxying %s@%s to build %s", ctx.ModulePath, ctx.Git.CurrentTag, mainPackage)
+
+	mod, err := template.Apply(goModTpl)
+	if err != nil {
+		return newErrProxy(err)
+	}
+
+	main, err := template.Apply(mainGoTpl)
+	if err != nil {
+		return newErrProxy(err)
+	}
+
+	dir := filepath.Join(ctx.Config.Dist, "proxy", build.ID)
+
+	log.Debugf("creating needed files")
+
+	if err := os.MkdirAll(dir, 0o755); err != nil {
+		return newErrProxy(err)
+	}
+
+	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(main), 0o666); err != nil {
+		return newErrProxy(err)
+	}
+
+	if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0o666); err != nil {
+		return newErrProxy(err)
+	}
+
+	if err := copyGoSum("go.sum", filepath.Join(dir, "go.sum")); err != nil {
+		return newErrProxy(err)
+	}
+
+	log.Debugf("tidying")
+	cmd := exec.CommandContext(ctx, ctx.Config.GoMod.GoBinary, "mod", "tidy")
+	cmd.Dir = dir
+	cmd.Env = append(ctx.Config.GoMod.Env, os.Environ()...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return newDetailedErrProxy(err, string(out))
+	}
+
+	build.Main = mainPackage
+	build.Dir = dir
 	return nil
 }
+
+func copyGoSum(src, dst string) error {
+	r, err := os.OpenFile(src, os.O_RDONLY, 0o666)
+	if err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			return nil
+		}
+		return err
+	}
+
+	w, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer w.Close()
+
+	_, err = io.Copy(w, r)
+	return err
+}
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index 2c71432c..1df0d017 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -38,7 +38,6 @@ type Piper interface {
 // BuildPipeline contains all build-related pipe implementations in order.
 // nolint:gochecknoglobals
 var BuildPipeline = []Piper{
-	gomod.Pipe{},           // setup gomod-related stuff
 	env.Pipe{},             // load and validate environment variables
 	git.Pipe{},             // get and validate git repo state
 	semver.Pipe{},          // parse current tag to a semver
@@ -46,6 +45,7 @@ var BuildPipeline = []Piper{
 	defaults.Pipe{},        // load default configs
 	snapshot.Pipe{},        // snapshot version handling
 	dist.Pipe{},            // ensure ./dist is clean
+	gomod.Pipe{},           // setup gomod-related stuff
 	effectiveconfig.Pipe{}, // writes the actual config (with defaults et al set) to dist
 	changelog.Pipe{},       // builds the release changelog
 	build.Pipe{},           // build
diff --git a/main.go b/main.go
index 6535feb0..444780b6 100644
--- a/main.go
+++ b/main.go
@@ -3,6 +3,7 @@ package main
 import (
 	"fmt"
 	"os"
+	"runtime/debug"
 
 	"github.com/goreleaser/goreleaser/cmd"
 )
@@ -34,5 +35,8 @@ func buildVersion(version, commit, date, builtBy string) string {
 	if builtBy != "" {
 		result = fmt.Sprintf("%s\nbuilt by: %s", result, builtBy)
 	}
+	if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
+		result = fmt.Sprintf("%s\nmodule version: %s, checksum: %s", result, info.Main.Version, info.Main.Sum)
+	}
 	return result
 }
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 634e079f..4f840e77 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -610,6 +610,7 @@ type Project struct {
 	EnvFiles        EnvFiles         `yaml:"env_files,omitempty"`
 	Before          Before           `yaml:",omitempty"`
 	Source          Source           `yaml:",omitempty"`
+	GoMod           GoMod            `yaml:"gomod,omitempty"`
 
 	// this is a hack ¯\_(ツ)_/¯
 	SingleBuild Build `yaml:"build,omitempty"`
@@ -624,6 +625,12 @@ type Project struct {
 	GiteaURLs GiteaURLs `yaml:"gitea_urls,omitempty"`
 }
 
+type GoMod struct {
+	Proxy    bool     `yaml:",omitempty"`
+	Env      []string `yaml:",omitempty"`
+	GoBinary string   `yaml:",omitempty"`
+}
+
 // Load config file.
 func Load(file string) (config Project, err error) {
 	f, err := os.Open(file) // #nosec
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index c1cbf696..88970413 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -12,6 +12,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/build"
 	"github.com/goreleaser/goreleaser/internal/pipe/checksums"
 	"github.com/goreleaser/goreleaser/internal/pipe/docker"
+	"github.com/goreleaser/goreleaser/internal/pipe/gomod"
 	"github.com/goreleaser/goreleaser/internal/pipe/milestone"
 	"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
 	"github.com/goreleaser/goreleaser/internal/pipe/project"
@@ -39,6 +40,7 @@ var Defaulters = []Defaulter{
 	snapshot.Pipe{},
 	release.Pipe{},
 	project.Pipe{},
+	gomod.Pipe{},
 	build.Pipe{},
 	sourcearchive.Pipe{},
 	archive.Pipe{},
diff --git a/www/docs/cookbooks/build-go-modules.md b/www/docs/cookbooks/build-go-modules.md
new file mode 100644
index 00000000..1cd8471b
--- /dev/null
+++ b/www/docs/cookbooks/build-go-modules.md
@@ -0,0 +1,85 @@
+# Building Go modules
+
+With the default configs, you can already build a Go module without issues.
+
+But, if you want to access module information in runtime (e.g. `debug.BuildInfo` or `go version -m $binary`), you'll
+need to setup GoReleaser to "proxy" that module before building it.
+
+To do that, you can simply add this to your config:
+
+```yaml
+# goreleaser.yml
+gomod:
+  proxy: true
+```
+
+In practice, what this does is:
+
+- for each of your builds, create a `dist/proxy/{{ build.id }}`;
+- creates a `go.mod` that requires your __main module__ at the __current tag__;
+- creates a `main.go` that imports your __main package__;
+- copy the project's `go.sum` to that folder.
+
+In which:
+
+- __build.id__: the `id` property in your `build` definition;
+- __main module__: is the output of `go list -m`;
+- __main package__: is the __main module__ + your build's `main`;
+- __current tag__: is the tag that is being built.
+
+So, let's say:
+
+- __main module__: `github.com/goreleaser/nfpm/v2`;
+- build's `main`: `./cmd/nfpm/`;
+- __current tag__: `v2.5.0`.
+
+GoReleaser will create a `main.go` like:
+
+```go
+// +build: main
+package main
+
+import _ "github.com/goreleaser/nfpm/v2/cmd/nfpm"
+```
+
+a `go.mod` like:
+
+```
+module nfpm
+
+require github.com/goreleaser/nfpm/v2 v2.5.0
+```
+
+Then, it'll run:
+
+```sh
+go mod tidy
+```
+
+And, to build, it will use something like:
+
+```shell
+go build -o nfpm github.com/goreleaser/nfpm/v2/cmd/nfpm
+```
+
+This will resolve the source code from the defined module proxy using `proxy.golang.org`.
+Your project's `go.sum` will be used to verify any modules that are downloaded, with `sum.golang.org` "filling in" any gaps.
+
+## Limitations
+
+1. Extra files will still be copied from the current project's root folder and not from the proxy cache;
+1. You can't build packages that are not contained in the main module.
+
+## More information
+
+You can find more information about it on the [issue][issue] that originated it and its subsequent [pull request][pr].
+
+Make sure to also read the [relevant documentation][docs] for more options.
+
+[issue]: https://github.com/goreleaser/goreleaser/issues/1354
+[pr]: https://github.com/goreleaser/goreleaser/pull/2129
+[docs]: /customization/gomod/
+
+## Real example
+
+Source code of a working example can be found at [goreleaser/example-mod-proxy](https://github.com/goreleaser/example-mod-proxy).
diff --git a/www/docs/customization/gomod.md b/www/docs/customization/gomod.md
new file mode 100644
index 00000000..d3d6715c
--- /dev/null
+++ b/www/docs/customization/gomod.md
@@ -0,0 +1,37 @@
+---
+title: Go Modules
+---
+
+GoReleaser has support for creating verifiable builds.
+A [verifiable build][vgo] is one that records enough information to be precise about exactly how to repeat it.
+All dependencies are loaded via `proxy.golang.org`, and verified against the checksum database `sum.golang.org`.
+A GoReleaser-created verifiable build will include module information in the resulting binary, which can be printed using `go version -m mybinary`.
+
+Configuration options available are described bellow.
+
+```yaml
+# goreleaser.yml
+
+gomod:
+  # Proxy a module from proxy.golang.org, making the builds verifiable.
+  # This will only be effective if running against a tag. Snapshots will ignore this setting.
+  #
+  # Default is false.
+  proxy: true
+
+  # If proxy is true, use these environment variables when running `go mod` commands (namely, `go mod tidy`).
+  # Defaults to `os.Environ()`.
+  env:
+    - GOPROXY=https://proxy.golang.org,direct
+    - GOSUMDB=sum.golang.org
+    - GOPRIVATE=example.com/blah
+
+  # Which Go binary to use.
+  # Defaults to `go`.
+  gobinary: go1.15
+```
+
+!!! tip
+    You can use `debug.ReadBuildInfo()` to get the version/checksum/dependencies of the module.
+
+[vgo]: https://research.swtch.com/vgo-repro
diff --git a/www/mkdocs.yml b/www/mkdocs.yml
index 82d60a4e..82ac7dd6 100644
--- a/www/mkdocs.yml
+++ b/www/mkdocs.yml
@@ -62,6 +62,7 @@ nav:
   - customization/artifactory.md
   - customization/bintray.md
   - customization/blob.md
+  - customization/gomod.md
   - customization/build.md
   - customization/checksum.md
   - customization/publishers.md
@@ -87,6 +88,7 @@ nav:
 - Cookbooks:
   - About: cookbooks/index.md
   - Blog Posts: cookbooks/blog-posts.md
+  - cookbooks//build-go-modules.md
   - cookbooks/semantic-release.md
   - cookbooks/release-a-library.md
   - cookbooks/publish-to-nexus.md


Test patch:
diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go
index 020b5835..797ce5e8 100644
--- a/internal/builders/golang/build_test.go
+++ b/internal/builders/golang/build_test.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -548,6 +549,47 @@ func TestRunPipeWithoutMainFunc(t *testing.T) {
 	})
 }
 
+func TestRunPipeWithProxiedRepo(t *testing.T) {
+	folder := testlib.Mktmp(t)
+	proxied := filepath.Join(folder, "dist/proxy/default")
+	require.NoError(t, os.MkdirAll(proxied, 0o750))
+	require.NoError(t, ioutil.WriteFile(
+		filepath.Join(proxied, "main.go"),
+		[]byte("// +build: main\npackage main\nimport github.com/goreleaser/goreleaser"),
+		0o666,
+	))
+	require.NoError(t, ioutil.WriteFile(
+		filepath.Join(proxied, "go.mod"),
+		[]byte("module foo\nrequire github.com/goreleaser/goreleaser v0.161.1"),
+		0o666,
+	))
+	cmd := exec.Command("go", "mod", "download")
+	cmd.Dir = proxied
+	require.NoError(t, cmd.Run())
+	config := config.Project{
+		GoMod: config.GoMod{
+			Proxy: true,
+		},
+		Builds: []config.Build{
+			{
+				Binary: "foo",
+				Hooks:  config.HookConfig{},
+				Main:   "github.com/goreleaser/goreleaser",
+				Dir:    proxied,
+				Targets: []string{
+					runtimeTarget,
+				},
+				GoBinary: "go",
+			},
+		},
+	}
+	ctx := context.New(config)
+
+	require.NoError(t, Default.Build(ctx, ctx.Config.Builds[0], api.Options{
+		Target: runtimeTarget,
+	}))
+}
+
 func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) {
 	folder := testlib.Mktmp(t)
 	require.NoError(t, ioutil.WriteFile(
diff --git a/internal/pipe/gomod/gomod_test.go b/internal/pipe/gomod/gomod_test.go
index 9af8ff1e..dfc12de1 100644
--- a/internal/pipe/gomod/gomod_test.go
+++ b/internal/pipe/gomod/gomod_test.go
@@ -1,7 +1,10 @@
 package gomod
 
 import (
+	"fmt"
 	"os"
+	"path/filepath"
+	"runtime"
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testlib"
@@ -12,25 +15,211 @@ import (
 
 func TestRun(t *testing.T) {
 	ctx := context.New(config.Project{})
-	require.NoError(t, Pipe{}.Run(ctx))
+	require.NoError(t, Pipe{}.Default(ctx))
+	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
+	require.Equal(t, "github.com/goreleaser/goreleaser", ctx.ModulePath)
+}
+
+func TestRunSnapshot(t *testing.T) {
+	ctx := context.New(config.Project{
+		GoMod: config.GoMod{
+			Proxy: true,
+		},
+	})
+	ctx.Snapshot = true
+	require.NoError(t, Pipe{}.Default(ctx))
+	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
 	require.Equal(t, "github.com/goreleaser/goreleaser", ctx.ModulePath)
 }
 
 func TestRunOutsideGoModule(t *testing.T) {
-	require.NoError(t, os.Chdir(t.TempDir()))
+	dir := testlib.Mktmp(t)
+	require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nfunc main() {println(0)}"), 0o666))
 	ctx := context.New(config.Project{})
+	require.NoError(t, Pipe{}.Default(ctx))
 	testlib.AssertSkipped(t, Pipe{}.Run(ctx))
 	require.Empty(t, ctx.ModulePath)
 }
 
 func TestRunCommandError(t *testing.T) {
-	os.Unsetenv("PATH")
-	require.NoError(t, os.Chdir(t.TempDir()))
-	ctx := context.New(config.Project{})
-	require.EqualError(t, Pipe{}.Run(ctx), "failed to get module path: exec: \"go\": executable file not found in $PATH: ")
+	ctx := context.New(config.Project{
+		GoMod: config.GoMod{
+			GoBinary: "not-a-valid-binary",
+		},
+	})
+	require.EqualError(t, Pipe{}.Run(ctx), "failed to get module path: exec: \"not-a-valid-binary\": executable file not found in $PATH: ")
 	require.Empty(t, ctx.ModulePath)
 }
 
 func TestDescription(t *testing.T) {
 	require.NotEmpty(t, Pipe{}.String())
 }
+
+func TestGoModProxy(t *testing.T) {
+	t.Run("goreleaser", func(t *testing.T) {
+		dir := testlib.Mktmp(t)
+		dist := filepath.Join(dir, "dist")
+		ctx := context.New(config.Project{
+			Dist: dist,
+			GoMod: config.GoMod{
+				Proxy: true,
+			},
+			Builds: []config.Build{
+				{
+					ID:     "foo",
+					Goos:   []string{runtime.GOOS},
+					Goarch: []string{runtime.GOARCH},
+					Main:   ".",
+				},
+			},
+		})
+		ctx.Git.CurrentTag = "v0.161.1"
+
+		mod := "github.com/goreleaser/goreleaser"
+
+		fakeGoModAndSum(t, mod)
+		require.NoError(t, Pipe{}.Default(ctx))
+		require.NoError(t, Pipe{}.Run(ctx))
+		requireGoMod(t, mod, ctx.Git.CurrentTag)
+		requireMainGo(t, mod)
+		require.Equal(t, mod, ctx.Config.Builds[0].Main)
+		require.Equal(t, filepath.Join(dist, "proxy", "foo"), ctx.Config.Builds[0].Dir)
+		require.Equal(t, mod, ctx.ModulePath)
+	})
+
+	t.Run("nfpm", func(t *testing.T) {
+		dir := testlib.Mktmp(t)
+		dist := filepath.Join(dir, "dist")
+		ctx := context.New(config.Project{
+			Dist: dist,
+			GoMod: config.GoMod{
+				Proxy: true,
+			},
+			Builds: []config.Build{
+				{
+					ID:     "foo",
+					Goos:   []string{runtime.GOOS},
+					Goarch: []string{runtime.GOARCH},
+					Main:   "./cmd/nfpm",
+				},
+			},
+		})
+		ctx.Git.CurrentTag = "v2.3.1"
+
+		mod := "github.com/goreleaser/nfpm/v2"
+		fakeGoModAndSum(t, mod)
+		require.NoError(t, Pipe{}.Default(ctx))
+		require.NoError(t, Pipe{}.Run(ctx))
+		requireGoMod(t, mod, ctx.Git.CurrentTag)
+		requireMainGo(t, mod+"/cmd/nfpm")
+		require.Equal(t, mod+"/cmd/nfpm", ctx.Config.Builds[0].Main)
+		require.Equal(t, filepath.Join(dist, "proxy", "foo"), ctx.Config.Builds[0].Dir)
+		require.Equal(t, mod, ctx.ModulePath)
+	})
+
+	// this repo does not have a go.sum file, which is ok, a project might not have any dependencies
+	t.Run("no go.sum", func(t *testing.T) {
+		dir := testlib.Mktmp(t)
+		dist := filepath.Join(dir, "dist")
+		ctx := context.New(config.Project{
+			Dist: dist,
+			GoMod: config.GoMod{
+				Proxy: true,
+			},
+			Builds: []config.Build{
+				{
+					ID:     "foo",
+					Goos:   []string{runtime.GOOS},
+					Goarch: []string{runtime.GOARCH},
+				},
+			},
+		})
+		ctx.Git.CurrentTag = "v0.0.1"
+
+		mod := "github.com/goreleaser/example-mod-proxy"
+		fakeGoMod(t, mod)
+		require.NoError(t, Pipe{}.Default(ctx))
+		require.NoError(t, Pipe{}.Run(ctx))
+		requireGoMod(t, mod, ctx.Git.CurrentTag)
+		requireMainGo(t, mod)
+		require.Equal(t, mod, ctx.Config.Builds[0].Main)
+		require.Equal(t, filepath.Join(dist, "proxy", "foo"), ctx.Config.Builds[0].Dir)
+		require.Equal(t, mod, ctx.ModulePath)
+	})
+
+	t.Run("no perms", func(t *testing.T) {
+		for file, mode := range map[string]os.FileMode{
+			"go.mod":          0o500,
+			"go.sum":          0o500,
+			"main.go":         0o500,
+			"../../../go.sum": 0o300,
+		} {
+			t.Run(file, func(t *testing.T) {
+				dir := testlib.Mktmp(t)
+				dist := filepath.Join(dir, "dist")
+				ctx := context.New(config.Project{
+					Dist: dist,
+					GoMod: config.GoMod{
+						Proxy: true,
+					},
+					Builds: []config.Build{
+						{
+							ID:     "foo",
+							Goos:   []string{runtime.GOOS},
+							Goarch: []string{runtime.GOARCH},
+						},
+					},
+				})
+				ctx.Git.CurrentTag = "v0.161.1"
+
+				mod := "github.com/goreleaser/goreleaser"
+
+				fakeGoModAndSum(t, mod)
+				require.NoError(t, Pipe{}.Default(ctx))
+				require.NoError(t, Pipe{}.Run(ctx)) // should succeed at first
+
+				// change perms of a file and run again, which should now fail on that file.
+				require.NoError(t, os.Chmod(filepath.Join(dist, "proxy", "foo", file), mode))
+				require.ErrorAs(t, Pipe{}.Run(ctx), &ErrProxy{})
+			})
+		}
+	})
+}
+
+func requireGoMod(tb testing.TB, module, version string) {
+	tb.Helper()
+
+	mod, err := os.ReadFile("dist/proxy/foo/go.mod")
+	require.NoError(tb, err)
+	require.Equal(tb, fmt.Sprintf(`module foo
+
+go 1.16
+
+require %s %s
+`, module, version), string(mod))
+}
+
+func requireMainGo(tb testing.TB, module string) {
+	tb.Helper()
+
+	main, err := os.ReadFile("dist/proxy/foo/main.go")
+	require.NoError(tb, err)
+	require.Equal(tb, fmt.Sprintf(`
+// +build main
+package main
+
+import _ "%s"
+`, module), string(main))
+}
+
+func fakeGoModAndSum(tb testing.TB, module string) {
+	tb.Helper()
+
+	fakeGoMod(tb, module)
+	require.NoError(tb, os.WriteFile("go.sum", []byte("\n"), 0o666))
+}
+
+func fakeGoMod(tb testing.TB, module string) {
+	tb.Helper()
+	require.NoError(tb, os.WriteFile("go.mod", []byte(fmt.Sprintf("module %s\n", module)), 0o666))
+}