cmd/gorelease: don't suggest a version that already exists

Fixes golang/go#37562

Change-Id: Ie02cfaa9efc8c8375481540e551ae38f19c3a2e8
Reviewed-on: https://go-review.googlesource.com/c/exp/+/288032
Trust: Jean de Klerk <deklerk@google.com>
Run-TryBot: Jean de Klerk <deklerk@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/cmd/gorelease/gorelease.go b/cmd/gorelease/gorelease.go
index 3e09351..124ad0d 100644
--- a/cmd/gorelease/gorelease.go
+++ b/cmd/gorelease/gorelease.go
@@ -81,6 +81,7 @@
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"errors"
 	"flag"
@@ -141,7 +142,8 @@
 	if err != nil {
 		log.Fatal(err)
 	}
-	success, err := runRelease(os.Stdout, wd, os.Args[1:])
+	ctx := context.WithValue(context.Background(), "env", append(os.Environ(), "GO111MODULE=on"))
+	success, err := runRelease(ctx, os.Stdout, wd, os.Args[1:])
 	if err != nil {
 		if _, ok := err.(*usageError); ok {
 			fmt.Fprintln(os.Stderr, err)
@@ -158,7 +160,7 @@
 // runRelease is the main function of gorelease. It's called by tests, so
 // it writes to w instead of os.Stdout and returns an error instead of
 // exiting.
-func runRelease(w io.Writer, dir string, args []string) (success bool, err error) {
+func runRelease(ctx context.Context, w io.Writer, dir string, args []string) (success bool, err error) {
 	// Validate arguments and flags. We'll print our own errors, since we want to
 	// test without printing to stderr.
 	fs := flag.NewFlagSet("gorelease", flag.ContinueOnError)
@@ -213,7 +215,7 @@
 	repoRoot := findRepoRoot(modRoot)
 
 	// Load packages for the version to be released from the local directory.
-	release, err := loadLocalModule(modRoot, repoRoot, releaseVersion)
+	release, err := loadLocalModule(ctx, modRoot, repoRoot, releaseVersion)
 	if err != nil {
 		return false, err
 	}
@@ -225,13 +227,13 @@
 		baseModPath = release.modPath
 		max = releaseVersion
 	}
-	base, err := loadDownloadedModule(baseModPath, baseVersion, max)
+	base, err := loadDownloadedModule(ctx, baseModPath, baseVersion, max)
 	if err != nil {
 		return false, err
 	}
 
 	// Compare packages and check for other issues.
-	report, err := makeReleaseReport(base, release)
+	report, err := makeReleaseReport(ctx, base, release)
 	if err != nil {
 		return false, err
 	}
@@ -259,6 +261,10 @@
 
 	diagnostics []string            // problems not related to loading specific packages
 	pkgs        []*packages.Package // loaded packages with type information
+
+	// Versions of this module which already exist. Only loaded for release
+	// (not base).
+	existingVersions []string
 }
 
 // loadLocalModule loads information about a module and its packages from a
@@ -269,7 +275,7 @@
 // repoRoot is the root directory of the repository containing the module or "".
 //
 // version is a proposed version for the module or "".
-func loadLocalModule(modRoot, repoRoot, version string) (m moduleInfo, err error) {
+func loadLocalModule(ctx context.Context, modRoot, repoRoot, version string) (m moduleInfo, err error) {
 	if repoRoot != "" && !hasFilePathPrefix(modRoot, repoRoot) {
 		return moduleInfo{}, fmt.Errorf("module root %q is not in repository root %q", modRoot, repoRoot)
 	}
@@ -373,7 +379,7 @@
 			err = fmt.Errorf("removing temporary module directory: %v", rerr)
 		}
 	}()
-	tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, prepareDiagnostics, err := prepareLoadDir(m.goModFile, m.modPath, tmpModRoot, version, false)
+	tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, prepareDiagnostics, err := prepareLoadDir(ctx, m.goModFile, m.modPath, tmpModRoot, version, false)
 	if err != nil {
 		return moduleInfo{}, err
 	}
@@ -384,7 +390,7 @@
 	}()
 
 	var loadDiagnostics []string
-	m.pkgs, loadDiagnostics, err = loadPackages(m.modPath, tmpModRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths)
+	m.pkgs, loadDiagnostics, err = loadPackages(ctx, m.modPath, tmpModRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths)
 	if err != nil {
 		return moduleInfo{}, err
 	}
@@ -392,7 +398,7 @@
 	m.diagnostics = append(m.diagnostics, prepareDiagnostics...)
 	m.diagnostics = append(m.diagnostics, loadDiagnostics...)
 
-	highestVersion, err := findSelectedVersion(tmpLoadDir, m.modPath)
+	highestVersion, err := findSelectedVersion(ctx, tmpLoadDir, m.modPath)
 	if err != nil {
 		return moduleInfo{}, err
 	}
@@ -405,6 +411,13 @@
 		m.highestTransitiveVersion = highestVersion
 	}
 
+	// Calculate the existing versions.
+	ev, err := existingVersions(ctx, m.modPath, tmpLoadDir)
+	if err != nil {
+		return moduleInfo{}, err
+	}
+	m.existingVersions = ev
+
 	return m, nil
 }
 
@@ -422,7 +435,7 @@
 // If version is "" and max is not "", available versions greater than or equal
 // to max will not be considered. Typically, loadDownloadedModule is used to
 // load the base version, and max is the release version.
