Repository: goreleaser/goreleaser
Pull request #2465 (2021-09-07 14:03:20)

feat: support templates for scm urls
Thank you for this really well polished project and the hard work put into it 🙌 

Background
---
When a git repository is hosted in multiple GitLab instances the
`.goreleaser.yml` needs to take in consideration both APIs endpoints. At
the moment it defaults to GitLab.com and you can override it with
`gitlab_urls` however this forces you to only support 1 GitLab instance.

We need this for
https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/14122
where we have a tool that is developed on GitLab.com but then mirrored
to an internal GitLab instance since we need it to operate GitLab.com
even when it's down.

Solution
---
Support templates like `{{ .Env.CI_SERVER_URL }}` for the
`gitlab_urls`, `github_urls`  and `gitea_urls` so it can use environment
variables and the same `.goreleaser` file can be used in multiple SCM
instances.

vercel[bot]: @steveazz 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%22e8d07a9e22cf2de3adda81f06dec7b653b2e8bcf%22%7D%2C%22id%22%3A%22QmdjZaeJWEqvZJKjo8tCTQWq5CBreuvwxe1veAxMSw3kkf%22%2C%22org%22%3A%22goreleaser%22%2C%22prId%22%3A2465%2C%22repo%22%3A%22goreleaser%22%7D).


caarlos0: Hey! 

Thanks for the kind word and for the PR. 

Can you do the same for github and gitea? (just so we have feature parity)

Would also be neat to add a note the docs saying its templateable, but I can do that later if you like

