diff --git a/internal/client/gitea.go b/internal/client/gitea.go index c2be325b2..2bba9e212 100644 --- a/internal/client/gitea.go +++ b/internal/client/gitea.go @@ -304,7 +304,7 @@ func (c *giteaClient) ReleaseURLTemplate(ctx *context.Context) (string, error) { } return fmt.Sprintf( - "%s/%s/%s/releases/download/{{ .Tag }}/{{ .ArtifactName }}", + "%s/%s/%s/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}", downloadURL, ctx.Config.Release.Gitea.Owner, ctx.Config.Release.Gitea.Name, diff --git a/internal/client/gitea_test.go b/internal/client/gitea_test.go index 5e860ee0e..be9fa6c62 100644 --- a/internal/client/gitea_test.go +++ b/internal/client/gitea_test.go @@ -486,12 +486,12 @@ func TestGiteaReleaseURLTemplate(t *testing.T) { { name: "string_url", downloadURL: "https://gitea.com", - wantDownloadURL: "https://gitea.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}", + wantDownloadURL: "https://gitea.com/owner/name/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}", }, { name: "download_url_template", downloadURL: "{{ .Env.GORELEASER_TEST_GITEA_URLS_DOWNLOAD }}", - wantDownloadURL: "https://gitea.mycompany.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}", + wantDownloadURL: "https://gitea.mycompany.com/owner/name/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}", }, { name: "download_url_template_invalid_value", diff --git a/internal/client/github.go b/internal/client/github.go index 31ae029f7..204108e61 100644 --- a/internal/client/github.go +++ b/internal/client/github.go @@ -497,7 +497,7 @@ func (c *githubClient) ReleaseURLTemplate(ctx *context.Context) (string, error) } return fmt.Sprintf( - "%s/%s/%s/releases/download/{{ .Tag }}/{{ .ArtifactName }}", + "%s/%s/%s/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}", downloadURL, ctx.Config.Release.GitHub.Owner, ctx.Config.Release.GitHub.Name, diff --git a/internal/client/github_test.go b/internal/client/github_test.go index 1e4feb7ea..8a62ca5f4 100644 --- a/internal/client/github_test.go +++ b/internal/client/github_test.go @@ -138,12 +138,12 @@ func TestGitHubReleaseURLTemplate(t *testing.T) { { name: "default_download_url", downloadURL: DefaultGitHubDownloadURL, - wantDownloadURL: "https://github.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}", + wantDownloadURL: "https://github.com/owner/name/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}", }, { name: "download_url_template", downloadURL: "{{ .Env.GORELEASER_TEST_GITHUB_URLS_DOWNLOAD }}", - wantDownloadURL: "https://github.mycompany.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}", + wantDownloadURL: "https://github.mycompany.com/owner/name/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}", }, { name: "download_url_template_invalid_value", diff --git a/internal/client/gitlab.go b/internal/client/gitlab.go index 7b442f203..4a03b0fc2 100644 --- a/internal/client/gitlab.go +++ b/internal/client/gitlab.go @@ -447,14 +447,14 @@ func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error) if ctx.Config.Release.GitLab.Owner != "" { urlTemplate = fmt.Sprintf( - "%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", + "%s/%s/%s/-/releases/{{ urlPathEscape .Tag }}/downloads/{{ .ArtifactName }}", downloadURL, ctx.Config.Release.GitLab.Owner, gitlabName, ) } else { urlTemplate = fmt.Sprintf( - "%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", + "%s/%s/-/releases/{{ urlPathEscape .Tag }}/downloads/{{ .ArtifactName }}", downloadURL, gitlabName, ) diff --git a/internal/client/gitlab_test.go b/internal/client/gitlab_test.go index 2d580d9b7..5195f47f2 100644 --- a/internal/client/gitlab_test.go +++ b/internal/client/gitlab_test.go @@ -37,19 +37,19 @@ func TestGitLabReleaseURLTemplate(t *testing.T) { name: "default_download_url", downloadURL: DefaultGitLabDownloadURL, repo: repo, - wantDownloadURL: "https://gitlab.com/owner/name/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", + wantDownloadURL: "https://gitlab.com/owner/name/-/releases/{{ urlPathEscape .Tag }}/downloads/{{ .ArtifactName }}", }, { name: "default_download_url_no_owner", downloadURL: DefaultGitLabDownloadURL, repo: config.Repo{Name: "name"}, - wantDownloadURL: "https://gitlab.com/name/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", + wantDownloadURL: "https://gitlab.com/name/-/releases/{{ urlPathEscape .Tag }}/downloads/{{ .ArtifactName }}", }, { name: "download_url_template", repo: repo, downloadURL: "{{ .Env.GORELEASER_TEST_GITLAB_URLS_DOWNLOAD }}", - wantDownloadURL: "https://gitlab.mycompany.com/owner/name/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}", + wantDownloadURL: "https://gitlab.mycompany.com/owner/name/-/releases/{{ urlPathEscape .Tag }}/downloads/{{ .ArtifactName }}", }, { name: "download_url_template_invalid_value", diff --git a/internal/client/mock.go b/internal/client/mock.go index 4af607157..cf62dc0cd 100644 --- a/internal/client/mock.go +++ b/internal/client/mock.go @@ -95,7 +95,7 @@ func (c *Mock) PublishRelease(_ *context.Context, _ string /* releaseID */) (err } func (c *Mock) ReleaseURLTemplate(_ *context.Context) (string, error) { - return "https://dummyhost/download/{{ .Tag }}/{{ .ArtifactName }}", nil + return "https://dummyhost/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}", nil } func (c *Mock) CreateFile(_ *context.Context, _ config.CommitAuthor, _ Repo, content []byte, path, msg string) error { diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go index 7ddb60ecd..d1934393c 100644 --- a/internal/tmpl/tmpl.go +++ b/internal/tmpl/tmpl.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "maps" + "net/url" "path/filepath" "regexp" "strings" @@ -251,6 +252,7 @@ func (t *Template) Apply(s string) (string, error) { "isEnvSet": t.isEnvSet, "map": makemap, "indexOrDefault": indexOrDefault, + "urlPathEscape": url.PathEscape, }). Parse(s) if err != nil { diff --git a/internal/tmpl/tmpl_test.go b/internal/tmpl/tmpl_test.go index 768a5af63..6d59f045e 100644 --- a/internal/tmpl/tmpl_test.go +++ b/internal/tmpl/tmpl_test.go @@ -39,8 +39,9 @@ func TestWithArtifact(t *testing.T) { Dirty: true, }), testctx.WithEnv(map[string]string{ - "FOO": "bar", - "MULTILINE": "something with\nmultiple lines\nremove this\nto test things", + "FOO": "bar", + "MULTILINE": "something with\nmultiple lines\nremove this\nto test things", + "WITH_SLASHES": "foo/bar", }), testctx.WithSemver(1, 2, 3, ""), testctx.Snapshot, @@ -103,6 +104,7 @@ func TestWithArtifact(t *testing.T) { "env bar: barrrrr": `env bar: {{ envOrDefault "BAR" "barrrrr" }}`, "env foo: bar": `env foo: {{ envOrDefault "FOO" "barrrrr" }}`, "env foo is set: true": `env foo is set: {{ isEnvSet "FOO" }}`, + "/foo%2Fbar": `/{{ urlPathEscape .Env.WITH_SLASHES}}`, "remove this": "{{ filter .Env.MULTILINE \".*remove.*\" }}", "something with\nmultiple lines\nto test things": "{{ reverseFilter .Env.MULTILINE \".*remove.*\" }}", diff --git a/www/docs/customization/templates.md b/www/docs/customization/templates.md index 966f79a4d..c90438ec6 100644 --- a/www/docs/customization/templates.md +++ b/www/docs/customization/templates.md @@ -178,6 +178,7 @@ On all fields, you have these available functions: | `incmajor "v1.2.4"` | increments the major of the given version[^panic-if-not-semver] | | `list "a" "b" "c"` | makes a list of strings[^pro] | | `in (list "a" "b" "c") "b"` | checks if a slice contains a value[^pro] | +| `urlPathEscape "foo/bar"` | escapes URL paths. See [PathEscape](https://pkg.go.dev/net/url#PathEscape) (since v2.5) | [^panic-if-not-semver]: Will panic if not a semantic version.