Repository: goreleaser/goreleaser
Pull request #2417 (2021-08-20 08:43:07)

fix: Change the init- and default-template for `snapshot.name_template`
# If applied, this commit will...

This commit will change the default- and the init-template that is used for `snapshot.name_template`. If applied, this commit will prevent situations as described in #2415 for new projects and projects that don't overwrite `snapshot.name_template`.

# Why is this change being made?

The change is necessary to remove an odity:
In regular mode, `.Version` removes the `v` from the most recent git tag (e.g. `v1.2.3` becomes `1.2.3`). In snapshot mode however, what is defined as `snapshot.name_template` becomes the value of `.Version`. And before this commit `snapshot.name_template` is defined as a value that does not remove the `v` prefix.

This did lead to the following odd situation, given the most recent git tag is `v1.2.3`:

```
Regular Mode:  .Version =                            '1.2.3'
Snapshot Mode: .Version = '{{incpatch .Tag}}-next' = 'v1.2.4-next'
```

Notice the difference: In regular mode, `.Version` has no `v` prefix, whereas in snapshot mode, is has one.
Which lead to odd situations wherever `{{.Version}}` was used in other parts of the goreleaser configuration and for example to Docker tags like `org/image:vv1.2.3-next` in snapshot mode and `org/image:v1.2.3` in regular mode.

# Provide links to any relevant tickets, URLs or other resources

- #2415


vercel[bot]: @cimnine 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%22ae1e51bc970175ec43b8e99cc025ed9cd6da81ff%22%7D%2C%22id%22%3A%22QmNVWT7HWaELPYgEcJrNYd64eLwsbW1SEz3sKqMb3DKMNF%22%2C%22org%22%3A%22goreleaser%22%2C%22prId%22%3A2417%2C%22repo%22%3A%22goreleaser%22%7D).



Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 20de36f0..51783798 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -5,7 +5,7 @@ before:
     - go mod tidy
     - ./scripts/completions.sh
 snapshot:
-  name_template: '{{ incpatch .Tag }}-next'
+  name_template: '{{ incpatch .Version }}-next'
 gomod:
   proxy: true
 builds:
diff --git a/internal/builders/golang/build.go b/internal/builders/golang/build.go
index 604b6ebb..49826fba 100644
--- a/internal/builders/golang/build.go
+++ b/internal/builders/golang/build.go
@@ -59,7 +59,11 @@ func (*Builder) WithDefaults(build config.Build) (config.Build, error) {
 		if len(build.Gomips) == 0 {
 			build.Gomips = []string{"hardfloat"}
 		}
-		targets, err := matrix(build, goVersion(build))
+		version, err := goVersion(build)
+		if err != nil {
+			return build, err
+		}
+		targets, err := matrix(build, version)
 		build.Targets = targets
 		if err != nil {
 			return build, err
diff --git a/internal/builders/golang/targets.go b/internal/builders/golang/targets.go
index dd658984..2f93152e 100644
--- a/internal/builders/golang/targets.go
+++ b/internal/builders/golang/targets.go
@@ -131,9 +131,14 @@ var (
 	go117re = regexp.MustCompile(`go version go1.1[7-9]`)
 )
 
-func goVersion(build config.Build) []byte {
-	bts, _ := exec.Command(build.GoBinary, "version").CombinedOutput()
-	return bts
+func goVersion(build config.Build) ([]byte, error) {
+	cmd := exec.Command(build.GoBinary, "version")
+	cmd.Dir = build.Dir // Set Dir to build directory in case of reletive path to GoBinary
+	bts, err := cmd.CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("unable to determine version of go binary (%s): %w", build.GoBinary, err)
+	}
+	return bts, nil
 }
 
 func valid(target target) bool {
diff --git a/internal/git/config.go b/internal/git/config.go
index b3010982..ecf90577 100644
--- a/internal/git/config.go
+++ b/internal/git/config.go
@@ -3,6 +3,7 @@ package git
 import (
 	"errors"
 	"fmt"
+	"net/url"
 	"strings"
 
 	"github.com/goreleaser/goreleaser/pkg/config"
@@ -17,25 +18,35 @@ func ExtractRepoFromConfig() (result config.Repo, err error) {
 	if err != nil {
 		return result, fmt.Errorf("repository doesn't have an `origin` remote")
 	}
-	return ExtractRepoFromURL(out), nil
+	return ExtractRepoFromURL(out)
 }
 
-func ExtractRepoFromURL(s string) config.Repo {
+func ExtractRepoFromURL(rawurl string) (config.Repo, error) {
 	// removes the .git suffix and any new lines
-	s = strings.NewReplacer(
-		".git", "",
-		"\n", "",
-	).Replace(s)
+	s := strings.TrimSuffix(strings.TrimSpace(rawurl), ".git")
+
 	// if the URL contains a :, indicating a SSH config,
 	// remove all chars until it, including itself
 	// on HTTP and HTTPS URLs it will remove the http(s): prefix,
 	// which is ok. On SSH URLs the whole user@server will be removed,
 	// which is required.
 	s = s[strings.LastIndex(s, ":")+1:]
-	// split by /, the last to parts should be the owner and name
-	ss := strings.Split(s, "/")
+
+	// now we can parse it with net/url
+	u, err := url.Parse(s)
+	if err != nil {
+		return config.Repo{}, err
+	}
+
+	// split the parsed url path by /, the last two parts should be the owner and name
+	ss := strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
+
+	// if less than 2 parts, its likely not a valid repository
+	if len(ss) < 2 {
+		return config.Repo{}, fmt.Errorf("unsupported repository URL: %s", rawurl)
+	}
 	return config.Repo{
 		Owner: ss[len(ss)-2],
 		Name:  ss[len(ss)-1],
-	}
+	}, nil
 }
diff --git a/internal/pipe/snapshot/snapshot.go b/internal/pipe/snapshot/snapshot.go
index 8b9daa33..ee525ad8 100644
--- a/internal/pipe/snapshot/snapshot.go
+++ b/internal/pipe/snapshot/snapshot.go
@@ -20,7 +20,7 @@ func (Pipe) String() string {
 // Default sets the pipe defaults.
 func (Pipe) Default(ctx *context.Context) error {
 	if ctx.Config.Snapshot.NameTemplate == "" {
-		ctx.Config.Snapshot.NameTemplate = "{{ .Tag }}-SNAPSHOT-{{ .ShortCommit }}"
+		ctx.Config.Snapshot.NameTemplate = "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}"
 	}
 	return nil
 }
diff --git a/internal/static/config.go b/internal/static/config.go
index 5098f5c8..707192d7 100644
--- a/internal/static/config.go
+++ b/internal/static/config.go
@@ -29,7 +29,7 @@ archives:
 checksum:
   name_template: 'checksums.txt'
 snapshot:
-  name_template: "{{ incpatch .Tag }}-next"
+  name_template: "{{ incpatch .Version }}-next"
 changelog:
   sort: asc
   filters:
diff --git a/www/docs/customization/snapshots.md b/www/docs/customization/snapshots.md
index f15a1d3e..10e312d5 100644
--- a/www/docs/customization/snapshots.md
+++ b/www/docs/customization/snapshots.md
@@ -16,8 +16,8 @@ snapshot:
   # Note that some pipes require this to be semantic version compliant (nfpm,
   # for example).
   #
-  # Default is `{{ .Tag }}-SNAPSHOT-{{.ShortCommit}}`.
-  name_template: '{{ incpatch .Tag }}-devel'
+  # Default is `{{ .Version }}-SNAPSHOT-{{.ShortCommit}}`.
+  name_template: '{{ incpatch .Version }}-devel'
 ```
 
 ## How it works
diff --git a/www/docs/customization/templates.md b/www/docs/customization/templates.md
index a84cd038..de1da9aa 100644
--- a/www/docs/customization/templates.md
+++ b/www/docs/customization/templates.md
@@ -13,7 +13,7 @@ On fields that support templating, these fields are always available:
 | Key                 | Description                                                                                                                  |
 |---------------------|------------------------------------------------------------------------------------------------------------------------------|
 | `.ProjectName`      | the project name                                                                                                             |
-| `.Version`          | the version being released (`v` prefix stripped),<br>or `{{ .Tag }}-SNAPSHOT-{{ .ShortCommit }}` in case of snapshot release |
+| `.Version`          | the version being released (`v` prefix stripped),<br>or what is configured in `snapshot.name_template` in case of a snapshot release (defaults to `{{ .Version }}-SNAPSHOT-{{.ShortCommit}}`) |
 | `.Branch`           | the current git branch                                                                                                       |
 | `.PrefixedTag`      | the current git tag prefixed with the monorepo config tag prefix (if any)                                                    |
 | `.Tag`              | the current git tag                                                                                                          |


Test patch:
diff --git a/cmd/testdata/good.yml b/cmd/testdata/good.yml
index 33fe3353..a4efc176 100644
--- a/cmd/testdata/good.yml
+++ b/cmd/testdata/good.yml
@@ -19,7 +19,7 @@ archives:
 checksum:
   name_template: 'checksums.txt'
 snapshot:
-  name_template: "{{ incpatch .Tag }}-next"
+  name_template: "{{ incpatch .Version }}-next"
 changelog:
   sort: asc
   filters:
diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go
index d394acd0..0ece15a5 100644
--- a/internal/builders/golang/build_test.go
+++ b/internal/builders/golang/build_test.go
@@ -90,6 +90,9 @@ func TestWithDefaults(t *testing.T) {
 		},
 	} {
 		t.Run(name, func(t *testing.T) {
+			if testcase.build.GoBinary != "" && testcase.build.GoBinary != "go" {
+				helperCreateGoVersionExe(t, testcase.build.GoBinary, "go1.17")
+			}
 			config := config.Project{
 				Builds: []config.Build{
 					testcase.build,
@@ -105,6 +108,29 @@ func TestWithDefaults(t *testing.T) {
 	}
 }
 
+// helperCreateGoVersionExe creates a temporary executable with the given 'name', which will output
+// a 'go version' string with the given 'version'. The temporary directory created by this function
+// will be placed in the PATH variable for the duration of (and cleaned up at the end of) the
+// current test run.
+func helperCreateGoVersionExe(t *testing.T, name, version string) {
+	t.Helper()
+	d := t.TempDir()
+	f, err := os.Create(filepath.Join(d, name))
+	if err != nil {
+		t.Fatalf("unable to create temporary GoBinary file for testing: %v", err)
+	}
+	fmt.Fprintf(f, "#!/bin/sh\necho \"go version %s %s/%s\"\n", version, runtime.GOOS, runtime.GOARCH)
+	if err := f.Chmod(0o0755); err != nil {
+		t.Fatalf("unable to create temporary GoBinary file for testing: %v", err)
+	}
+	if err := f.Close(); err != nil {
+		t.Fatalf("unable to create temporary GoBinary file for testing: %v", err)
+	}
+	currentPath := os.Getenv("PATH")
+	t.Cleanup(func() { os.Setenv("PATH", currentPath) })
+	os.Setenv("PATH", fmt.Sprintf("%s%c%s", d, os.PathListSeparator, currentPath))
+}
+
 func TestInvalidTargets(t *testing.T) {
 	type testcase struct {
 		build       config.Build
diff --git a/internal/git/config_test.go b/internal/git/config_test.go
index 2cea5bc3..f878ce59 100644
--- a/internal/git/config_test.go
+++ b/internal/git/config_test.go
@@ -18,6 +18,7 @@ func TestRepoName(t *testing.T) {
 }
 
 func TestExtractRepoFromURL(t *testing.T) {
+	// valid urls
 	for _, url := range []string{
 		"git@github.com:goreleaser/goreleaser.git",
 		"git@custom:goreleaser/goreleaser.git",
@@ -27,8 +28,21 @@ func TestExtractRepoFromURL(t *testing.T) {
 		"https://github.enterprise.com/crazy/url/goreleaser/goreleaser.git",
 	} {
 		t.Run(url, func(t *testing.T) {
-			repo := git.ExtractRepoFromURL(url)
+			repo, err := git.ExtractRepoFromURL(url)
+			require.NoError(t, err)
 			require.Equal(t, "goreleaser/goreleaser", repo.String())
 		})
 	}
+
+	// invalid urls
+	for _, url := range []string{
+		"git@gist.github.com:someid.git",
+		"https://gist.github.com/someid.git",
+	} {
+		t.Run(url, func(t *testing.T) {
+			repo, err := git.ExtractRepoFromURL(url)
+			require.EqualError(t, err, "unsupported repository URL: "+url)
+			require.Equal(t, "", repo.String())
+		})
+	}
 }
diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go
index 8514dc1b..a0c3a060 100644
--- a/internal/pipe/build/build_test.go
+++ b/internal/pipe/build/build_test.go
@@ -330,6 +330,15 @@ func TestDefaultPartialBuilds(t *testing.T) {
 			},
 		},
 	}
+	// Create any 'Dir' paths necessary for builds.
+	cwd, _ := os.Getwd()
+	t.Cleanup(func() { os.Chdir(cwd) })
+	os.Chdir(t.TempDir())
+	for _, b := range ctx.Config.Builds {
+		if b.Dir != "" {
+			os.Mkdir(b.Dir, 0o755)
+		}
+	}
 	require.NoError(t, Pipe{}.Default(ctx))
 	t.Run("build0", func(t *testing.T) {
 		build := ctx.Config.Builds[0]
diff --git a/internal/pipe/snapshot/snapshot_test.go b/internal/pipe/snapshot/snapshot_test.go
index c5b32913..5ba6fb5c 100644
--- a/internal/pipe/snapshot/snapshot_test.go
+++ b/internal/pipe/snapshot/snapshot_test.go
@@ -20,7 +20,7 @@ func TestDefault(t *testing.T) {
 		},
 	}
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.Equal(t, "{{ .Tag }}-SNAPSHOT-{{ .ShortCommit }}", ctx.Config.Snapshot.NameTemplate)
+	require.Equal(t, "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}", ctx.Config.Snapshot.NameTemplate)
 }
 
 func TestDefaultSet(t *testing.T) {