Thanks!
codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2465?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2465](https://codecov.io/gh/goreleaser/goreleaser/pull/2465?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (e8d07a9) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/7d22a32de9bf2e9a321fce913fb8be27234774c9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (7d22a32) will **increase** coverage by `0.74%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2465/graphs/tree.svg?width=650&height=150&src=pr&token=3bJvv6b7hN&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)](https://codecov.io/gh/goreleaser/goreleaser/pull/2465?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##           master    #2465      +/-   ##
==========================================
+ Coverage   83.01%   83.76%   +0.74%     
==========================================
  Files          87       87              
  Lines        6271     6277       +6     
==========================================
+ Hits         5206     5258      +52     
+ Misses        915      866      -49     
- Partials      150      153       +3     
```


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

------

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

steveazz: @caarlos0 this should be ready for another round of review! Let me know what you think and I'm sorry it turned out to be a large PR 🙇 

Patch:
diff --git a/internal/client/gitea.go b/internal/client/gitea.go
index 601c5f8e..fabfdc31 100644
--- a/internal/client/gitea.go
+++ b/internal/client/gitea.go
@@ -21,7 +21,12 @@ type giteaClient struct {
 	client *gitea.Client
 }
 
-func getInstanceURL(apiURL string) (string, error) {
+func getInstanceURL(ctx *context.Context) (string, error) {
+	apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GiteaURLs.API)
+	if err != nil {
+		return "", fmt.Errorf("templating Gitea API URL: %w", err)
+	}
+
 	u, err := url.Parse(apiURL)
 	if err != nil {
 		return "", err
@@ -36,7 +41,7 @@ func getInstanceURL(apiURL string) (string, error) {
 
 // NewGitea returns a gitea client implementation.
 func NewGitea(ctx *context.Context, token string) (Client, error) {
-	instanceURL, err := getInstanceURL(ctx.Config.GiteaURLs.API)
+	instanceURL, err := getInstanceURL(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -231,9 +236,14 @@ func (c *giteaClient) CreateRelease(ctx *context.Context, body string) (string,
 }
 
 func (c *giteaClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
+	downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GiteaURLs.Download)
+	if err != nil {
+		return "", fmt.Errorf("templating Gitea download URL: %w", err)
+	}
+
 	return fmt.Sprintf(
 		"%s/%s/%s/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
-		ctx.Config.GiteaURLs.Download,
+		downloadURL,
 		ctx.Config.Release.Gitea.Owner,
 		ctx.Config.Release.Gitea.Name,
 	), nil
diff --git a/internal/client/github.go b/internal/client/github.go
index 40dd864c..d6da97c2 100644
--- a/internal/client/github.go
+++ b/internal/client/github.go
@@ -40,18 +40,11 @@ func NewGitHub(ctx *context.Context, token string) (Client, error) {
 	}
 	base.(*http.Transport).Proxy = http.ProxyFromEnvironment
 	httpClient.Transport.(*oauth2.Transport).Base = base
+
 	client := github.NewClient(httpClient)
-	if ctx.Config.GitHubURLs.API != "" {
-		api, err := url.Parse(ctx.Config.GitHubURLs.API)
-		if err != nil {
-			return &githubClient{}, err
-		}
-		upload, err := url.Parse(ctx.Config.GitHubURLs.Upload)
-		if err != nil {
-			return &githubClient{}, err
-		}
-		client.BaseURL = api
-		client.UploadURL = upload
+	err := overrideGitHubClientAPI(ctx, client)
+	if err != nil {
+		return &githubClient{}, err
 	}
 
 	return &githubClient{client: client}, nil
@@ -181,9 +174,14 @@ func (c *githubClient) CreateRelease(ctx *context.Context, body string) (string,
 }
 
 func (c *githubClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
+	downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.Download)
+	if err != nil {
+		return "", fmt.Errorf("templating GitHub download URL: %w", err)
+	}
+
 	return fmt.Sprintf(
 		"%s/%s/%s/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
-		ctx.Config.GitHubURLs.Download,
+		downloadURL,
 		ctx.Config.Release.GitHub.Owner,
 		ctx.Config.Release.GitHub.Name,
 	), nil
@@ -251,3 +249,32 @@ func (c *githubClient) getMilestoneByTitle(ctx *context.Context, repo Repo, titl
 
 	return nil, nil
 }
+
+func overrideGitHubClientAPI(ctx *context.Context, client *github.Client) error {
+	if ctx.Config.GitHubURLs.API == "" {
+		return nil
+	}
+
+	apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.API)
+	if err != nil {
+		return fmt.Errorf("templating GitHub API URL: %w", err)
+	}
+	api, err := url.Parse(apiURL)
+	if err != nil {
+		return err
+	}
+
+	uploadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.Upload)
+	if err != nil {
+		return fmt.Errorf("templating GitHub upload URL: %w", err)
+	}
+	upload, err := url.Parse(uploadURL)
+	if err != nil {
+		return err
+	}
+
+	client.BaseURL = api
+	client.UploadURL = upload
+
+	return nil
+}
diff --git a/internal/client/gitlab.go b/internal/client/gitlab.go
index 02a819b3..67b96161 100644
--- a/internal/client/gitlab.go
+++ b/internal/client/gitlab.go
@@ -35,7 +35,12 @@ func NewGitLab(ctx *context.Context, token string) (Client, error) {
 		}),
 	}
 	if ctx.Config.GitLabURLs.API != "" {
-		options = append(options, gitlab.WithBaseURL(ctx.Config.GitLabURLs.API))
+		apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.API)
+		if err != nil {
+			return nil, fmt.Errorf("templating GitLab API URL: %w", err)
+		}
+
+		options = append(options, gitlab.WithBaseURL(apiURL))
 	}
 	client, err := gitlab.NewClient(token, options...)
 	if err != nil {
@@ -270,17 +275,22 @@ func (c *gitlabClient) ReleaseURLTemplate(ctx *context.Context) (string, error)
 	if err != nil {
 		return "", err
 	}
+	downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
+	if err != nil {
+		return "", err
+	}
+
 	if ctx.Config.Release.GitLab.Owner != "" {
 		urlTemplate = fmt.Sprintf(
 			"%s/%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
-			ctx.Config.GitLabURLs.Download,
+			downloadURL,
 			ctx.Config.Release.GitLab.Owner,
 			gitlabName,
 		)
 	} else {
 		urlTemplate = fmt.Sprintf(
 			"%s/%s/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
-			ctx.Config.GitLabURLs.Download,
+			downloadURL,
 			gitlabName,
 		)
 	}
@@ -326,7 +336,11 @@ func (c *gitlabClient) Upload(
 		return err
 	}
 
-	gitlabBaseURL := ctx.Config.GitLabURLs.Download
+	gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
+	if err != nil {
+		return fmt.Errorf("templating GitLab Download URL: %w", err)
+	}
+
 	linkURL := gitlabBaseURL + "/" + projectDetails.PathWithNamespace + projectFile.URL
 	name := artifact.Name
 	filename := "/" + name
diff --git a/internal/pipe/defaults/defaults.go b/internal/pipe/defaults/defaults.go
index 538700e2..4245c8de 100644
--- a/internal/pipe/defaults/defaults.go
+++ b/internal/pipe/defaults/defaults.go
@@ -3,10 +3,12 @@
 package defaults
 
 import (
+	"fmt"
 	"strings"
 
 	"github.com/goreleaser/goreleaser/internal/client"
 	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/goreleaser/goreleaser/pkg/defaults"
 )
@@ -30,7 +32,12 @@ func (Pipe) Run(ctx *context.Context) error {
 		ctx.Config.GitLabURLs.Download = client.DefaultGitLabDownloadURL
 	}
 	if ctx.Config.GiteaURLs.Download == "" {
-		ctx.Config.GiteaURLs.Download = strings.ReplaceAll(ctx.Config.GiteaURLs.API, "/api/v1", "")
+		apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GiteaURLs.API)
+		if err != nil {
+			return fmt.Errorf("templating Gitea API URL: %w", err)
+		}
+
+		ctx.Config.GiteaURLs.Download = strings.ReplaceAll(apiURL, "/api/v1", "")
 	}
 	for _, defaulter := range defaults.Defaulters {
 		if err := middleware.Logging(
diff --git a/www/docs/scm/gitea.md b/www/docs/scm/gitea.md
index 12b6abd2..a0272e3a 100644
--- a/www/docs/scm/gitea.md
+++ b/www/docs/scm/gitea.md
@@ -19,7 +19,7 @@ env_files:
 ## URLs
 
 You can use GoReleaser with Gitea by providing its URLs in
-the `.goreleaser.yml` configuration file:
+the `.goreleaser.yml` configuration file. This takes a normal string or a template value.
 
 ```yaml
 # .goreleaser.yml
diff --git a/www/docs/scm/github.md b/www/docs/scm/github.md
index cd6e85fb..7b97df63 100644
--- a/www/docs/scm/github.md
+++ b/www/docs/scm/github.md
@@ -18,7 +18,9 @@ env_files:
 
 ## GitHub Enterprise
 
-You can use GoReleaser with GitHub Enterprise by providing its URLs in the `.goreleaser.yml` configuration file:
+You can use GoReleaser with GitHub Enterprise by providing its URLs in the
+`.goreleaser.yml` configuration file. This takes a normal string or a template
+value.
 
 ```yaml
 # .goreleaser.yml
diff --git a/www/docs/scm/gitlab.md b/www/docs/scm/gitlab.md
index ed058e96..5cfc47bd 100644
--- a/www/docs/scm/gitlab.md
+++ b/www/docs/scm/gitlab.md
@@ -18,8 +18,8 @@ env_files:
 
 ## GitLab Enterprise or private hosted
 
-You can use GoReleaser with GitLab Enterprise by providing its URLs in
-the `.goreleaser.yml` configuration file:
+You can use GoReleaser with GitLab Enterprise by providing its URLs in the
+`.goreleaser.yml` configuration file. This takes a normal string or a template value.
 
 ```yaml
 # .goreleaser.yml


Test patch:
diff --git a/internal/client/gitea_test.go b/internal/client/gitea_test.go
index 05a47079..b1037c77 100644
--- a/internal/client/gitea_test.go
+++ b/internal/client/gitea_test.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"strings"
 	"testing"
+	"text/template"
 
 	"code.gitea.io/sdk/gitea"
 	"github.com/goreleaser/goreleaser/internal/artifact"
@@ -23,30 +24,95 @@ type GetInstanceURLSuite struct {
 func (s *GetInstanceURLSuite) TestWithScheme() {
 	t := s.T()
 	rootURL := "https://gitea.com"
-	result, err := getInstanceURL(rootURL + "/api/v1")
+	ctx := context.New(config.Project{
+		GiteaURLs: config.GiteaURLs{
+			API: rootURL + "/api/v1",
+		},
+	})
+
+	result, err := getInstanceURL(ctx)
 	require.NoError(t, err)
 	require.Equal(t, rootURL, result)
 }
 
 func (s *GetInstanceURLSuite) TestParseError() {
 	t := s.T()
-	host := "://wrong.gitea.com"
-	result, err := getInstanceURL(host)
+	ctx := context.New(config.Project{
+		GiteaURLs: config.GiteaURLs{
+			API: "://wrong.gitea.com",
+		},
+	})
+
+	result, err := getInstanceURL(ctx)
 	require.Error(t, err)
 	require.Empty(t, result)
 }
 
 func (s *GetInstanceURLSuite) TestNoScheme() {
 	t := s.T()
-	host := "gitea.com"
-	result, err := getInstanceURL(host)
+	ctx := context.New(config.Project{
+		GiteaURLs: config.GiteaURLs{
+			API: "gitea.com",
+		},
+	})
+
+	result, err := getInstanceURL(ctx)
 	require.Error(t, err)
 	require.Empty(t, result)
 }
 
 func (s *GetInstanceURLSuite) TestEmpty() {
 	t := s.T()
-	result, err := getInstanceURL("")
+	ctx := context.New(config.Project{
+		GiteaURLs: config.GiteaURLs{
+			API: "",
+		},
+	})
+
+	result, err := getInstanceURL(ctx)
+	require.Error(t, err)
+	require.Empty(t, result)
+}
+
+func (s *GetInstanceURLSuite) TestTemplate() {
+	t := s.T()
+	rootURL := "https://gitea.mycompany.com"
+	ctx := context.New(config.Project{
+		Env: []string{
+			fmt.Sprintf("GORELEASER_TEST_GITAEA_URLS_API=%s", rootURL),
+		},
+		GiteaURLs: config.GiteaURLs{
+			API: "{{ .Env.GORELEASER_TEST_GITAEA_URLS_API }}",
+		},
+	})
+
+	result, err := getInstanceURL(ctx)
+	require.NoError(t, err)
+	require.Equal(t, rootURL, result)
+}
+
+func (s *GetInstanceURLSuite) TestTemplateMissingValue() {
+	t := s.T()
+	ctx := context.New(config.Project{
+		GiteaURLs: config.GiteaURLs{
+			API: "{{ .Env.GORELEASER_NOT_EXISTS }}",
+		},
+	})
+
+	result, err := getInstanceURL(ctx)
+	require.ErrorAs(t, err, &template.ExecError{})
+	require.Empty(t, result)
+}
+
+func (s *GetInstanceURLSuite) TestTemplateInvalid() {
+	t := s.T()
+	ctx := context.New(config.Project{
+		GiteaURLs: config.GiteaURLs{
+			API: "{{.dddddddddd",
+		},
+	})
+
+	result, err := getInstanceURL(ctx)
 	require.Error(t, err)
 	require.Empty(t, result)
 }
@@ -408,24 +474,62 @@ func TestGiteaUploadSuite(t *testing.T) {
 }
 
 func TestGiteaReleaseURLTemplate(t *testing.T) {
-	ctx := context.New(config.Project{
-		GiteaURLs: config.GiteaURLs{
-			API:      "https://gitea.com/api/v1",
-			Download: "https://gitea.com",
+	tests := []struct {
+		name            string
+		downloadURL     string
+		wantDownloadURL string
+		wantErr         bool
+	}{
+		{
+			name:            "string_url",
+			downloadURL:     "https://gitea.com",
+			wantDownloadURL: "https://gitea.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
 		},
-		Release: config.Release{
-			Gitea: config.Repo{
-				Owner: "owner",
-				Name:  "name",
-			},
+		{
+			name:            "download_url_template",
+			downloadURL:     "{{ .Env.GORELEASER_TEST_GITEA_URLS_DOWNLOAD }}",
+			wantDownloadURL: "https://gitea.mycompany.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
 		},
-	})
-	client, err := NewGitea(ctx, ctx.Token)
-	require.NoError(t, err)
-
-	urlTpl, err := client.ReleaseURLTemplate(ctx)
-	require.NoError(t, err)
+		{
+			name:        "download_url_template_invalid_value",
+			downloadURL: "{{ .Env.GORELEASER_NOT_EXISTS }}",
+			wantErr:     true,
+		},
+		{
+			name:        "download_url_template_invalid",
+			downloadURL: "{{.dddddddddd",
+			wantErr:     true,
+		},
+	}
 
-	expectedURL := "https://gitea.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
-	require.Equal(t, expectedURL, urlTpl)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := context.New(config.Project{
+				Env: []string{
+					"GORELEASER_TEST_GITEA_URLS_DOWNLOAD=https://gitea.mycompany.com",
+				},
+				GiteaURLs: config.GiteaURLs{
+					API:      "https://gitea.com/api/v1",
+					Download: tt.downloadURL,
+				},
+				Release: config.Release{
+					Gitea: config.Repo{
+						Owner: "owner",
+						Name:  "name",
+					},
+				},
+			})
+			client, err := NewGitea(ctx, ctx.Token)
+			require.NoError(t, err)
+
+			urlTpl, err := client.ReleaseURLTemplate(ctx)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantDownloadURL, urlTpl)
+		})
+	}
 }