-func loadDownloadedModule(modPath, version, max string) (m moduleInfo, err error) {
+func loadDownloadedModule(ctx context.Context, modPath, version, max string) (m moduleInfo, err error) {
 	// Check the module path and version.
 	// If the version is a query, resolve it to a canonical version.
 	m = moduleInfo{modPath: modPath}
@@ -445,7 +458,7 @@
 	if version == "" {
 		// Unspecified version: use the highest version below max.
 		m.versionInferred = true
-		if m.version, err = inferBaseVersion(modPath, max); err != nil {
+		if m.version, err = inferBaseVersion(ctx, modPath, max); err != nil {
 			return moduleInfo{}, err
 		}
 		if m.version == "none" {
@@ -454,7 +467,7 @@
 	} else if version != module.CanonicalVersion(version) {
 		// Version query: find the real version.
 		m.versionQuery = version
-		if m.version, err = queryVersion(modPath, version); err != nil {
+		if m.version, err = queryVersion(ctx, modPath, version); err != nil {
 			return moduleInfo{}, err
 		}
 		if m.version != "none" && max != "" && semver.Compare(m.version, max) >= 0 {
@@ -479,7 +492,7 @@
 	// which is not inside modRoot. This is what the go command uses. Even if
 	// the module didn't have a go.mod file, one will be synthesized there.
 	v := module.Version{Path: modPath, Version: m.version}
-	if m.modRoot, m.goModPath, err = downloadModule(v); err != nil {
+	if m.modRoot, m.goModPath, err = downloadModule(ctx, v); err != nil {
 		return moduleInfo{}, err
 	}
 	if m.goModData, err = ioutil.ReadFile(m.goModPath); err != nil {
@@ -494,7 +507,7 @@
 	m.modPath = m.goModFile.Module.Mod.Path
 
 	// Load packages.
-	tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, _, err := prepareLoadDir(nil, m.modPath, m.modRoot, m.version, true)
+	tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, _, err := prepareLoadDir(ctx, nil, m.modPath, m.modRoot, m.version, true)
 	if err != nil {
 		return moduleInfo{}, err
 	}
@@ -504,7 +517,7 @@
 		}
 	}()
 
-	if m.pkgs, _, err = loadPackages(m.modPath, m.modRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths); err != nil {
+	if m.pkgs, _, err = loadPackages(ctx, m.modPath, m.modRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths); err != nil {
 		return moduleInfo{}, err
 	}
 
@@ -518,7 +531,7 @@
 // The report recommends or validates a release version and indicates a
 // version control tag to use (with an appropriate prefix, for modules not
 // in the repository root directory).
-func makeReleaseReport(base, release moduleInfo) (report, error) {
+func makeReleaseReport(ctx context.Context, base, release moduleInfo) (report, error) {
 	// Compare each pair of packages.
 	// Ignore internal packages.
 	// If we don't have a base version to compare against just check the new
@@ -609,6 +622,38 @@
 	return r, nil
 }
 
+// existingVersions returns the versions that already exist for the given
+// modPath.
+func existingVersions(ctx context.Context, modPath, modRoot string) (versions []string, err error) {
+	defer func() {
+		if err != nil {
+			err = fmt.Errorf("listing versions of %s: %w", modPath, err)
+		}
+	}()
+
+	type listVersions struct {
+		Versions []string `json: "Versions"`
+	}
+	cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-versions", modPath)
+	if env, ok := ctx.Value("env").([]string); ok {
+		cmd.Env = env
+	}
+	cmd.Dir = modRoot
+	out, err := cmd.Output()
+	if err != nil {
+		return nil, cleanCmdError(err)
+	}
+	if len(out) == 0 {
+		return nil, nil
+	}
+
+	var lv listVersions
+	if err := json.Unmarshal(out, &lv); err != nil {
+		return nil, err
+	}
+	return lv.Versions, nil
+}
+
 // findRepoRoot finds the root directory of the repository that contains dir.
 // findRepoRoot returns "" if it can't find the repository root.
 func findRepoRoot(dir string) string {
@@ -671,14 +716,14 @@
 // highest available release version. Pre-release versions are not considered.
 // If there is no available version, and max appears to be the first release
 // version (for example, "v0.1.0", "v2.0.0"), "none" is returned.
-func inferBaseVersion(modPath, max string) (baseVersion string, err error) {
+func inferBaseVersion(ctx context.Context, modPath, max string) (baseVersion string, err error) {
 	defer func() {
 		if err != nil {
 			err = &baseVersionError{err: err}
 		}
 	}()
 
-	versions, err := loadVersions(modPath)
+	versions, err := loadVersions(ctx, modPath)
 	if err != nil {
 		return "", err
 	}
@@ -698,7 +743,7 @@
 }
 
 // queryVersion returns the canonical version for a given module version query.
-func queryVersion(modPath, query string) (resolved string, err error) {
+func queryVersion(ctx context.Context, modPath, query string) (resolved string, err error) {
 	defer func() {
 		if err != nil {
 			err = fmt.Errorf("could not resolve version %s@%s: %w", modPath, query, err)
@@ -718,9 +763,12 @@
 		}
 	}()
 	arg := modPath + "@" + query
-	cmd := exec.Command("go", "list", "-m", "-f", "{{.Version}}", "--", arg)
+	cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", arg)
+	if env, ok := ctx.Value("env").([]string); ok {
+		cmd.Env = env
+	}
 	cmd.Dir = tmpDir
-	cmd.Env = append(os.Environ(), "GO111MODULE=on")
+	cmd.Env = append(cmd.Env, "GO111MODULE=on")
 	out, err := cmd.Output()
 	if err != nil {
 		return "", cleanCmdError(err)
@@ -731,7 +779,13 @@
 // loadVersions loads the list of versions for the given module using
 // 'go list -m -versions'. The returned versions are sorted in ascending
 // semver order.
-func loadVersions(modPath string) ([]string, error) {
+func loadVersions(ctx context.Context, modPath string) (versions []string, err error) {
+	defer func() {
+		if err != nil {
+			err = fmt.Errorf("could not load versions for %s: %v", modPath, err)
+		}
+	}()
+
 	tmpDir, err := ioutil.TempDir("", "")
 	if err != nil {
 		return nil, err
@@ -741,14 +795,17 @@
 			err = rerr
 		}
 	}()
-	cmd := exec.Command("go", "list", "-m", "-versions", "--", modPath)
+	cmd := exec.CommandContext(ctx, "go", "list", "-m", "-versions", "--", modPath)
+	if env, ok := ctx.Value("env").([]string); ok {
+		cmd.Env = env
+	}
 	cmd.Dir = tmpDir
-	cmd.Env = append(os.Environ(), "GO111MODULE=on")
+	cmd.Env = append(cmd.Env, "GO111MODULE=on")
 	out, err := cmd.Output()
 	if err != nil {
 		return nil, cleanCmdError(err)
 	}
-	versions := strings.Fields(string(out))
+	versions = strings.Fields(string(out))
 	if len(versions) > 0 {
 		versions = versions[1:] // skip module path
 	}
@@ -847,7 +904,7 @@
 
 // downloadModule downloads a specific version of a module to the
 // module cache using 'go mod download'.
-func downloadModule(m module.Version) (modRoot, goModPath string, err error) {
+func downloadModule(ctx context.Context, m module.Version) (modRoot, goModPath string, err error) {
 	defer func() {
 		if err != nil {
 			err = &downloadError{m: m, err: cleanCmdError(err)}
@@ -865,7 +922,10 @@
 		return "", "", err
 	}
 	defer os.Remove(tmpDir)
-	cmd := exec.Command("go", "mod", "download", "-json", "--", m.Path+"@"+m.Version)
+	cmd := exec.CommandContext(ctx, "go", "mod", "download", "-json", "--", m.Path+"@"+m.Version)
+	if env, ok := ctx.Value("env").([]string); ok {
+		cmd.Env = env
+	}
 	cmd.Dir = tmpDir
 	out, err := cmd.Output()
 	var xerr *exec.ExitError
@@ -931,7 +991,7 @@
 //
 // pkgPaths are the import paths of the module being loaded, including the path
 // to any main packages (as if they were importable).
-func prepareLoadDir(modFile *modfile.File, modPath, modRoot, version string, cached bool) (dir string, goModData, goSumData []byte, pkgPaths []string, diagnostics []string, err error) {
+func prepareLoadDir(ctx context.Context, modFile *modfile.File, modPath, modRoot, version string, cached bool) (dir string, goModData, goSumData []byte, pkgPaths []string, diagnostics []string, err error) {
 	defer func() {
 		if err != nil {
 			if cached {
@@ -1003,7 +1063,10 @@
 	}
 
 	// Add missing requirements.
-	cmd := exec.Command("go", "get", "-d", ".")
+	cmd := exec.CommandContext(ctx, "go", "get", "-d", ".")
+	if env, ok := ctx.Value("env").([]string); ok {
+		cmd.Env = env
+	}
 	cmd.Dir = dir
 	if _, err := cmd.Output(); err != nil {
 		return "", nil, nil, nil, nil, fmt.Errorf("looking for missing dependencies: %w", cleanCmdError(err))
@@ -1011,17 +1074,15 @@
 
 	// Report new requirements in go.mod.
 	goModPath := filepath.Join(dir, "go.mod")
-	loadReqs := func(data []byte) ([]string, error) {
+	loadReqs := func(data []byte) (reqs []module.Version, err error) {
 		modFile, err := modfile.ParseLax(goModPath, data, nil)
 		if err != nil {
 			return nil, err
 		}
-		lines := make([]string, len(modFile.Require))
-		for i, req := range modFile.Require {
-			lines[i] = req.Mod.String()
+		for _, r := range modFile.Require {
+			reqs = append(reqs, r.Mod)
 		}
-		sort.Strings(lines)
-		return lines, nil
+		return reqs, nil
 	}
 
 	oldReqs, err := loadReqs(goModData)
@@ -1037,19 +1098,27 @@
 		return "", nil, nil, nil, nil, err
 	}
 
-	oldMap := make(map[string]bool)
+	oldMap := make(map[module.Version]bool)
 	for _, req := range oldReqs {
 		oldMap[req] = true
 	}
-	var missing []string
+	var missing []module.Version
 	for _, req := range newReqs {
+		// Ignore cyclic imports, since a module never needs to require itself.
+		if req.Path == modPath {
+			continue
+		}
 		if !oldMap[req] {
 			missing = append(missing, req)
 		}
 	}
 
 	if len(missing) > 0 {
-		diagnostics = append(diagnostics, fmt.Sprintf("go.mod: the following requirements are needed\n\t%s\nRun 'go mod tidy' to add missing requirements.", strings.Join(missing, "\n\t")))
+		var missingReqs []string
+		for _, m := range missing {
+			missingReqs = append(missingReqs, m.String())
+		}
+		diagnostics = append(diagnostics, fmt.Sprintf("go.mod: the following requirements are needed\n\t%s\nRun 'go mod tidy' to add missing requirements.", strings.Join(missingReqs, "\n\t")))
 		return dir, goModData, goSumData, imps, diagnostics, nil
 	}
 
@@ -1137,14 +1206,18 @@
 // returned through diagnostics.
 // err will be non-nil in case of a fatal error that prevented packages
 // from being loaded.
-func loadPackages(modPath, modRoot, loadDir string, goModData, goSumData []byte, pkgPaths []string) (pkgs []*packages.Package, diagnostics []string, err error) {
+func loadPackages(ctx context.Context, modPath, modRoot, loadDir string, goModData, goSumData []byte, pkgPaths []string) (pkgs []*packages.Package, diagnostics []string, err error) {
 	// Load packages.
 	// TODO(jayconrod): if there are errors loading packages in the release
 	// version, try loading in the release directory. Errors there would imply
 	// that packages don't load without replace / exclude directives.
 	cfg := &packages.Config{
-		Mode: packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
-		Dir:  loadDir,
+		Mode:    packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
+		Dir:     loadDir,
+		Context: ctx,
+	}
+	if env, ok := ctx.Value("env").([]string); ok {
+		cfg.Env = env
 	}
 	if len(pkgPaths) > 0 {
 		pkgs, err = packages.Load(cfg, pkgPaths...)
@@ -1219,8 +1292,17 @@
 // containing the go.mod for modPath.
 //
 // If no module cycle exists, it returns empty string.
-func findSelectedVersion(modDir, modPath string) (latestVersion string, err error) {
-	cmd := exec.Command("go", "list", "-m", "-f", "{{.Version}}", "--", modPath)
+func findSelectedVersion(ctx context.Context, modDir, modPath string) (latestVersion string, err error) {
+	defer func() {
+		if err != nil {
+			err = fmt.Errorf("could not find selected version for %s: %v", modPath, err)
+		}
+	}()
+
+	cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", modPath)
+	if env, ok := ctx.Value("env").([]string); ok {
+		cmd.Env = env
+	}
 	cmd.Dir = modDir
 	out, err := cmd.Output()
 	if err != nil {
diff --git a/cmd/gorelease/gorelease_test.go b/cmd/gorelease/gorelease_test.go
index a29f438..fca52c1 100644
--- a/cmd/gorelease/gorelease_test.go
+++ b/cmd/gorelease/gorelease_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"bytes"
+	"context"
 	"flag"
 	"fmt"
 	"io/ioutil"
@@ -16,62 +17,58 @@
 	"strings"
 	"testing"
 
+	"golang.org/x/mod/module"
 	"golang.org/x/tools/txtar"
 )
 
-var workDir string
-
 var (
 	testwork     = flag.Bool("testwork", false, "preserve work directory")
 	updateGolden = flag.Bool("u", false, "update expected text in test files instead of failing")
 )
 
-func TestMain(m *testing.M) {
-	status := 1
-	defer func() {
-		if !*testwork && workDir != "" {
-			os.RemoveAll(workDir)
-		}
-		os.Exit(status)
-	}()
+// prepareProxy creates a proxy dir and returns an associated ctx.
+//
+// proxyVersions must be a map of module version to true. If proxyVersions is
+// empty, all modules in mod/ will be included in the proxy list. If proxy
+// versions is non-empty, only those modules in mod/ that match an entry in
+// proxyVersions will be included.
+//
+// ctx must be used in runRelease.
+// cleanup must be called when the relevant tests are finished.
+func prepareProxy(proxyVersions map[module.Version]bool, tests []*test) (ctx context.Context, cleanup func(), _ error) {
+	env := append(os.Environ(), "GO111MODULE=on", "GOSUMDB=off")
 
-	flag.Parse()
-
-	proxyDir, proxyURL, err := buildProxyDir()
+	proxyDir, proxyURL, err := buildProxyDir(proxyVersions, tests)
 	if err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		return
+		return nil, nil, fmt.Errorf("error building proxy dir: %v", err)
 	}
-	os.Setenv("GOPROXY", proxyURL)
-	if *testwork {
-		fmt.Fprintf(os.Stderr, "test proxy dir: %s\ntest proxy URL: %s\n", proxyDir, proxyURL)
-	} else {
-		defer os.RemoveAll(proxyDir)
-	}
+	env = append(env, fmt.Sprintf("GOPROXY=%s", proxyURL))
 
 	cacheDir, err := ioutil.TempDir("", "gorelease_test-gocache")
 	if err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		return
+		return nil, nil, err
 	}
-	os.Setenv("GOPATH", cacheDir)
-	if *testwork {
-		fmt.Fprintf(os.Stderr, "test cache dir: %s\n", cacheDir)
-	} else {
-		defer func() {
-			if err := exec.Command("go", "clean", "-modcache").Run(); err != nil {
-				fmt.Fprintln(os.Stderr, err)
+	env = append(env, fmt.Sprintf("GOPATH=%s", cacheDir))
+
+	return context.WithValue(context.Background(), "env", env), func() {
+		if *testwork {
+			fmt.Fprintf(os.Stderr, "test cache dir: %s\n", cacheDir)
+			fmt.Fprintf(os.Stderr, "test proxy dir: %s\ntest proxy URL: %s\n", proxyDir, proxyURL)
+		} else {
+			cmd := exec.Command("go", "clean", "-modcache")
+			cmd.Env = env
+			if err := cmd.Run(); err != nil {
+				fmt.Fprintln(os.Stderr, fmt.Errorf("error running go clean: %v", err))
 			}
+
 			if err := os.RemoveAll(cacheDir); err != nil {
-				fmt.Fprintln(os.Stderr, err)
+				fmt.Fprintln(os.Stderr, fmt.Errorf("error removing cache dir %s: %v", cacheDir, err))
 			}
-		}()
-	}
-
-	os.Setenv("GO111MODULE", "on")
-	os.Setenv("GOSUMDB", "off")
-
-	status = m.Run()
+			if err := os.RemoveAll(proxyDir); err != nil {
+				fmt.Fprintln(os.Stderr, fmt.Errorf("error removing proxy dir %s: %v", proxyDir, err))
+			}
+		}
+	}, nil
 }
 
 // test describes an individual test case, written as a .test file in the
@@ -126,6 +123,13 @@
 
 	// want is set to the contents of the file named "want" in the txtar archive.
 	want []byte
+
+	// proxyVersions is used to set the exact contents of the GOPROXY.
+	//
+	// If empty, all of testadata/mod/ will be included in the proxy.
+	// If it is not empty, each entry must be of the form <modpath>@v<version>
+	// and exist in testdata/mod/.
+	proxyVersions map[module.Version]bool
 }
 
 // readTest reads and parses a .test file with the given name.
@@ -180,6 +184,22 @@
 			if err != nil {
 				return nil, fmt.Errorf("%s:%d: %v", testPath, lineNum, err)
 			}
+		case "proxyVersions":
+			parts := strings.Split(value, ",")
+			proxyVersions := make(map[module.Version]bool)
+			for _, modpathWithVersion := range parts {
+				vParts := strings.Split(modpathWithVersion, "@")
+				if len(vParts) != 2 {
+					return nil, fmt.Errorf("proxyVersions entry %s is invalid: it should be of the format <modpath>@v<semver> (ex: github.com/foo/bar@v1.2.3)", modpathWithVersion)
+				}
+				modPath, version := vParts[0], vParts[1]
+				mv := module.Version{
+					Path:    modPath,
+					Version: version,
+				}
+				proxyVersions[mv] = true
+			}
+			t.proxyVersions = proxyVersions
 		default:
 			return nil, fmt.Errorf("%s:%d: unknown key: %q", testPath, lineNum, key)
 		}
@@ -233,21 +253,41 @@
 		t.Fatal("no .test files found in testdata directory")
 	}
 
+	var tests []*test
 	for _, testPath := range testPaths {
-		testPath := testPath
-		testName := strings.TrimSuffix(strings.TrimPrefix(filepath.ToSlash(testPath), "testdata/"), ".test")
-		t.Run(testName, func(t *testing.T) {
-			t.Parallel()
+		test, err := readTest(testPath)
+		if err != nil {
+			t.Fatal(err)
+		}
+		tests = append(tests, test)
+	}
 
-			test, err := readTest(testPath)
-			if err != nil {
-				t.Fatal(err)
-			}
+	defaultContext, cleanup, err := prepareProxy(nil, tests)
+	if err != nil {
+		t.Fatalf("preparing test proxy: %v", err)
+	}
+	t.Cleanup(cleanup)
+
+	for _, test := range tests {
+		testName := strings.TrimSuffix(strings.TrimPrefix(filepath.ToSlash(test.testPath), "testdata/"), ".test")
+		t.Run(testName, func(t *testing.T) {
+			ctx := defaultContext
 
 			if test.skip != "" {
 				t.Skip(test.skip)
 			}
 
+			t.Parallel()
+
+			if len(test.proxyVersions) > 0 {
+				var cleanup func()
+				ctx, cleanup, err = prepareProxy(test.proxyVersions, tests)
+				if err != nil {
+					t.Fatalf("preparing test proxy: %v", err)
+				}
+				t.Cleanup(cleanup)
+			}
+
 			// Extract the files in the release version. They may be part of the
 			// test archive or in testdata/mod.
 			testDir, err := ioutil.TempDir("", "")
@@ -257,7 +297,9 @@
 			if *testwork {
 				fmt.Fprintf(os.Stderr, "test dir: %s\n", testDir)
 			} else {
-				defer os.RemoveAll(testDir)
+				t.Cleanup(func() {
+					os.RemoveAll(testDir)
+				})
 			}
 
 			var arc *txtar.Archive
@@ -286,7 +328,7 @@
 			}
 			buf := &bytes.Buffer{}
 			releaseDir := filepath.Join(testDir, test.dir)
-			success, err := runRelease(buf, releaseDir, args)
+			success, err := runRelease(ctx, buf, releaseDir, args)
 			if err != nil {
 				if !test.wantError {
 					t.Fatalf("unexpected error: %v", err)
diff --git a/cmd/gorelease/proxy_test.go b/cmd/gorelease/proxy_test.go
index 8aaebea..d91d983 100644
--- a/cmd/gorelease/proxy_test.go
+++ b/cmd/gorelease/proxy_test.go
@@ -24,22 +24,33 @@
 // buildProxyDir constructs a temporary directory suitable for use as a
 // module proxy with a file:// URL. The caller is responsible for deleting
 // the directory when it's no longer needed.
-func buildProxyDir() (proxyDir, proxyURL string, err error) {
+//
+// proxyVersions must be a map of module version true. If proxyVersions is
+// empty, all modules in mod/ will be included in the proxy list. If proxy
+// versions is non-empty, only those modules in mod/ that match an entry in
+// proxyVersions will be included.
+func buildProxyDir(proxyVersions map[module.Version]bool, tests []*test) (proxyDir, proxyURL string, err error) {
 	proxyDir, err = ioutil.TempDir("", "gorelease-proxy")
 	if err != nil {
 		return "", "", err
 	}
-	defer func(proxyDir string) {
-		if err != nil {
-			os.RemoveAll(proxyDir)
-		}
-	}(proxyDir)
 
 	txtarPaths, err := filepath.Glob(filepath.FromSlash("testdata/mod/*.txt"))
 	if err != nil {
 		return "", "", err
 	}
+
+	// Map of modPath to versions for that modPath.
 	versionLists := make(map[string][]string)
+
+	for _, t := range tests {
+		versionLists[t.modPath] = []string{}
+		modDir := filepath.Join(proxyDir, t.modPath, "@v")
+		if err := os.MkdirAll(modDir, 0777); err != nil {
+			return "", "", err
+		}
+	}
+
 	for _, txtarPath := range txtarPaths {
 		base := filepath.Base(txtarPath)
 		stem := base[:len(base)-len(".txt")]
@@ -49,6 +60,20 @@
 		}
 		modPath := strings.ReplaceAll(stem[:i], "_", "/")
 		version := stem[i+1:]
+		mv := module.Version{
+			Path:    modPath,
+			Version: version,
+		}
+
+		// User has supplied proxyVersions. Honor proxy versions by only
+		// accepting those versions supplied in proxyVersions.
+		if len(proxyVersions) > 0 {
+			if !proxyVersions[mv] {
+				// modPath@version is not in proxyVersions: skip.
+				continue
+			}
+		}
+
 		versionLists[modPath] = append(versionLists[modPath], version)
 
 		modDir := filepath.Join(proxyDir, modPath, "@v")
diff --git a/cmd/gorelease/report.go b/cmd/gorelease/report.go
index aede072..0dbeca1 100644
--- a/cmd/gorelease/report.go
+++ b/cmd/gorelease/report.go
@@ -178,6 +178,12 @@
 		return
 	}
 
+	for _, v := range r.release.existingVersions {
+		if semver.Compare(v, r.release.version) == 0 {
+			setNotValid("version %s already exists", v)
+		}
+	}
+
 	// Check that compatible / incompatible changes are consistent.
 	if semver.Major(r.base.version) == "v0" || r.base.modPath != r.release.modPath {
 		return
@@ -244,6 +250,24 @@
 		// and link to documentation.
 	}
 
+	// Check whether we're comparing to the latest version of base.
+	//
+	// This could happen further up, but we want the more pressing errors above
+	// to take precedence.
+	var latestForBaseMajor string
+	for _, v := range r.release.existingVersions {
+		if semver.Major(v) != semver.Major(r.base.version) {
+			continue
+		}
+		if latestForBaseMajor == "" || semver.Compare(latestForBaseMajor, v) < 0 {
+			latestForBaseMajor = v
+		}
+	}
+	if latestForBaseMajor != "" && latestForBaseMajor != r.base.version {
+		setNotValid(fmt.Sprintf("Can only suggest a release version when compared against the most recent version of this major: %s.", latestForBaseMajor))
+		return
+	}
+
 	if r.base.version == "none" {
 		if _, pathMajor, ok := module.SplitPathVersion(r.release.modPath); !ok {
 			panic(fmt.Sprintf("could not parse module path %q", r.release.modPath))
diff --git a/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_major.test b/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_major.test
new file mode 100644
index 0000000..63cb373
--- /dev/null
+++ b/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_major.test
@@ -0,0 +1,24 @@
+mod=example.com/basic
+base=v1.0.1
+success=false
+# A() was removed, which is a breaking change: it shouldn't try to suggest a
+# higher version.
+-- want --
+example.com/basic/a
+-------------------
+Incompatible changes:
+- A: removed
+Compatible changes:
+- B: added
+
+Cannot suggest a release version.
+Incompatible changes were detected.
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func B() int { return 0 }
+
diff --git a/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_minor.test b/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_minor.test
new file mode 100644
index 0000000..cb43f45
--- /dev/null
+++ b/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_minor.test
@@ -0,0 +1,23 @@
+mod=example.com/basic
+base=v0.0.1
+success=false
+# B() was added, so now it should suggest a new minor version. # But, there's a
+# later version that already exists: so it should not try to suggest anything at
+# all.
+-- want --
+example.com/basic/a
+-------------------
+Compatible changes:
+- B: added
+
+Cannot suggest a release version.
+Can only suggest a release version when compared against the most recent version of this major: v0.1.2.
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
+func B() int { return 0 }
diff --git a/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_patch.test b/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_patch.test
new file mode 100644
index 0000000..81ed36c
--- /dev/null
+++ b/cmd/gorelease/testdata/alreadyexists/alreadyexists_suggest_patch.test
@@ -0,0 +1,22 @@
+mod=example.com/basic
+base=v0.1.0
+success=false
+# A() was changed in a small way, so now it should suggest a new patch version.
+# But, there's a later version that already exists: so it should not try to
+# suggest anything at all.
+-- want --
+Cannot suggest a release version.
+Can only suggest a release version when compared against the most recent version of this major: v0.1.2.
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 1 }
+func A2() int { return 2 }
+-- b/b.go --
+package b
+
+func B() int { return 3 }
diff --git a/cmd/gorelease/testdata/alreadyexists/alreadyexists_verify.test b/cmd/gorelease/testdata/alreadyexists/alreadyexists_verify.test
new file mode 100644
index 0000000..969606a
--- /dev/null
+++ b/cmd/gorelease/testdata/alreadyexists/alreadyexists_verify.test
@@ -0,0 +1,17 @@
+mod=example.com/basic
+base=v0.0.1
+release=v0.1.0
+success=false
+# The contents below are a copy of the v0.0.1 contents - nothing has changed.
+# But v0.1.0 already exists, so it should present a diagnostic.
+-- want --
+v0.1.0 is not a valid semantic version for this release.
+version v0.1.0 already exists
+-- go.mod --
+module example.com/basic
+
+go 1.12
+-- a/a.go --
+package a
+
+func A() int { return 0 }
diff --git a/cmd/gorelease/testdata/basic/v0_compatible_suggest.test b/cmd/gorelease/testdata/basic/v0_compatible_suggest.test
index 32029f1..014fe08 100644
--- a/cmd/gorelease/testdata/basic/v0_compatible_suggest.test
+++ b/cmd/gorelease/testdata/basic/v0_compatible_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic
 version=v0.1.0
 base=v0.0.1
+proxyVersions=example.com/basic@v0.0.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v0_compatible_verify.test b/cmd/gorelease/testdata/basic/v0_compatible_verify.test
index 8324045..5461eea 100644
--- a/cmd/gorelease/testdata/basic/v0_compatible_verify.test
+++ b/cmd/gorelease/testdata/basic/v0_compatible_verify.test
@@ -2,6 +2,7 @@
 version=v0.1.0
 base=v0.0.1
 release=v0.1.0
+proxyVersions=example.com/basic@v0.0.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v0_incompatible_suggest.test b/cmd/gorelease/testdata/basic/v0_incompatible_suggest.test
index 312b6b3..4aa8364 100644
--- a/cmd/gorelease/testdata/basic/v0_incompatible_suggest.test
+++ b/cmd/gorelease/testdata/basic/v0_incompatible_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic
 version=v0.1.2
 base=v0.1.1
+proxyVersions=example.com/basic@v0.1.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v0_incompatible_verify.test b/cmd/gorelease/testdata/basic/v0_incompatible_verify.test
index 4784e3d..c6742d3 100644
--- a/cmd/gorelease/testdata/basic/v0_incompatible_verify.test
+++ b/cmd/gorelease/testdata/basic/v0_incompatible_verify.test
@@ -2,6 +2,7 @@
 version=v0.1.2
 base=v0.1.1
 release=v0.1.2
+proxyVersions=example.com/basic@v0.1.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v0_patch_suggest.test b/cmd/gorelease/testdata/basic/v0_patch_suggest.test
index cee5349..a98e6b6 100644
--- a/cmd/gorelease/testdata/basic/v0_patch_suggest.test
+++ b/cmd/gorelease/testdata/basic/v0_patch_suggest.test
@@ -1,5 +1,6 @@
 mod=example.com/basic
 version=v0.1.1
 base=v0.1.0
+proxyVersions=example.com/basic@v0.1.0
 -- want --
 Suggested version: v0.1.1
diff --git a/cmd/gorelease/testdata/basic/v0_patch_verify.test b/cmd/gorelease/testdata/basic/v0_patch_verify.test
index a641f8c..b01e475 100644
--- a/cmd/gorelease/testdata/basic/v0_patch_verify.test
+++ b/cmd/gorelease/testdata/basic/v0_patch_verify.test
@@ -2,5 +2,6 @@
 version=v0.1.1
 base=v0.1.0
 release=v0.1.1
+proxyVersions=example.com/basic@v0.1.0
 -- want --
 v0.1.1 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v0_pre_suggest.test b/cmd/gorelease/testdata/basic/v0_pre_suggest.test
index 2c09e5f..169a325 100644
--- a/cmd/gorelease/testdata/basic/v0_pre_suggest.test
+++ b/cmd/gorelease/testdata/basic/v0_pre_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic
 version=v0.1.2
 base=v0.1.1-pre
+proxyVersions=example.com/basic@v0.1.1-pre
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_compatible_suggest.test b/cmd/gorelease/testdata/basic/v1_compatible_suggest.test
index fb27db1..42b4ddd 100644
--- a/cmd/gorelease/testdata/basic/v1_compatible_suggest.test
+++ b/cmd/gorelease/testdata/basic/v1_compatible_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic
 version=v1.1.0
 base=v1.0.1
+proxyVersions=example.com/basic@v1.0.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_compatible_verify.test b/cmd/gorelease/testdata/basic/v1_compatible_verify.test
index 8c98ee1..45822ee 100644
--- a/cmd/gorelease/testdata/basic/v1_compatible_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_compatible_verify.test
@@ -2,6 +2,7 @@
 version=v1.1.0
 base=v1.0.1
 release=v1.1.0
+proxyVersions=example.com/basic@v1.0.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_fork_base_modpath_version_verify.test b/cmd/gorelease/testdata/basic/v1_fork_base_modpath_version_verify.test
index eaa57be..47e17ca 100644
--- a/cmd/gorelease/testdata/basic/v1_fork_base_modpath_version_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_fork_base_modpath_version_verify.test
@@ -2,6 +2,7 @@
 base=example.com/basic@v1.1.1
 version=v1.1.2
 release=v1.1.2
+proxyVersions=example.com/basic@v1.1.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_fork_base_verify.test b/cmd/gorelease/testdata/basic/v1_fork_base_verify.test
index 8840112..569b734 100644
--- a/cmd/gorelease/testdata/basic/v1_fork_base_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_fork_base_verify.test
@@ -4,7 +4,7 @@
 mod=example.com/basic
 version=v1.1.2
 base=example.com/basicfork@v1.1.2
-release=v1.1.2
+release=v1.1.3
 -- want --
 example.com/basicfork/a
 -----------------------
diff --git a/cmd/gorelease/testdata/basic/v1_incompatible_verify.test b/cmd/gorelease/testdata/basic/v1_incompatible_verify.test
index e5d6c4d..149b849 100644
--- a/cmd/gorelease/testdata/basic/v1_incompatible_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_incompatible_verify.test
@@ -3,6 +3,7 @@
 base=v1.1.1
 success=false
 release=v1.1.2
+proxyVersions=example.com/basic@v1.1.1
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_patch_suggest.test b/cmd/gorelease/testdata/basic/v1_patch_suggest.test
index a0effa1..a72569d 100644
--- a/cmd/gorelease/testdata/basic/v1_patch_suggest.test
+++ b/cmd/gorelease/testdata/basic/v1_patch_suggest.test
@@ -1,5 +1,6 @@
 mod=example.com/basic
 version=v1.1.1
 base=v1.1.0
+proxyVersions=example.com/basic@v1.1.0
 -- want --
 Suggested version: v1.1.1
diff --git a/cmd/gorelease/testdata/basic/v1_patch_verify.test b/cmd/gorelease/testdata/basic/v1_patch_verify.test
index 64c734b..a01d5be 100644
--- a/cmd/gorelease/testdata/basic/v1_patch_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_patch_verify.test
@@ -2,5 +2,6 @@
 version=v1.1.1
 base=v1.1.0
 release=v1.1.1
+proxyVersions=example.com/basic@v1.1.0
 -- want --
 v1.1.1 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v1_pre_suggest.test b/cmd/gorelease/testdata/basic/v1_pre_suggest.test
index de0f2c0..efca8c4 100644
--- a/cmd/gorelease/testdata/basic/v1_pre_suggest.test
+++ b/cmd/gorelease/testdata/basic/v1_pre_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic
 version=v1.1.2
 base=v1.1.1-pre
+proxyVersions=example.com/basic@v1.1.1-pre
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_querybase_higher.test b/cmd/gorelease/testdata/basic/v1_querybase_higher.test
index 7bf77bc..ba6838f 100644
--- a/cmd/gorelease/testdata/basic/v1_querybase_higher.test
+++ b/cmd/gorelease/testdata/basic/v1_querybase_higher.test
@@ -3,6 +3,5 @@
 release=v1.0.1
 base=>v1.0.1
 error=true
-
 -- want --
 base version v1.1.0 (>v1.0.1) must be lower than release version v1.0.1
diff --git a/cmd/gorelease/testdata/basic/v1_querybase_suggest.test b/cmd/gorelease/testdata/basic/v1_querybase_suggest.test
index 6bfc95c..ec73b41 100644
--- a/cmd/gorelease/testdata/basic/v1_querybase_suggest.test
+++ b/cmd/gorelease/testdata/basic/v1_querybase_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic
 version=v1.0.1
 base=version-1.0.1
+proxyVersions=example.com/basic@version-1.0.1,example.com/basic@v1.0.1
 -- want --
 Base version: v1.0.1 (version-1.0.1)
 Suggested version: v1.0.2
diff --git a/cmd/gorelease/testdata/basic/v1_v2_base_modpath_query_verify.test b/cmd/gorelease/testdata/basic/v1_v2_base_modpath_query_verify.test
index 1d56841..d27a389 100644
--- a/cmd/gorelease/testdata/basic/v1_v2_base_modpath_query_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_v2_base_modpath_query_verify.test
@@ -2,6 +2,7 @@
 base=example.com/basic@>=v1.1.0
 version=v2.0.1
 release=v2.0.1
+proxyVersions=example.com/basic@v1.1.0
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_v2_base_modpath_verify.test b/cmd/gorelease/testdata/basic/v1_v2_base_modpath_verify.test
index 6533f3a..54fe0db 100644
--- a/cmd/gorelease/testdata/basic/v1_v2_base_modpath_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_v2_base_modpath_verify.test
@@ -2,6 +2,7 @@
 base=example.com/basic
 version=v2.1.0
 release=v2.1.0
+proxyVersions=example.com/basic@v1.1.2
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v1_v2_base_modpath_version_verify.test b/cmd/gorelease/testdata/basic/v1_v2_base_modpath_version_verify.test
index 22ed2f4..8edac73 100644
--- a/cmd/gorelease/testdata/basic/v1_v2_base_modpath_version_verify.test
+++ b/cmd/gorelease/testdata/basic/v1_v2_base_modpath_version_verify.test
@@ -2,6 +2,7 @@
 base=example.com/basic@v1.1.0
 version=v2.0.1
 release=v2.0.1
+proxyVersions=example.com/basic@v1.1.0
 -- want --
 example.com/basic/a
 -------------------
diff --git a/cmd/gorelease/testdata/basic/v2_compatible_suggest.test b/cmd/gorelease/testdata/basic/v2_compatible_suggest.test
index a421c56..78740e2 100644
--- a/cmd/gorelease/testdata/basic/v2_compatible_suggest.test
+++ b/cmd/gorelease/testdata/basic/v2_compatible_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic/v2
 version=v2.1.0
 base=v2.0.1
+proxyVersions=example.com/basic/v2@v2.0.1
 -- want --
 example.com/basic/v2/a
 ----------------------
diff --git a/cmd/gorelease/testdata/basic/v2_compatible_verify.test b/cmd/gorelease/testdata/basic/v2_compatible_verify.test
index ac138a7..63307ee 100644
--- a/cmd/gorelease/testdata/basic/v2_compatible_verify.test
+++ b/cmd/gorelease/testdata/basic/v2_compatible_verify.test
@@ -2,6 +2,7 @@
 version=v2.1.0
 base=v2.0.1
 release=v2.1.0
+proxyVersions=example.com/basic/v2@v2.0.1
 -- want --
 example.com/basic/v2/a
 ----------------------
diff --git a/cmd/gorelease/testdata/basic/v2_incompatible_verify.test b/cmd/gorelease/testdata/basic/v2_incompatible_verify.test
index 8ea2095..3948f41 100644
--- a/cmd/gorelease/testdata/basic/v2_incompatible_verify.test
+++ b/cmd/gorelease/testdata/basic/v2_incompatible_verify.test
@@ -3,6 +3,7 @@
 base=v2.1.1
 success=false
 release=v2.1.2
+proxyVersions=example.com/basic/v2@v2.1.1
 -- want --
 example.com/basic/v2/a
 ----------------------
diff --git a/cmd/gorelease/testdata/basic/v2_patch_suggest.test b/cmd/gorelease/testdata/basic/v2_patch_suggest.test
index 1416690..991885c 100644
--- a/cmd/gorelease/testdata/basic/v2_patch_suggest.test
+++ b/cmd/gorelease/testdata/basic/v2_patch_suggest.test
@@ -1,5 +1,6 @@
 mod=example.com/basic/v2
 version=v2.1.1
 base=v2.1.0
+proxyVersions=example.com/basic/v2@v2.1.0
 -- want --
 Suggested version: v2.1.1
diff --git a/cmd/gorelease/testdata/basic/v2_patch_verify.test b/cmd/gorelease/testdata/basic/v2_patch_verify.test
index 4aead44..f5c7f04 100644
--- a/cmd/gorelease/testdata/basic/v2_patch_verify.test
+++ b/cmd/gorelease/testdata/basic/v2_patch_verify.test
@@ -2,5 +2,6 @@
 version=v2.1.1
 base=v2.1.0
 release=v2.1.1
+proxyVersions=example.com/basic/v2@v2.1.0
 -- want --
 v2.1.1 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/basic/v2_pre_suggest.test b/cmd/gorelease/testdata/basic/v2_pre_suggest.test
index c786dcb..94716f4 100644
--- a/cmd/gorelease/testdata/basic/v2_pre_suggest.test
+++ b/cmd/gorelease/testdata/basic/v2_pre_suggest.test
@@ -1,6 +1,7 @@
 mod=example.com/basic/v2
 version=v2.1.2
 base=v2.1.1-pre
+proxyVersions=example.com/basic/v2@v2.1.1-pre
 -- want --
 example.com/basic/v2/a
 ----------------------
diff --git a/cmd/gorelease/testdata/empty/empty.test b/cmd/gorelease/testdata/empty/empty.test
index c87fd6b..cd95565 100644
--- a/cmd/gorelease/testdata/empty/empty.test
+++ b/cmd/gorelease/testdata/empty/empty.test
@@ -2,5 +2,6 @@
 base=v0.0.1
 version=v0.0.2
 release=v0.0.2
+proxyVersions=example.com/empty@v0.0.1
 -- want --
 v0.0.2 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/errors/bad_release.test b/cmd/gorelease/testdata/errors/bad_release.test
index 6015532..74dc009 100644
--- a/cmd/gorelease/testdata/errors/bad_release.test
+++ b/cmd/gorelease/testdata/errors/bad_release.test
@@ -2,7 +2,6 @@
 base=v0.1.0
 release=master
 error=true
-
 -- want --
 usage: gorelease [-base=version] [-version=version]
 release version "master" is not a canonical semantic version
diff --git a/cmd/gorelease/testdata/errors/base_higher.test b/cmd/gorelease/testdata/errors/base_higher.test
index 12d7bba..a2325f9 100644
--- a/cmd/gorelease/testdata/errors/base_higher.test
+++ b/cmd/gorelease/testdata/errors/base_higher.test
@@ -2,7 +2,6 @@
 base=v0.2.0
 release=v0.1.0
 error=true
-
 -- want --
 usage: gorelease [-base=version] [-version=version]
 base version ("v0.2.0") must be lower than release version ("v0.1.0")
diff --git a/cmd/gorelease/testdata/errors/base_modpath_none.test b/cmd/gorelease/testdata/errors/base_modpath_none.test
index bb438d9..74a6946 100644
--- a/cmd/gorelease/testdata/errors/base_modpath_none.test
+++ b/cmd/gorelease/testdata/errors/base_modpath_none.test
@@ -1,7 +1,6 @@
 mod=example.com/basic/v2
 base=example.com/basic@none
 error=true
-
 -- want --
 usage: gorelease [-base=version] [-version=version]
 base version ("example.com/basic@none") cannot have version "none" with explicit module path
diff --git a/cmd/gorelease/testdata/errors/errors.test b/cmd/gorelease/testdata/errors/errors.test
index 002a8fc..a7b396b 100644
--- a/cmd/gorelease/testdata/errors/errors.test
+++ b/cmd/gorelease/testdata/errors/errors.test
@@ -3,7 +3,7 @@
 base=v0.1.0
 release=v0.2.0
 success=false
-
+proxyVersions=example.com/errors@v0.1.0
 -- want --
 example.com/errors/added
 ------------------------
diff --git a/cmd/gorelease/testdata/errors/same_base_release.test b/cmd/gorelease/testdata/errors/same_base_release.test
index 0d0a09d..4da457b 100644
--- a/cmd/gorelease/testdata/errors/same_base_release.test
+++ b/cmd/gorelease/testdata/errors/same_base_release.test
@@ -2,7 +2,6 @@
 base=v0.1.0
 release=v0.1.0
 error=true
-
 -- want --
 usage: gorelease [-base=version] [-version=version]
 -base and -version must be different
diff --git a/cmd/gorelease/testdata/errors/upgrade_base.test b/cmd/gorelease/testdata/errors/upgrade_base.test
index d09b68a..994b3b7 100644
--- a/cmd/gorelease/testdata/errors/upgrade_base.test
+++ b/cmd/gorelease/testdata/errors/upgrade_base.test
@@ -2,6 +2,5 @@
 version=v0.1.0
 base=upgrade
 error=true
-
 -- want --
 could not resolve version example.com/errors@upgrade: query is based on requirements in main go.mod file
diff --git a/cmd/gorelease/testdata/first/v0_0_0.test b/cmd/gorelease/testdata/first/v0_0_0.test
index d3004df..8214501 100644
--- a/cmd/gorelease/testdata/first/v0_0_0.test
+++ b/cmd/gorelease/testdata/first/v0_0_0.test
@@ -1,7 +1,6 @@
 mod=example.com/first
 base=none
 release=v0.0.0
-
 -- want --
 v0.0.0 is a valid semantic version for this release.
 -- go.mod --
diff --git a/cmd/gorelease/testdata/first/v0_0_1.test b/cmd/gorelease/testdata/first/v0_0_1.test
index acbc4c9..79630a1 100644
--- a/cmd/gorelease/testdata/first/v0_0_1.test
+++ b/cmd/gorelease/testdata/first/v0_0_1.test
@@ -1,7 +1,6 @@
 mod=example.com/first
 base=none
 release=v0.0.1
-
 -- want --
 v0.0.1 is a valid semantic version for this release.
 -- go.mod --
diff --git a/cmd/gorelease/testdata/first/v0_1_0-alpha.1.test b/cmd/gorelease/testdata/first/v0_1_0-alpha.1.test
index 6c2df3f..8b17521 100644
--- a/cmd/gorelease/testdata/first/v0_1_0-alpha.1.test
+++ b/cmd/gorelease/testdata/first/v0_1_0-alpha.1.test
@@ -1,7 +1,6 @@
 mod=example.com/first
 base=none
 release=v0.1.0-alpha.1
-
 -- want --
 v0.1.0-alpha.1 is a valid semantic version for this release.
 -- go.mod --
diff --git a/cmd/gorelease/testdata/first/v0_1_0.test b/cmd/gorelease/testdata/first/v0_1_0.test
index 8c56a93..656ba5d 100644
--- a/cmd/gorelease/testdata/first/v0_1_0.test
+++ b/cmd/gorelease/testdata/first/v0_1_0.test
@@ -1,7 +1,6 @@
 mod=example.com/first
 base=none
 release=v0.1.0
-
 -- want --
 v0.1.0 is a valid semantic version for this release.
 -- go.mod --
diff --git a/cmd/gorelease/testdata/first/v0_err.test b/cmd/gorelease/testdata/first/v0_err.test
index 623a4ee..c655554 100644
--- a/cmd/gorelease/testdata/first/v0_err.test
+++ b/cmd/gorelease/testdata/first/v0_err.test
@@ -6,7 +6,6 @@
 # TODO(golang.org/issue/36087): go list doesn't report positions in correct
 # place for scanner errors.
 skip=packages.Load gives error with extra "-: " prefix
-
 -- want --
 example.com/first
 -----------------
diff --git a/cmd/gorelease/testdata/first/v1_0_0.test b/cmd/gorelease/testdata/first/v1_0_0.test
index 9bbd271..e1b4f5c 100644
--- a/cmd/gorelease/testdata/first/v1_0_0.test
+++ b/cmd/gorelease/testdata/first/v1_0_0.test
@@ -1,7 +1,6 @@
 mod=example.com/first
 base=none
 release=v1.0.0
-
 -- want --
 v1.0.0 is a valid semantic version for this release.
 -- go.mod --
diff --git a/cmd/gorelease/testdata/first/v2_err.test b/cmd/gorelease/testdata/first/v2_err.test
index 09938c8..dfc71d5 100644
--- a/cmd/gorelease/testdata/first/v2_err.test
+++ b/cmd/gorelease/testdata/first/v2_err.test
@@ -6,7 +6,6 @@
 # TODO(golang.org/issue/36087): go list doesn't report positions in correct
 # place for scanner errors.
 skip=packages.Load gives error with extra "-: " prefix
-
 -- want --
 example.com/first
 -----------------
diff --git a/cmd/gorelease/testdata/first/v2_moderr.test b/cmd/gorelease/testdata/first/v2_moderr.test
index 2b6879c..63bb806 100644
--- a/cmd/gorelease/testdata/first/v2_moderr.test
+++ b/cmd/gorelease/testdata/first/v2_moderr.test
@@ -2,7 +2,6 @@
 base=none
 release=v2.0.0
 success=false
-
 -- want --
 v2.0.0 is not a valid semantic version for this release.
 The module path does not end with the major version suffix /v2,
diff --git a/cmd/gorelease/testdata/internalcompat/internalcompat.test b/cmd/gorelease/testdata/internalcompat/internalcompat.test
index 8bc32fa..9bf59a3 100644
--- a/cmd/gorelease/testdata/internalcompat/internalcompat.test
+++ b/cmd/gorelease/testdata/internalcompat/internalcompat.test
@@ -1,6 +1,5 @@
 mod=example.com/internalcompat/b
 version=v1.0.0
-release=v1.0.0
+release=v1.0.1
 base=example.com/internalcompat/a@v1.0.0
-
 -- want --
diff --git a/cmd/gorelease/testdata/mod/example.com_cycle_v1.0.0.txt b/cmd/gorelease/testdata/mod/example.com_cycle_v1.0.0.txt
index 7200d20..55e152d 100644
--- a/cmd/gorelease/testdata/mod/example.com_cycle_v1.0.0.txt
+++ b/cmd/gorelease/testdata/mod/example.com_cycle_v1.0.0.txt
@@ -8,7 +8,6 @@
 go 1.12
 
 require example.com/cycledep v1.0.0
-require example.com/cycle v1.5.0
 -- cycle/main.go --
 package main
 
diff --git a/cmd/gorelease/testdata/nomod/nomod.test b/cmd/gorelease/testdata/nomod/nomod.test
index 9f332fe..973d393 100644
--- a/cmd/gorelease/testdata/nomod/nomod.test
+++ b/cmd/gorelease/testdata/nomod/nomod.test
@@ -2,5 +2,6 @@
 version=v0.0.2
 base=v0.0.1
 release=v0.0.2
+proxyVersions=example.com/nomod@v0.0.1
 -- want --
 v0.0.2 is a valid semantic version for this release.
diff --git a/cmd/gorelease/testdata/private/unreported.test b/cmd/gorelease/testdata/private/unreported.test
index 3aabad3..8ed58fe 100644
--- a/cmd/gorelease/testdata/private/unreported.test
+++ b/cmd/gorelease/testdata/private/unreported.test
@@ -1,5 +1,6 @@
 mod=example.com/private
 version=v1.0.1
 base=v1.0.0
+proxyVersions=example.com/private@v1.0.0
 -- want --
 Suggested version: v1.0.1
diff --git a/cmd/gorelease/testdata/regress/issue37756.test b/cmd/gorelease/testdata/regress/issue37756.test
index 0a9093c..f95c55d 100644
--- a/cmd/gorelease/testdata/regress/issue37756.test
+++ b/cmd/gorelease/testdata/regress/issue37756.test
@@ -3,6 +3,7 @@
 mod=example.com/issue37756
 version=v1.1.0
 base=v1.0.0
+proxyVersions=example.com/issue37756@v1.0.0
 -- want --
 example.com/issue37756/a
 ------------------------
diff --git a/cmd/gorelease/testdata/require/add_requirement.test b/cmd/gorelease/testdata/require/add_requirement.test
index 3cbc78b..7833dd3 100644
--- a/cmd/gorelease/testdata/require/add_requirement.test
+++ b/cmd/gorelease/testdata/require/add_requirement.test
@@ -1,5 +1,6 @@
 mod=example.com/require
 base=v0.0.1
+proxyVersions=example.com/require@v0.0.1,example.com/basic@v1.0.1
 -- want --
 Suggested version: v0.1.0
 -- go.mod --
diff --git a/cmd/gorelease/testdata/require/decrement_go_version.test b/cmd/gorelease/testdata/require/decrement_go_version.test
index abcd56f..7360f13 100644
--- a/cmd/gorelease/testdata/require/decrement_go_version.test
+++ b/cmd/gorelease/testdata/require/decrement_go_version.test
@@ -1,5 +1,6 @@
 mod=example.com/require
 base=v0.0.1
+proxyVersions=example.com/require@v0.0.1
 -- want --
 Suggested version: v0.0.2
 -- go.mod --
diff --git a/cmd/gorelease/testdata/require/increment_go_version.test b/cmd/gorelease/testdata/require/increment_go_version.test
index 2f7c993..a698bee 100644
--- a/cmd/gorelease/testdata/require/increment_go_version.test
+++ b/cmd/gorelease/testdata/require/increment_go_version.test
@@ -1,5 +1,6 @@
 mod=example.com/require
 base=v0.0.1
+proxyVersions=example.com/require@v0.0.1
 -- want --
 Suggested version: v0.1.0
 -- go.mod --
diff --git a/cmd/gorelease/testdata/require/increment_requirement_minor.test b/cmd/gorelease/testdata/require/increment_requirement_minor.test
index a453b3e..0d83508 100644
--- a/cmd/gorelease/testdata/require/increment_requirement_minor.test
+++ b/cmd/gorelease/testdata/require/increment_requirement_minor.test
@@ -1,5 +1,6 @@
 mod=example.com/require
 base=v0.1.0
+proxyVersions=example.com/require@v0.1.0,example.com/basic@v1.1.0,example.com/basic@v1.0.1
 -- want --
 Suggested version: v0.2.0
 -- go.mod --
diff --git a/cmd/gorelease/testdata/require/increment_requirement_patch.test b/cmd/gorelease/testdata/require/increment_requirement_patch.test
index 0469ae4..2665fd3 100644
--- a/cmd/gorelease/testdata/require/increment_requirement_patch.test
+++ b/cmd/gorelease/testdata/require/increment_requirement_patch.test
@@ -1,5 +1,6 @@
 mod=example.com/require
 base=v0.1.1
+proxyVersions=example.com/require@v0.1.1,example.com/basic@v1.1.1,example.com/basic@v1.1.0
 -- want --
 Suggested version: v0.1.2
 -- go.mod --
diff --git a/cmd/gorelease/testdata/require/remove_requirements.test b/cmd/gorelease/testdata/require/remove_requirements.test
index f57d12a..b3f82ed 100644
--- a/cmd/gorelease/testdata/require/remove_requirements.test
+++ b/cmd/gorelease/testdata/require/remove_requirements.test
@@ -1,5 +1,6 @@
 mod=example.com/require
 base=v0.0.1
+proxyVersions=example.com/require@v0.0.1
 -- want --
 Suggested version: v0.0.2
 -- go.mod --
diff --git a/go.mod b/go.mod
index 00c5b45..4e6430c 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@
 	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4
 	golang.org/x/image v0.0.0-20190802002840-cff245a6509b
 	golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f
-	golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449
+	golang.org/x/mod v0.4.2
 	golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24
 	golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa
 	golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898
diff --git a/go.sum b/go.sum
index 545190e..971c6c5 100644
--- a/go.sum
+++ b/go.sum
@@ -17,8 +17,8 @@
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ=
-golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=