diff --git a/internal/client/github_test.go b/internal/client/github_test.go
index 6069c8ca..f0d3173d 100644
--- a/internal/client/github_test.go
+++ b/internal/client/github_test.go
@@ -1,7 +1,9 @@
 package client
 
 import (
+	"fmt"
 	"testing"
+	"text/template"
 
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/pkg/config"
@@ -11,15 +13,21 @@ import (
 
 func TestNewGitHubClient(t *testing.T) {
 	t.Run("good urls", func(t *testing.T) {
+		githubURL := "https://github.mycompany.com"
 		ctx := context.New(config.Project{
 			GitHubURLs: config.GitHubURLs{
-				API:    "https://github.mycompany.com/api",
-				Upload: "https://github.mycompany.com/upload",
+				API:    githubURL + "/api",
+				Upload: githubURL + "/upload",
 			},
 		})
-		_, err := NewGitHub(ctx, ctx.Token)
 
+		client, err := NewGitHub(ctx, ctx.Token)
 		require.NoError(t, err)
+
+		githubClient, ok := client.(*githubClient)
+		require.True(t, ok)
+		require.Equal(t, githubURL+"/api", githubClient.client.BaseURL.String())
+		require.Equal(t, githubURL+"/upload", githubClient.client.UploadURL.String())
 	})
 
 	t.Run("bad api url", func(t *testing.T) {
@@ -45,6 +53,62 @@ func TestNewGitHubClient(t *testing.T) {
 
 		require.EqualError(t, err, `parse "not a url:4994": first path segment in URL cannot contain colon`)
 	})
+
+	t.Run("template", func(t *testing.T) {
+		githubURL := "https://github.mycompany.com"
+		ctx := context.New(config.Project{
+			Env: []string{
+				fmt.Sprintf("GORELEASER_TEST_GITHUB_URLS_API=%s/api", githubURL),
+				fmt.Sprintf("GORELEASER_TEST_GITHUB_URLS_UPLOAD=%s/upload", githubURL),
+			},
+			GitHubURLs: config.GitHubURLs{
+				API:    "{{ .Env.GORELEASER_TEST_GITHUB_URLS_API }}",
+				Upload: "{{ .Env.GORELEASER_TEST_GITHUB_URLS_UPLOAD }}",
+			},
+		})
+
+		client, err := NewGitHub(ctx, ctx.Token)
+		require.NoError(t, err)
+
+		githubClient, ok := client.(*githubClient)
+		require.True(t, ok)
+		require.Equal(t, githubURL+"/api", githubClient.client.BaseURL.String())
+		require.Equal(t, githubURL+"/upload", githubClient.client.UploadURL.String())
+	})
+
+	t.Run("template invalid api", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			GitHubURLs: config.GitHubURLs{
+				API: "{{ .Env.GORELEASER_NOT_EXISTS }}",
+			},
+		})
+
+		_, err := NewGitHub(ctx, ctx.Token)
+		require.ErrorAs(t, err, &template.ExecError{})
+	})
+
+	t.Run("template invalid upload", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			GitHubURLs: config.GitHubURLs{
+				API:    "https://github.mycompany.com/api",
+				Upload: "{{ .Env.GORELEASER_NOT_EXISTS }}",
+			},
+		})
+
+		_, err := NewGitHub(ctx, ctx.Token)
+		require.ErrorAs(t, err, &template.ExecError{})
+	})
+
+	t.Run("template invalid", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			GitHubURLs: config.GitHubURLs{
+				API: "{{.dddddddddd",
+			},
+		})
+
+		_, err := NewGitHub(ctx, ctx.Token)
+		require.Error(t, err)
+	})
 }
 
 func TestGitHubUploadReleaseIDNotInt(t *testing.T) {
@@ -60,26 +124,63 @@ func TestGitHubUploadReleaseIDNotInt(t *testing.T) {
 }
 
 func TestGitHubReleaseURLTemplate(t *testing.T) {
-	ctx := context.New(config.Project{
-		GitHubURLs: config.GitHubURLs{
-			// default URL would otherwise be set via pipe/defaults
-			Download: DefaultGitHubDownloadURL,
+	tests := []struct {
+		name            string
+		downloadURL     string
+		wantDownloadURL string
+		wantErr         bool
+	}{
+		{
+			name:            "default_download_url",
+			downloadURL:     DefaultGitHubDownloadURL,
+			wantDownloadURL: "https://github.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
 		},
-		Release: config.Release{
-			GitHub: config.Repo{
-				Owner: "owner",
-				Name:  "name",
-			},
+		{
+			name:            "download_url_template",
+			downloadURL:     "{{ .Env.GORELEASER_TEST_GITHUB_URLS_DOWNLOAD }}",
+			wantDownloadURL: "https://github.mycompany.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
 		},
-	})
-	client, err := NewGitHub(ctx, ctx.Token)
-	require.NoError(t, err)
+		{
+			name:        "download_url_template_invalid_value",
+			downloadURL: "{{ .Env.GORELEASER_NOT_EXISTS }}",
+			wantErr:     true,
+		},
+		{
+			name:        "download_url_template_invalid",
+			downloadURL: "{{.dddddddddd",
+			wantErr:     true,
+		},
+	}
 
-	urlTpl, err := client.ReleaseURLTemplate(ctx)
-	require.NoError(t, err)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := context.New(config.Project{
+				Env: []string{
+					"GORELEASER_TEST_GITHUB_URLS_DOWNLOAD=https://github.mycompany.com",
+				},
+				GitHubURLs: config.GitHubURLs{
+					Download: tt.downloadURL,
+				},
+				Release: config.Release{
+					GitHub: config.Repo{
+						Owner: "owner",
+						Name:  "name",
+					},
+				},
+			})
+			client, err := NewGitHub(ctx, ctx.Token)
+			require.NoError(t, err)
+
+			urlTpl, err := client.ReleaseURLTemplate(ctx)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
 
-	expectedURL := "https://github.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
-	require.Equal(t, expectedURL, urlTpl)
+			require.NoError(t, err)
+			require.Equal(t, tt.wantDownloadURL, urlTpl)
+		})
+	}
 }
 
 func TestGitHubCreateReleaseWrongNameTemplate(t *testing.T) {
diff --git a/internal/client/gitlab_test.go b/internal/client/gitlab_test.go
index 8c789145..6b06eacd 100644
--- a/internal/client/gitlab_test.go
+++ b/internal/client/gitlab_test.go
@@ -1,32 +1,229 @@
 package client
 
 import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"strings"
 	"testing"
+	"text/template"
 
+	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
 )
 
 func TestGitLabReleaseURLTemplate(t *testing.T) {
-	ctx := context.New(config.Project{
-		GitLabURLs: config.GitLabURLs{
-			// default URL would otherwise be set via pipe/defaults
-			Download: DefaultGitLabDownloadURL,
-		},
-		Release: config.Release{
-			GitLab: config.Repo{
-				Owner: "owner",
-				Name:  "name",
+	tests := []struct {
+		name            string
+		downloadURL     string
+		wantDownloadURL string
+		wantErr         bool
+	}{
+		{
+			name:            "default_download_url",
+			downloadURL:     DefaultGitLabDownloadURL,
+			wantDownloadURL: "https://gitlab.com/owner/name/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
+		},
+		{
+			name:            "download_url_template",
+			downloadURL:     "{{ .Env.GORELEASER_TEST_GITLAB_URLS_DOWNLOAD }}",
+			wantDownloadURL: "https://gitlab.mycompany.com/owner/name/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}",
+		},
+		{
+			name:        "download_url_template_invalid_value",
+			downloadURL: "{{ .Env.GORELEASER_NOT_EXISTS }}",
+			wantErr:     true,
+		},
+		{
+			name:        "download_url_template_invalid",
+			downloadURL: "{{.dddddddddd",
+			wantErr:     true,
+		},
+	}
+
+	for _, tt := range tests {
+		ctx := context.New(config.Project{
+			Env: []string{
+				"GORELEASER_TEST_GITLAB_URLS_DOWNLOAD=https://gitlab.mycompany.com",
 			},
+			GitLabURLs: config.GitLabURLs{
+				Download: tt.downloadURL,
+			},
+			Release: config.Release{
+				GitLab: config.Repo{
+					Owner: "owner",
+					Name:  "name",
+				},
+			},
+		})
+		client, err := NewGitLab(ctx, ctx.Token)
+		require.NoError(t, err)
+
+		urlTpl, err := client.ReleaseURLTemplate(ctx)
+		if tt.wantErr {
+			require.Error(t, err)
+			return
+		}
+
+		require.NoError(t, err)
+		require.Equal(t, tt.wantDownloadURL, urlTpl)
+	}
+}
+
+func TestGitLabURLsAPITemplate(t *testing.T) {
+	tests := []struct {
+		name     string
+		apiURL   string
+		wantHost string
+	}{
+		{
+			name:     "default_values",
+			wantHost: "gitlab.com",
+		},
+		{
+			name:     "speicifed_api_env_key",
+			apiURL:   "https://gitlab.mycompany.com",
+			wantHost: "gitlab.mycompany.com",
 		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			envs := []string{}
+			gitlabURLs := config.GitLabURLs{}
+
+			if tt.apiURL != "" {
+				envs = append(envs, fmt.Sprintf("GORELEASER_TEST_GITLAB_URLS_API=%s", tt.apiURL))
+				gitlabURLs.API = "{{ .Env.GORELEASER_TEST_GITLAB_URLS_API }}"
+			}
+
+			ctx := context.New(config.Project{
+				Env:        envs,
+				GitLabURLs: gitlabURLs,
+			})
+
+			client, err := NewGitLab(ctx, ctx.Token)
+			require.NoError(t, err)
+
+			gitlabClient, ok := client.(*gitlabClient)
+			require.True(t, ok)
+
+			require.Equal(t, tt.wantHost, gitlabClient.client.BaseURL().Host)
+		})
+	}
+
+	t.Run("no_env_specified", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			GitLabURLs: config.GitLabURLs{
+				API: "{{ .Env.GORELEASER_NOT_EXISTS }}",
+			},
+		})
+
+		_, err := NewGitLab(ctx, ctx.Token)
+		require.ErrorAs(t, err, &template.ExecError{})
+	})
+
+	t.Run("invalid_template", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			GitLabURLs: config.GitLabURLs{
+				API: "{{.dddddddddd",
+			},
+		})
+
+		_, err := NewGitLab(ctx, ctx.Token)
+		require.Error(t, err)
 	})
-	client, err := NewGitLab(ctx, ctx.Token)
-	require.NoError(t, err)
+}
+
+func TestGitLabURLsDownloadTemplate(t *testing.T) {
+	tests := []struct {
+		name        string
+		downloadURL string
+		wantURL     string
+		wantErr     bool
+	}{
+		{
+			name:    "empty_download_url",
+			wantURL: "/",
+		},
+		{
+			name:        "download_url_template",
+			downloadURL: "{{ .Env.GORELEASER_TEST_GITLAB_URLS_DOWNLOAD }}",
+			wantURL:     "https://gitlab.mycompany.com/",
+		},
+		{
+			name:        "download_url_template_invalid_value",
+			downloadURL: "{{ .Eenv.GORELEASER_NOT_EXISTS }}",
+			wantErr:     true,
+		},
+		{
+			name:        "download_url_template_invalid",
+			downloadURL: "{{.dddddddddd",
+			wantErr:     true,
+		},
+		{
+			name:        "download_url_string",
+			downloadURL: "https://gitlab.mycompany.com",
+			wantURL:     "https://gitlab.mycompany.com/",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				defer fmt.Fprint(w, "{}")
+				defer w.WriteHeader(http.StatusOK)
+				defer r.Body.Close()
+
+				if !strings.Contains(r.URL.Path, "assets/links") {
+					_, _ = io.Copy(io.Discard, r.Body)
+					return
+				}
+
+				b, err := io.ReadAll(r.Body)
+				require.NoError(t, err)
+
+				reqBody := map[string]interface{}{}
+				err = json.Unmarshal(b, &reqBody)
+				require.NoError(t, err)
+
+				require.Equal(t, tt.wantURL, reqBody["url"])
+			}))
+			defer srv.Close()
+
+			ctx := context.New(config.Project{
+				Env: []string{
+					"GORELEASER_TEST_GITLAB_URLS_DOWNLOAD=https://gitlab.mycompany.com",
+				},
+				Release: config.Release{
+					GitLab: config.Repo{
+						Owner: "test",
+						Name:  "test",
+					},
+				},
+				GitLabURLs: config.GitLabURLs{
+					API:      srv.URL,
+					Download: tt.downloadURL,
+				},
+			})
+
+			tmpFile, err := os.CreateTemp(t.TempDir(), "")
+			require.NoError(t, err)
 
-	urlTpl, err := client.ReleaseURLTemplate(ctx)
-	require.NoError(t, err)
+			client, err := NewGitLab(ctx, ctx.Token)
+			require.NoError(t, err)
 
-	expectedURL := "https://gitlab.com/owner/name/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}"
-	require.Equal(t, expectedURL, urlTpl)
+			err = client.Upload(ctx, "1234", &artifact.Artifact{Name: "test", Path: "some-path"}, tmpFile)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
 }
diff --git a/internal/pipe/defaults/defaults_test.go b/internal/pipe/defaults/defaults_test.go
index 4880dec6..c1f2c2f6 100644
--- a/internal/pipe/defaults/defaults_test.go
+++ b/internal/pipe/defaults/defaults_test.go
@@ -114,3 +114,53 @@ func TestFillPartial(t *testing.T) {
 	require.NoError(t, Pipe{}.Run(ctx))
 	require.Equal(t, "https://gitea.com", ctx.Config.GiteaURLs.Download)
 }
+
+func TestGiteaTemplateDownloadURL(t *testing.T) {
+	tests := []struct {
+		name    string
+		apiURL  string
+		wantErr bool
+	}{
+		{
+			name:   "string_url",
+			apiURL: "https://gitea.com/api/v1",
+		},
+		{
+			name:   "download_url_template",
+			apiURL: "{{ .Env.GORELEASER_TEST_GITEA_URLS_API }}",
+		},
+		{
+			name:    "download_url_template_invalid_value",
+			apiURL:  "{{ .Env.GORELEASER_NOT_EXISTS }}",
+			wantErr: true,
+		},
+		{
+			name:    "download_url_template_invalid",
+			apiURL:  "{{.dddddddddd",
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitea,
+			Env: context.Env{
+				"GORELEASER_TEST_GITEA_URLS_API": "https://gitea.com/api/v1",
+			},
+			Config: config.Project{
+				GiteaURLs: config.GiteaURLs{
+					API: tt.apiURL,
+				},
+			},
+		}
+
+		err := Pipe{}.Run(ctx)
+		if tt.wantErr {
+			require.Error(t, err)
+			return
+		}
+
+		require.NoError(t, err)
+		require.Equal(t, "https://gitea.com", ctx.Config.GiteaURLs.Download)
+	}
+}