Repository: goreleaser/goreleaser
Pull request #4261 (2023-08-24 16:46:50)

fix: failing when pull_request.base is empty
if the base branch is empty, it'll try to fetch the default branch, which would fail if base repo name/owner are also empty.

the default behavior when base owner/name are missing is to using head's, so I changed it do that before trying to get the default branch, which should fix the issue.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/goreleaser/goreleaser/pull/4261?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#4261](https://app.codecov.io/gh/goreleaser/goreleaser/pull/4261?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (f247f59) into [main](https://app.codecov.io/gh/goreleaser/goreleaser/commit/76f2b81bb503277d5c63b71e78a8e50637bb86b7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (76f2b81) will **increase** coverage by `0.00%`.
> Report is 1 commits behind head on main.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main    #4261   +/-   ##
=======================================
  Coverage   84.07%   84.07%           
=======================================
  Files         131      131           
  Lines       12415    12417    +2     
=======================================
+ Hits        10438    10440    +2     
  Misses       1575     1575           
  Partials      402      402           
```


| [Files Changed](https://app.codecov.io/gh/goreleaser/goreleaser/pull/4261?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/client/github.go](https://app.codecov.io/gh/goreleaser/goreleaser/pull/4261?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvY2xpZW50L2dpdGh1Yi5nbw==) | `59.68% <100.00%> (+0.18%)` | :arrow_up: |



Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9388c4c4..0a72c0bf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -29,7 +29,7 @@ jobs:
     env:
       DOCKER_CLI_EXPERIMENTAL: "enabled"
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
         with:
           fetch-depth: 0
       - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index f0f41898..6bebf1b8 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -15,7 +15,7 @@ jobs:
       contents: read
 
     steps:
-    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+    - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
     - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4
       with:
         go-version: stable
diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml
index bb9f369a..7450f922 100644
--- a/.github/workflows/depsreview.yaml
+++ b/.github/workflows/depsreview.yaml
@@ -8,7 +8,7 @@ jobs:
   dependency-review:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
       - uses: actions/dependency-review-action@v3
         with:
           allow-licenses: BSD-2-Clause, BSD-3-Clause, MIT, Apache-2.0, MPL-2.0
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 8d5144cd..914b5211 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -17,7 +17,7 @@ jobs:
   htmltest:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
       - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1
         with:
           repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/fig.yml b/.github/workflows/fig.yml
index 65fd8ab6..8450ca51 100644
--- a/.github/workflows/fig.yml
+++ b/.github/workflows/fig.yml
@@ -13,7 +13,7 @@ jobs:
   fig:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
       - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4
         with:
           go-version: stable
diff --git a/.github/workflows/generate-releases.yml b/.github/workflows/generate-releases.yml
index f2fbe0b6..0993b2e6 100644
--- a/.github/workflows/generate-releases.yml
+++ b/.github/workflows/generate-releases.yml
@@ -12,7 +12,7 @@ jobs:
       contents: write  # for stefanzweifel/git-auto-commit-action to push code in repo
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
         with:
           token: ${{ secrets.GH_PAT }}
       - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1
diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml
index 79d11c59..867983f3 100644
--- a/.github/workflows/generate.yml
+++ b/.github/workflows/generate.yml
@@ -19,7 +19,7 @@ jobs:
       contents: write  # for stefanzweifel/git-auto-commit-action to push code in repo
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
         with:
           token: ${{ secrets.GH_PAT }}
       - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4
diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
index c3bce88b..68bb1d3b 100644
--- a/.github/workflows/gitleaks.yml
+++ b/.github/workflows/gitleaks.yml
@@ -14,7 +14,7 @@ jobs:
   gitleaks:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
         with:
           fetch-depth: 0
       - uses: gitleaks/gitleaks-action@v2
diff --git a/.github/workflows/grype.yml b/.github/workflows/grype.yml
index d81d732f..2cf37743 100644
--- a/.github/workflows/grype.yml
+++ b/.github/workflows/grype.yml
@@ -17,7 +17,7 @@ jobs:
       contents: read
 
     steps:
-    - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+    - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
     - uses: anchore/scan-action@v3
       with:
         path: "."
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 712816af..56e8d0ce 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,7 +17,7 @@ jobs:
     name: lint
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
       - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4
         with:
           go-version: stable
diff --git a/.github/workflows/nightly-oss.yml b/.github/workflows/nightly-oss.yml
index 1a5a5e3a..3eead07c 100644
--- a/.github/workflows/nightly-oss.yml
+++ b/.github/workflows/nightly-oss.yml
@@ -16,7 +16,7 @@ jobs:
     env:
       DOCKER_CLI_EXPERIMENTAL: "enabled"
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
         with:
           fetch-depth: 0
       - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 76de2ca5..f80e7505 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -49,7 +49,7 @@ jobs:
       matrix:
         format: [ deb, rpm, apk ]
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
         with:
           fetch-depth: 0
       - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1
@@ -70,7 +70,7 @@ jobs:
     env:
       DOCKER_CLI_EXPERIMENTAL: "enabled"
     steps:
-      - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
+      - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
         with:
           fetch-depth: 0
       - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index f9e1f9b6..f34b25ed 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -17,6 +17,10 @@ gomod:
 
 report_sizes: true
 
+git:
+  ignore_tags:
+    - nightly
+
 metadata:
   mod_timestamp: "{{ .CommitTimestamp }}"
 
diff --git a/internal/client/github.go b/internal/client/github.go
index 268ea217..dad28703 100644
--- a/internal/client/github.go
+++ b/internal/client/github.go
@@ -196,6 +196,8 @@ func (c *githubClient) OpenPullRequest(
 	draft bool,
 ) error {
 	c.checkRateLimit(ctx)
+	base.Owner = firstNonEmpty(base.Owner, head.Owner)
+	base.Name = firstNonEmpty(base.Name, head.Name)
 	if base.Branch == "" {
 		def, err := c.getDefaultBranch(ctx, base)
 		if err != nil {
@@ -217,8 +219,8 @@ func (c *githubClient) OpenPullRequest(
 	log.Info("opening pull request")
 	pr, res, err := c.client.PullRequests.Create(
 		ctx,
-		firstNonEmpty(base.Owner, head.Owner),
-		firstNonEmpty(base.Name, head.Name),
+		base.Owner,
+		base.Name,
 		&github.NewPullRequest{
 			Title: github.String(title),
 			Base:  github.String(base.Branch),
diff --git a/internal/pipe/git/errors.go b/internal/pipe/git/errors.go
index 331508ec..d1fa8bc4 100644
--- a/internal/pipe/git/errors.go
+++ b/internal/pipe/git/errors.go
@@ -3,6 +3,9 @@ package git
 import (
 	"errors"
 	"fmt"
+	"regexp/syntax"
+
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 )
 
 // ErrDirty happens when the repo has uncommitted/unstashed changes.
@@ -33,3 +36,12 @@ var ErrNotRepository = errors.New("current folder is not a git repository")
 
 // ErrNoGit happens when git is not present in PATH.
 var ErrNoGit = errors.New("git not present in PATH")
+
+// shouldErr returns true if the errors are template or regex related.
+func shouldErr(err error) bool {
+	if errors.As(err, &tmpl.Error{}) {
+		return true
+	}
+	se := &syntax.Error{}
+	return errors.As(err, &se)
+}
diff --git a/internal/pipe/git/git.go b/internal/pipe/git/git.go
index 29deabf8..66ad07a5 100644
--- a/internal/pipe/git/git.go
+++ b/internal/pipe/git/git.go
@@ -5,6 +5,7 @@ import (
 	"net/url"
 	"os"
 	"os/exec"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
@@ -12,6 +13,7 @@ import (
 	"github.com/caarlos0/log"
 	"github.com/goreleaser/goreleaser/internal/git"
 	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
@@ -115,7 +117,10 @@ func getGitInfo(ctx *context.Context) (context.GitInfo, error) {
 	}
 
 	tag, err := getTag(ctx)
-	if err != nil {
+	if err != nil && shouldErr(err) {
+		return context.GitInfo{}, fmt.Errorf("could not get current tag: %w", err)
+	}
+	if tag == "" {
 		return context.GitInfo{
 			Branch:      branch,
 			Commit:      full,
@@ -254,12 +259,16 @@ func getTag(ctx *context.Context) (string, error) {
 		},
 	} {
 		tags, err := fn()
-		if len(tags) > 0 {
-			return tags[0], err
+		if err != nil {
+			return "", err
 		}
+		tags, err = filterOutTags(ctx, tags)
 		if err != nil {
 			return "", err
 		}
+		if len(tags) > 0 {
+			return tags[0], err
+		}
 	}
 
 	return "", nil
@@ -277,17 +286,47 @@ func getPreviousTag(ctx *context.Context, current string) (string, error) {
 		},
 	} {
 		tags, err := fn()
-		if len(tags) > 0 {
-			return tags[0], err
+		if err != nil {
+			return "", err
 		}
+		tags, err = filterOutTags(ctx, tags)
 		if err != nil {
 			return "", err
 		}
+		if len(tags) > 0 {
+			return tags[0], err
+		}
 	}
 
 	return "", nil
 }
 
+func filterOutTags(ctx *context.Context, tags []string) ([]string, error) {
+	regexes := []*regexp.Regexp{}
+	for _, s := range ctx.Config.Git.IgnoreTags {
+		applied, err := tmpl.New(ctx).Apply(s)
+		if err != nil {
+			return nil, err
+		}
+		re, err := regexp.Compile(applied)
+		if err != nil {
+			return nil, err
+		}
+		regexes = append(regexes, re)
+	}
+	var result []string
+outer:
+	for _, tag := range tags {
+		for _, re := range regexes {
+			if re.MatchString(tag) {
+				continue outer
+			}
+		}
+		result = append(result, tag)
+	}
+	return result, nil
+}
+
 func gitTagsPointingAt(ctx *context.Context, ref string) ([]string, error) {
 	args := []string{}
 	if ctx.Config.Git.PrereleaseSuffix != "" {
diff --git a/internal/pipe/webhook/webhook.go b/internal/pipe/webhook/webhook.go
index 1c9121bc..5c867032 100644
--- a/internal/pipe/webhook/webhook.go
+++ b/internal/pipe/webhook/webhook.go
@@ -68,7 +68,7 @@ func (p Pipe) Announce(ctx *context.Context) error {
 
 	msg, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Webhook.MessageTemplate)
 	if err != nil {
-		return fmt.Errorf("webhook: %s", err)
+		return fmt.Errorf("webhook: %w", err)
 	}
 
 	log.Infof("posting: '%s'", msg)
diff --git a/internal/tmpl/errors.go b/internal/tmpl/errors.go
new file mode 100644
index 00000000..90030b84
--- /dev/null
+++ b/internal/tmpl/errors.go
@@ -0,0 +1,45 @@
+package tmpl
+
+import (
+	"fmt"
+	"regexp"
+)
+
+var res = []*regexp.Regexp{
+	regexp.MustCompile(`^template: tmpl:\d+:\d+: executing ".+" at .+: `),
+	regexp.MustCompile(`^template: tmpl:\d+:\d+: `),
+	regexp.MustCompile(`^template: tmpl:\d+: `),
+}
+
+func newTmplError(str string, err error) error {
+	if err == nil {
+		return nil
+	}
+	details := err.Error()
+	for _, re := range res {
+		if re.MatchString(details) {
+			details = re.ReplaceAllString(details, "")
+			break
+		}
+	}
+	return Error{str, details, err}
+}
+
+// Error is returned on any template error.
+type Error struct {
+	str     string
+	details string
+	err     error
+}
+
+func (e Error) Unwrap() error {
+	return e.err
+}
+
+func (e Error) Error() string {
+	return fmt.Sprintf(
+		"template: failed to apply %q: %s",
+		e.str,
+		e.details,
+	)
+}
diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go
index bd58963f..72cf98e3 100644
--- a/internal/tmpl/tmpl.go
+++ b/internal/tmpl/tmpl.go
@@ -223,11 +223,11 @@ func (t *Template) Apply(s string) (string, error) {
 		}).
 		Parse(s)
 	if err != nil {
-		return "", err
+		return "", newTmplError(s, err)
 	}
 
 	err = tmpl.Execute(&out, t.fields)
-	return out.String(), err
+	return out.String(), newTmplError(s, err)
 }
 
 // ApplyAll applies all the given strings against the Fields stored in the
@@ -237,7 +237,7 @@ func (t *Template) ApplyAll(sps ...*string) error {
 		s := *sp
 		result, err := t.Apply(s)
 		if err != nil {
-			return fmt.Errorf("failed to apply template: %s: %w", s, err)
+			return newTmplError(s, err)
 		}
 		*sp = result
 	}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index d04b28c2..2467a3b2 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -18,8 +18,9 @@ import (
 
 // Git configs.
 type Git struct {
-	TagSort          string `yaml:"tag_sort,omitempty" json:"tag_sort,omitempty"`
-	PrereleaseSuffix string `yaml:"prerelease_suffix,omitempty" json:"prerelease_suffix,omitempty"`
+	TagSort          string   `yaml:"tag_sort,omitempty" json:"tag_sort,omitempty"`
+	PrereleaseSuffix string   `yaml:"prerelease_suffix,omitempty" json:"prerelease_suffix,omitempty"`
+	IgnoreTags       []string `yaml:"ignore_tags,omitempty" json:"ignore_tags,omitempty"`
 }
 
 // GitHubURLs holds the URLs to be used when using github enterprise.
diff --git a/www/docs/customization/git.md b/www/docs/customization/git.md
index a7fa3a64..289a4249 100644
--- a/www/docs/customization/git.md
+++ b/www/docs/customization/git.md
@@ -18,4 +18,14 @@ git:
   #
   # Since: v1.17
   prerelease_suffix: "-"
+
+  # Regular expressions for tags to be ignored by GoReleaser.
+  # This means that GoReleaser will not pick up tags that match any of the
+  # provided ignores as either previous or current tags.
+  #
+  # Templates: allowed.
+  # Since: v1.21.
+  ignore_tags:
+    - nightly
+    - "{{.Env.FOO}}.*"
 ```
diff --git a/www/docs/static/schema.json b/www/docs/static/schema.json
index e8090ee1..b68b2f12 100644
--- a/www/docs/static/schema.json
+++ b/www/docs/static/schema.json
@@ -1028,6 +1028,12 @@
 					},
 					"prerelease_suffix": {
 						"type": "string"
+					},
+					"ignore_tags": {
+						"items": {
+							"type": "string"
+						},
+						"type": "array"
 					}
 				},
 				"additionalProperties": false,


Test patch:
diff --git a/internal/client/gitea_test.go b/internal/client/gitea_test.go
index b67e5de2..145dae66 100644
--- a/internal/client/gitea_test.go
+++ b/internal/client/gitea_test.go
@@ -7,11 +7,11 @@ import (
 	"os"
 	"strings"
 	"testing"
-	"text/template"
 
 	"code.gitea.io/sdk/gitea"
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/jarcoal/httpmock"
@@ -102,7 +102,7 @@ func (s *GetInstanceURLSuite) TestTemplateMissingValue() {
 	})
 
 	result, err := getInstanceURL(ctx)
-	require.ErrorAs(t, err, &template.ExecError{})
+	require.ErrorAs(t, err, &tmpl.Error{})
 	require.Empty(t, result)
 }
 
diff --git a/internal/client/github_test.go b/internal/client/github_test.go
index 8effeb9e..013d438b 100644
--- a/internal/client/github_test.go
+++ b/internal/client/github_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/google/go-github/v54/github"
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -197,7 +198,7 @@ func TestGitHubCreateReleaseWrongNameTemplate(t *testing.T) {
 
 	str, err := client.CreateRelease(ctx, "")
 	require.Empty(t, str)
-	require.EqualError(t, err, `template: tmpl:1: unclosed action`)
+	testlib.RequireTemplateError(t, err)
 }
 
 func TestGitHubGetDefaultBranch(t *testing.T) {
@@ -651,6 +652,55 @@ func TestGitHubOpenPullRequestBaseEmpty(t *testing.T) {
 	}))
 	defer srv.Close()
 
+	ctx := testctx.NewWithCfg(config.Project{
+		GitHubURLs: config.GitHubURLs{
+			API: srv.URL + "/",
+		},
+	})
+	client, err := newGitHub(ctx, "test-token")
+	require.NoError(t, err)
+	repo := Repo{
+		Owner:  "someone",
+		Name:   "something",
+		Branch: "foo",
+	}
+
+	require.NoError(t, client.OpenPullRequest(ctx, Repo{}, repo, "some title", false))
+}
+
+func TestGitHubOpenPullRequestHeadEmpty(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+
+		if r.URL.Path == "/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+
+		if r.URL.Path == "/repos/someone/something/pulls" {
+			r, err := os.Open("testdata/github/pull.json")
+			require.NoError(t, err)
+			_, err = io.Copy(w, r)
+			require.NoError(t, err)
+			return
+		}
+
+		if r.URL.Path == "/repos/someone/something" {
+			w.WriteHeader(http.StatusOK)
+			fmt.Fprint(w, `{"default_branch": "main"}`)
+			return
+		}
+
+		if r.URL.Path == "/rate_limit" {
+			w.WriteHeader(http.StatusOK)
+			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
+			return
+		}
+
+		t.Error("unhandled request: " + r.URL.Path)
+	}))
+	defer srv.Close()
+
 	ctx := testctx.NewWithCfg(config.Project{
 		GitHubURLs: config.GitHubURLs{
 			API: srv.URL + "/",
diff --git a/internal/exec/exec_test.go b/internal/exec/exec_test.go
index f5d1bb0a..6e7b6e8b 100644
--- a/internal/exec/exec_test.go
+++ b/internal/exec/exec_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/pipe"
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -88,9 +89,10 @@ func TestExecute(t *testing.T) {
 	}
 
 	testCases := []struct {
-		name       string
-		publishers []config.Publisher
-		expectErr  error
+		name        string
+		publishers  []config.Publisher
+		expectErr   error
+		expectErrAs any
 	}{
 		{
 			"filter by IDs",
@@ -109,6 +111,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"no filter",
@@ -131,6 +134,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"disabled",
@@ -143,6 +147,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			pipe.ErrSkip{},
+			nil,
 		},
 		{
 			"disabled invalid tmpl",
@@ -154,7 +159,8 @@ func TestExecute(t *testing.T) {
 					Env:     []string{},
 				},
 			},
-			fmt.Errorf(`template: tmpl:1:3: executing "tmpl" at <.NOPE>`),
+			nil,
+			&tmpl.Error{},
 		},
 		{
 			"include checksum",
@@ -178,6 +184,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"include signatures",
@@ -202,6 +209,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"docker",
@@ -221,6 +229,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"extra files",
@@ -246,6 +255,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"extra files with rename",
@@ -274,6 +284,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"try dir templating",
@@ -294,6 +305,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"check env templating",
@@ -321,6 +333,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"override path",
@@ -346,6 +359,7 @@ func TestExecute(t *testing.T) {
 				},
 			},
 			nil,
+			nil,
 		},
 		{
 			"command error",
@@ -373,18 +387,23 @@ func TestExecute(t *testing.T) {
 			},
 			// stderr is sent to output via logger
 			fmt.Errorf(`publishing: %s failed: exit status 1: test error`, MockCmd),
+			nil,
 		},
 	}
 
 	for i, tc := range testCases {
 		t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
 			err := Execute(ctx, tc.publishers)
-			if tc.expectErr == nil {
-				require.NoError(t, err)
+			if tc.expectErr != nil {
+				require.Error(t, err)
+				require.True(t, strings.HasPrefix(err.Error(), tc.expectErr.Error()), err.Error())
+				return
+			}
+			if tc.expectErrAs != nil {
+				require.ErrorAs(t, err, tc.expectErrAs)
 				return
 			}
-			require.Error(t, err)
-			require.True(t, strings.HasPrefix(err.Error(), tc.expectErr.Error()), err.Error())
+			require.NoError(t, err)
 		})
 	}
 }
diff --git a/internal/extrafiles/extra_files_test.go b/internal/extrafiles/extra_files_test.go
index 1cea6db6..ac3c559a 100644
--- a/internal/extrafiles/extra_files_test.go
+++ b/internal/extrafiles/extra_files_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -30,7 +31,7 @@ func TestBadTemplate(t *testing.T) {
 	ctx := testctx.New()
 	files, err := Find(ctx, globs)
 	require.Empty(t, files)
-	require.EqualError(t, err, `failed to apply template to glob "./testdata/file{{ .Env.NOPE }}.golden": template: tmpl:1:22: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`)
+	testlib.RequireTemplateError(t, err)
 }
 
 func TestShouldGetSpecificFile(t *testing.T) {
@@ -123,7 +124,7 @@ func TestTargetInvalidNameTemplate(t *testing.T) {
 	ctx := testctx.New()
 	files, err := Find(ctx, globs)
 	require.Empty(t, files)
-	require.EqualError(t, err, `failed to apply template to name "file1_{{.Env.HONK}}.golden": template: tmpl:1:12: executing "tmpl" at <.Env.HONK>: map has no entry for key "HONK"`)
+	testlib.RequireTemplateError(t, err)
 }
 
 func TestTargetNameMatchesMultipleFiles(t *testing.T) {
diff --git a/internal/pipe/archive/archive_meta_test.go b/internal/pipe/archive/archive_meta_test.go
index cb7244a7..f947a561 100644
--- a/internal/pipe/archive/archive_meta_test.go
+++ b/internal/pipe/archive/archive_meta_test.go
@@ -51,7 +51,7 @@ func TestMeta(t *testing.T) {
 		})
 
 		require.NoError(t, Pipe{}.Default(ctx))
-		require.EqualError(t, Pipe{}.Run(ctx), `template: tmpl:1:5: executing "tmpl" at <.Os>: map has no entry for key "Os"`)
+		testlib.RequireTemplateError(t, Pipe{}.Run(ctx))
 	})
 
 	t.Run("no files", func(t *testing.T) {
diff --git a/internal/pipe/archive/archive_test.go b/internal/pipe/archive/archive_test.go
index 447c560e..c3231a17 100644
--- a/internal/pipe/archive/archive_test.go
+++ b/internal/pipe/archive/archive_test.go
@@ -655,7 +655,7 @@ func TestRunPipeInvalidWrapInDirectoryTemplate(t *testing.T) {
 			artifact.ExtraID:     "default",
 		},
 	})
-	require.EqualError(t, Pipe{}.Run(ctx), `template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Run(ctx))
 }
 
 func TestRunPipeWrap(t *testing.T) {
diff --git a/internal/pipe/aur/aur_test.go b/internal/pipe/aur/aur_test.go
index cdbfd3c5..83f02c16 100644
--- a/internal/pipe/aur/aur_test.go
+++ b/internal/pipe/aur/aur_test.go
@@ -150,11 +150,12 @@ func TestSrcInfoSimple(t *testing.T) {
 
 func TestFullPipe(t *testing.T) {
 	type testcase struct {
-		prepare                func(ctx *context.Context)
-		expectedRunError       string
-		expectedPublishError   string
-		expectedPublishErrorIs error
-		expectedErrorCheck     func(testing.TB, error)
+		prepare                   func(ctx *context.Context)
+		expectedRunError          string
+		expectedRunErrorCheck     func(testing.TB, error)
+		expectedPublishError      string
+		expectedPublishErrorIs    error
+		expectedPublishErrorCheck func(testing.TB, error)
 	}
 	for name, tt := range map[string]testcase{
 		"default": {
@@ -185,25 +186,25 @@ func TestFullPipe(t *testing.T) {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.AURs[0].Name = "{{ .Asdsa }"
 			},
-			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorCheck: testlib.RequireTemplateError,
 		},
 		"invalid-package-template": {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.AURs[0].Package = "{{ .Asdsa }"
 			},
-			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorCheck: testlib.RequireTemplateError,
 		},
 		"invalid-commit-template": {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.AURs[0].CommitMessageTemplate = "{{ .Asdsa }"
 			},
-			expectedErrorCheck: testlib.RequireTemplateError,
+			expectedPublishErrorCheck: testlib.RequireTemplateError,
 		},
 		"invalid-key-template": {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.AURs[0].PrivateKey = "{{ .Asdsa }"
 			},
-			expectedErrorCheck: testlib.RequireTemplateError,
+			expectedPublishErrorCheck: testlib.RequireTemplateError,
 		},
 		"no-key": {
 			prepare: func(ctx *context.Context) {
@@ -221,7 +222,7 @@ func TestFullPipe(t *testing.T) {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.AURs[0].GitURL = "{{ .Asdsa }"
 			},
-			expectedErrorCheck: testlib.RequireTemplateError,
+			expectedPublishErrorCheck: testlib.RequireTemplateError,
 		},
 		"no-git-url": {
 			prepare: func(ctx *context.Context) {
@@ -233,13 +234,13 @@ func TestFullPipe(t *testing.T) {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.AURs[0].GitSSHCommand = "{{ .Asdsa }"
 			},
-			expectedErrorCheck: testlib.RequireTemplateError,
+			expectedPublishErrorCheck: testlib.RequireTemplateError,
 		},
 		"invalid-commit-author-template": {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.AURs[0].CommitAuthor.Name = "{{ .Asdsa }"
 			},
-			expectedErrorCheck: testlib.RequireTemplateError,
+			expectedPublishErrorCheck: testlib.RequireTemplateError,
 		},
 	} {
 		t.Run(name, func(t *testing.T) {
@@ -321,6 +322,10 @@ func TestFullPipe(t *testing.T) {
 				require.EqualError(t, runAll(ctx, client), tt.expectedRunError)
 				return
 			}
+			if tt.expectedRunErrorCheck != nil {
+				tt.expectedRunErrorCheck(t, runAll(ctx, client))
+				return
+			}
 			require.NoError(t, runAll(ctx, client))
 
 			if tt.expectedPublishError != "" {
@@ -333,8 +338,8 @@ func TestFullPipe(t *testing.T) {
 				return
 			}
 
-			if tt.expectedErrorCheck != nil {
-				tt.expectedErrorCheck(t, Pipe{}.Publish(ctx))
+			if tt.expectedPublishErrorCheck != nil {
+				tt.expectedPublishErrorCheck(t, Pipe{}.Publish(ctx))
 				return
 			}
 
diff --git a/internal/pipe/before/before_test.go b/internal/pipe/before/before_test.go
index 54ef2f0e..4885386d 100644
--- a/internal/pipe/before/before_test.go
+++ b/internal/pipe/before/before_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/caarlos0/log"
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -83,13 +84,13 @@ func TestRunWithEnv(t *testing.T) {
 }
 
 func TestInvalidTemplate(t *testing.T) {
-	require.EqualError(t, Pipe{}.Run(testctx.NewWithCfg(
+	testlib.RequireTemplateError(t, Pipe{}.Run(testctx.NewWithCfg(
 		config.Project{
 			Before: config.Before{
 				Hooks: []string{"touch {{ .fasdsd }"},
 			},
 		},
-	)), `template: tmpl:1: unexpected "}" in operand`)
+	)))
 }
 
 func TestSkip(t *testing.T) {
diff --git a/internal/pipe/brew/brew_test.go b/internal/pipe/brew/brew_test.go
index e2b1ac7d..c1c18b62 100644
--- a/internal/pipe/brew/brew_test.go
+++ b/internal/pipe/brew/brew_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/golden"
 	"github.com/goreleaser/goreleaser/internal/testctx"
 	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -161,9 +162,11 @@ func TestSplit(t *testing.T) {
 
 func TestFullPipe(t *testing.T) {
 	type testcase struct {
-		prepare              func(ctx *context.Context)
-		expectedRunError     string
-		expectedPublishError string
+		prepare                func(ctx *context.Context)
+		expectedRunError       string
+		expectedRunErrorAs     any
+		expectedPublishError   string
+		expectedPublishErrorAs any
 	}
 	for name, tt := range map[string]testcase{
 		"default": {
@@ -247,7 +250,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Brews[0].Repository.Name = "test"
 				ctx.Config.Brews[0].CommitMessageTemplate = "{{ .Asdsa }"
 			},
-			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedPublishErrorAs: &tmpl.Error{},
 		},
 		"valid_repository_templates": {
 			prepare: func(ctx *context.Context) {
@@ -264,14 +267,14 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Brews[0].Repository.Owner = "test"
 				ctx.Config.Brews[0].Repository.Name = "{{ .Asdsa }"
 			},
-			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"invalid_repository_owner_template": {
 			prepare: func(ctx *context.Context) {
 				ctx.Config.Brews[0].Repository.Owner = "{{ .Asdsa }"
 				ctx.Config.Brews[0].Repository.Name = "test"
 			},
-			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"invalid_repository_skip_upload_template": {
 			prepare: func(ctx *context.Context) {
@@ -279,7 +282,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Brews[0].Repository.Owner = "test"
 				ctx.Config.Brews[0].Repository.Name = "test"
 			},
-			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"invalid_install_template": {
 			prepare: func(ctx *context.Context) {
@@ -287,7 +290,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Brews[0].Repository.Name = "test"
 				ctx.Config.Brews[0].Install = "{{ .aaaa }"
 			},
-			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 	} {
 		t.Run(name, func(t *testing.T) {
@@ -381,18 +384,27 @@ func TestFullPipe(t *testing.T) {
 
 			require.NoError(t, Pipe{}.Default(ctx))
 
-			if tt.expectedRunError == "" {
-				require.NoError(t, runAll(ctx, client))
-			} else {
-				require.EqualError(t, runAll(ctx, client), tt.expectedRunError)
+			err = runAll(ctx, client)
+			if tt.expectedRunError != "" {
+				require.EqualError(t, err, tt.expectedRunError)
 				return
 			}
-			if tt.expectedPublishError != "" {
-				require.EqualError(t, publishAll(ctx, client), tt.expectedPublishError)
+			if tt.expectedRunErrorAs != nil {
+				require.ErrorAs(t, err, tt.expectedRunErrorAs)
 				return
 			}
+			require.NoError(t, err)
 
-			require.NoError(t, publishAll(ctx, client))
+			err = publishAll(ctx, client)
+			if tt.expectedPublishError != "" {
+				require.EqualError(t, err, tt.expectedPublishError)
+				return
+			}
+			if tt.expectedPublishErrorAs != nil {
+				require.ErrorAs(t, err, tt.expectedPublishErrorAs)
+				return
+			}
+			require.NoError(t, err)
 
 			content := []byte(client.Content)
 			if url := ctx.Config.Brews[0].Repository.Git.URL; url == "" {
diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go
index 1a0a7c54..8c24f5ef 100644
--- a/internal/pipe/build/build_test.go
+++ b/internal/pipe/build/build_test.go
@@ -562,7 +562,7 @@ func TestPipeOnBuild_invalidBinaryTpl(t *testing.T) {
 	})
 	g := semerrgroup.New(ctx.Parallelism)
 	runPipeOnBuild(ctx, g, build)
-	require.EqualError(t, g.Wait(), `template: tmpl:1:11: executing "tmpl" at <.XYZ>: map has no entry for key "XYZ"`)
+	testlib.RequireTemplateError(t, g.Wait())
 }
 
 func TestBuildOptionsForTarget(t *testing.T) {
diff --git a/internal/pipe/checksums/checksums_test.go b/internal/pipe/checksums/checksums_test.go
index 759ce9b7..0e65cdbd 100644
--- a/internal/pipe/checksums/checksums_test.go
+++ b/internal/pipe/checksums/checksums_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -159,9 +160,9 @@ func TestPipeInvalidNameTemplate(t *testing.T) {
 	_, err = binFile.WriteString("fake artifact")
 	require.NoError(t, err)
 
-	for template, eerr := range map[string]string{
-		"{{ .Pro }_checksums.txt": `template: tmpl:1: unexpected "}" in operand`,
-		"{{.Env.NOPE}}":           `template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`,
+	for _, template := range []string{
+		"{{ .Pro }_checksums.txt",
+		"{{.Env.NOPE}}",
 	} {
 		t.Run(template, func(t *testing.T) {
 			folder := t.TempDir()
@@ -181,9 +182,7 @@ func TestPipeInvalidNameTemplate(t *testing.T) {
 				Type: artifact.UploadableBinary,
 				Path: binFile.Name(),
 			})
-			err = Pipe{}.Run(ctx)
-			require.Error(t, err)
-			require.Equal(t, eerr, err.Error())
+			testlib.RequireTemplateError(t, Pipe{}.Run(ctx))
 		})
 	}
 }
diff --git a/internal/pipe/discord/discord_test.go b/internal/pipe/discord/discord_test.go
index f901be46..18de1314 100644
--- a/internal/pipe/discord/discord_test.go
+++ b/internal/pipe/discord/discord_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -26,7 +27,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `discord: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/env/env_test.go b/internal/pipe/env/env_test.go
index fccf89c6..0c6a4c6d 100644
--- a/internal/pipe/env/env_test.go
+++ b/internal/pipe/env/env_test.go
@@ -78,7 +78,7 @@ func TestSetDefaultTokenFiles(t *testing.T) {
 				"FOO={{ .Asss }",
 			},
 		})
-		require.EqualError(t, Pipe{}.Run(ctx), `template: tmpl:1: unexpected "}" in operand`)
+		testlib.RequireTemplateError(t, Pipe{}.Run(ctx))
 	})
 
 	t.Run("no token", func(t *testing.T) {
diff --git a/internal/pipe/git/git_test.go b/internal/pipe/git/git_test.go
index 4516f3c5..15c8cdad 100644
--- a/internal/pipe/git/git_test.go
+++ b/internal/pipe/git/git_test.go
@@ -358,3 +358,73 @@ func TestPreviousTagFromCI(t *testing.T) {
 		})
 	}
 }
+
+func TestFilterTags(t *testing.T) {
+	testlib.Mktmp(t)
+	testlib.GitInit(t)
+	testlib.GitRemoteAdd(t, "git@github.com:foo/bar.git")
+	testlib.GitCommit(t, "commit1")
+	testlib.GitTag(t, "v0.0.1")
+	testlib.GitTag(t, "nightly")
+	testlib.GitCommit(t, "commit2")
+	testlib.GitTag(t, "v0.0.2")
+	testlib.GitTag(t, "v0.1.0-dev")
+
+	t.Run("no filter", func(t *testing.T) {
+		ctx := testctx.New()
+		require.NoError(t, Pipe{}.Run(ctx))
+		require.Equal(t, "v0.0.1", ctx.Git.PreviousTag)
+		require.Equal(t, "v0.1.0-dev", ctx.Git.CurrentTag)
+	})
+
+	t.Run("regex", func(t *testing.T) {
+		ctx := testctx.NewWithCfg(config.Project{
+			Git: config.Git{
+				IgnoreTags: []string{
+					".*-dev",
+				},
+			},
+		})
+		require.NoError(t, Pipe{}.Run(ctx))
+		require.Equal(t, "v0.0.1", ctx.Git.PreviousTag)
+		require.Equal(t, "v0.0.2", ctx.Git.CurrentTag)
+	})
+
+	t.Run("template", func(t *testing.T) {
+		ctx := testctx.NewWithCfg(config.Project{
+			Git: config.Git{
+				IgnoreTags: []string{
+					"{{.Env.PREFIX}}.*",
+					"nightly",
+				},
+			},
+		}, testctx.WithEnv(map[string]string{
+			"PREFIX": `v0\.0\.`,
+		}))
+		require.NoError(t, Pipe{}.Run(ctx))
+		require.Empty(t, ctx.Git.PreviousTag)
+		require.Equal(t, "v0.1.0-dev", ctx.Git.CurrentTag)
+	})
+
+	t.Run("invalid regex", func(t *testing.T) {
+		ctx := testctx.NewWithCfg(config.Project{
+			Git: config.Git{
+				IgnoreTags: []string{
+					"[",
+				},
+			},
+		})
+		require.EqualError(t, Pipe{}.Run(ctx), "could not get current tag: error parsing regexp: missing closing ]: `[`")
+	})
+
+	t.Run("invalid template", func(t *testing.T) {
+		ctx := testctx.NewWithCfg(config.Project{
+			Git: config.Git{
+				IgnoreTags: []string{
+					"{{.Env.Nope}}",
+				},
+			},
+		})
+		testlib.RequireTemplateError(t, Pipe{}.Run(ctx))
+	})
+}
diff --git a/internal/pipe/krew/krew_test.go b/internal/pipe/krew/krew_test.go
index a4d88a93..91110efa 100644
--- a/internal/pipe/krew/krew_test.go
+++ b/internal/pipe/krew/krew_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/golden"
 	"github.com/goreleaser/goreleaser/internal/testctx"
 	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -134,9 +135,11 @@ func TestSimple(t *testing.T) {
 
 func TestFullPipe(t *testing.T) {
 	type testcase struct {
-		prepare              func(ctx *context.Context)
-		expectedRunError     string
-		expectedPublishError string
+		prepare                func(ctx *context.Context)
+		expectedRunError       string
+		expectedRunErrorAs     any
+		expectedPublishError   string
+		expectedPublishErrorAs any
 	}
 	for name, tt := range map[string]testcase{
 		"default": {
@@ -175,7 +178,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Krews[0].Repository.Name = "test"
 				ctx.Config.Krews[0].CommitMessageTemplate = "{{ .Asdsa }"
 			},
-			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+			expectedPublishErrorAs: &tmpl.Error{},
 		},
 		"invalid desc": {
 			prepare: func(ctx *context.Context) {
@@ -183,7 +186,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Krews[0].Repository.Name = "test"
 				ctx.Config.Krews[0].Description = "{{ .Asdsa }"
 			},
-			expectedRunError: `failed to apply template: {{ .Asdsa }: template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"invalid short desc": {
 			prepare: func(ctx *context.Context) {
@@ -191,7 +194,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Krews[0].Repository.Name = "test"
 				ctx.Config.Krews[0].ShortDescription = "{{ .Asdsa }"
 			},
-			expectedRunError: `failed to apply template: {{ .Asdsa }: template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"invalid homepage": {
 			prepare: func(ctx *context.Context) {
@@ -199,7 +202,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Krews[0].Repository.Name = "test"
 				ctx.Config.Krews[0].Homepage = "{{ .Asdsa }"
 			},
-			expectedRunError: `failed to apply template: {{ .Asdsa }: template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"invalid name": {
 			prepare: func(ctx *context.Context) {
@@ -207,7 +210,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Krews[0].Repository.Name = "test"
 				ctx.Config.Krews[0].Name = "{{ .Asdsa }"
 			},
-			expectedRunError: `failed to apply template: {{ .Asdsa }: template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"invalid caveats": {
 			prepare: func(ctx *context.Context) {
@@ -215,7 +218,7 @@ func TestFullPipe(t *testing.T) {
 				ctx.Config.Krews[0].Repository.Name = "test"
 				ctx.Config.Krews[0].Caveats = "{{ .Asdsa }"
 			},
-			expectedRunError: `failed to apply template: {{ .Asdsa }: template: tmpl:1: unexpected "}" in operand`,
+			expectedRunErrorAs: &tmpl.Error{},
 		},
 		"no short desc": {
 			prepare: func(ctx *context.Context) {
@@ -304,11 +307,16 @@ func TestFullPipe(t *testing.T) {
 			distFile := filepath.Join(folder, "krew", name+".yaml")
 
 			require.NoError(t, Pipe{}.Default(ctx))
+
 			err = runAll(ctx, client)
 			if tt.expectedRunError != "" {
 				require.EqualError(t, err, tt.expectedRunError)
 				return
 			}
+			if tt.expectedRunErrorAs != nil {
+				require.ErrorAs(t, err, tt.expectedRunErrorAs)
+				return
+			}
 			require.NoError(t, err)
 
 			err = publishAll(ctx, client)
@@ -316,7 +324,10 @@ func TestFullPipe(t *testing.T) {
 				require.EqualError(t, err, tt.expectedPublishError)
 				return
 			}
-
+			if tt.expectedPublishErrorAs != nil {
+				require.ErrorAs(t, err, tt.expectedPublishErrorAs)
+				return
+			}
 			require.NoError(t, err)
 
 			content := []byte(client.Content)
diff --git a/internal/pipe/linkedin/linkedin_test.go b/internal/pipe/linkedin/linkedin_test.go
index 2c47b98e..f782bca5 100644
--- a/internal/pipe/linkedin/linkedin_test.go
+++ b/internal/pipe/linkedin/linkedin_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -33,7 +34,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `linkedin: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/mastodon/mastodon_test.go b/internal/pipe/mastodon/mastodon_test.go
index 05342799..91ce3bce 100644
--- a/internal/pipe/mastodon/mastodon_test.go
+++ b/internal/pipe/mastodon/mastodon_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -26,7 +27,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `mastodon: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/mattermost/mattermost_test.go b/internal/pipe/mattermost/mattermost_test.go
index 535badf1..7d69956e 100644
--- a/internal/pipe/mattermost/mattermost_test.go
+++ b/internal/pipe/mattermost/mattermost_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 )
 
@@ -31,7 +32,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `mattermost: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/opencollective/opencollective_test.go b/internal/pipe/opencollective/opencollective_test.go
index bc46dec4..edbd51e9 100644
--- a/internal/pipe/opencollective/opencollective_test.go
+++ b/internal/pipe/opencollective/opencollective_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -27,7 +28,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `opencollective: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/prebuild/prebuild_test.go b/internal/pipe/prebuild/prebuild_test.go
index b485f053..d28fed93 100644
--- a/internal/pipe/prebuild/prebuild_test.go
+++ b/internal/pipe/prebuild/prebuild_test.go
@@ -3,6 +3,7 @@ package prebuild
 import (
 	"testing"
 
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -31,7 +32,7 @@ func TestRun(t *testing.T) {
 		ctx := context.New(config.Project{
 			Builds: []config.Build{{Main: "{{ .Env.FOO }}"}},
 		})
-		require.EqualError(t, Pipe{}.Run(ctx), `template: tmpl:1:7: executing "tmpl" at <.Env.FOO>: map has no entry for key "FOO"`)
+		testlib.RequireTemplateError(t, Pipe{}.Run(ctx))
 	})
 }
 
diff --git a/internal/pipe/reddit/reddit_test.go b/internal/pipe/reddit/reddit_test.go
index d7633012..c123071d 100644
--- a/internal/pipe/reddit/reddit_test.go
+++ b/internal/pipe/reddit/reddit_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -26,7 +27,7 @@ func TestAnnounceInvalidURLTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `reddit: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceInvalidTitleTemplate(t *testing.T) {
@@ -37,7 +38,7 @@ func TestAnnounceInvalidTitleTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `reddit: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/release/body_test.go b/internal/pipe/release/body_test.go
index e1bb3766..d6f8ca39 100644
--- a/internal/pipe/release/body_test.go
+++ b/internal/pipe/release/body_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/golden"
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -85,7 +86,7 @@ func TestDescribeBodyWithInvalidHeaderTemplate(t *testing.T) {
 		},
 	})
 	_, err := describeBody(ctx)
-	require.EqualError(t, err, `template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, err)
 }
 
 func TestDescribeBodyWithInvalidFooterTemplate(t *testing.T) {
@@ -95,5 +96,5 @@ func TestDescribeBodyWithInvalidFooterTemplate(t *testing.T) {
 		},
 	})
 	_, err := describeBody(ctx)
-	require.EqualError(t, err, `template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, err)
 }
diff --git a/internal/pipe/sbom/sbom_test.go b/internal/pipe/sbom/sbom_test.go
index 607c7849..e33dbebd 100644
--- a/internal/pipe/sbom/sbom_test.go
+++ b/internal/pipe/sbom/sbom_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/artifact"
 	"github.com/goreleaser/goreleaser/internal/testctx"
 	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/assert"
@@ -221,6 +222,7 @@ func TestSBOMCatalogArtifacts(t *testing.T) {
 		ctx            *context.Context
 		sbomPaths      []string
 		sbomNames      []string
+		expectedErrAs  any
 		expectedErrMsg string
 	}{
 		{
@@ -237,8 +239,8 @@ func TestSBOMCatalogArtifacts(t *testing.T) {
 			}),
 		},
 		{
-			desc:           "invalid args template",
-			expectedErrMsg: `cataloging artifacts failed: arg "${FOO}-{{ .foo }{{}}{": invalid template: template: tmpl:1: unexpected "}" in operand`,
+			desc:          "invalid args template",
+			expectedErrAs: &tmpl.Error{},
 			ctx: testctx.NewWithCfg(config.Project{
 				SBOMs: []config.SBOM{
 					{
@@ -415,12 +417,25 @@ func TestSBOMCatalogArtifacts(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.desc, func(t *testing.T) {
-			testSBOMCataloging(t, test.ctx, test.sbomPaths, test.sbomNames, test.expectedErrMsg)
+			testSBOMCataloging(
+				t,
+				test.ctx,
+				test.sbomPaths,
+				test.sbomNames,
+				test.expectedErrAs,
+				test.expectedErrMsg,
+			)
 		})
 	}
 }
 
-func testSBOMCataloging(tb testing.TB, ctx *context.Context, sbomPaths, sbomNames []string, expectedErrMsg string) {
+func testSBOMCataloging(
+	tb testing.TB,
+	ctx *context.Context,
+	sbomPaths, sbomNames []string,
+	expectedErrAs any,
+	expectedErrMsg string,
+) {
 	tb.Helper()
 	testlib.CheckPath(tb, "syft")
 	tmpdir := tb.TempDir()
@@ -501,6 +516,10 @@ func testSBOMCataloging(tb testing.TB, ctx *context.Context, sbomPaths, sbomName
 		require.Contains(tb, err.Error(), expectedErrMsg)
 		return
 	}
+	if expectedErrAs != nil {
+		require.ErrorAs(tb, Pipe{}.Run(ctx), expectedErrAs)
+		return
+	}
 
 	require.NoError(tb, Pipe{}.Run(ctx))
 
diff --git a/internal/pipe/sign/sign_test.go b/internal/pipe/sign/sign_test.go
index ef4c3027..b9f40a7a 100644
--- a/internal/pipe/sign/sign_test.go
+++ b/internal/pipe/sign/sign_test.go
@@ -18,6 +18,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/git"
 	"github.com/goreleaser/goreleaser/internal/testctx"
 	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
@@ -104,6 +105,7 @@ func TestSignArtifacts(t *testing.T) {
 		certificateNames []string
 		expectedErrMsg   string
 		expectedErrIs    error
+		expectedErrAs    any
 		user             string
 	}{
 		{
@@ -132,8 +134,8 @@ func TestSignArtifacts(t *testing.T) {
 			}),
 		},
 		{
-			desc:           "invalid certificate template",
-			expectedErrMsg: `sign failed: artifact1: template: tmpl:1:3: executing "tmpl" at <.blah>: map has no entry for key "blah"`,
+			desc:          "invalid certificate template",
+			expectedErrAs: &tmpl.Error{},
 			ctx: testctx.NewWithCfg(config.Project{
 				Signs: []config.Sign{
 					{
@@ -145,8 +147,8 @@ func TestSignArtifacts(t *testing.T) {
 			}),
 		},
 		{
-			desc:           "invalid signature template",
-			expectedErrMsg: `sign failed: artifact1: template: tmpl:1:3: executing "tmpl" at <.blah>: map has no entry for key "blah"`,
+			desc:          "invalid signature template",
+			expectedErrAs: &tmpl.Error{},
 			ctx: testctx.NewWithCfg(config.Project{
 				Signs: []config.Sign{
 					{
@@ -158,8 +160,8 @@ func TestSignArtifacts(t *testing.T) {
 			}),
 		},
 		{
-			desc:           "invalid args template",
-			expectedErrMsg: `sign failed: artifact1: template: tmpl:1: unexpected "}" in operand`,
+			desc:          "invalid args template",
+			expectedErrAs: &tmpl.Error{},
 			ctx: testctx.NewWithCfg(config.Project{
 				Signs: []config.Sign{
 					{
@@ -174,8 +176,8 @@ func TestSignArtifacts(t *testing.T) {
 			}),
 		},
 		{
-			desc:           "invalid env template",
-			expectedErrMsg: `sign failed: artifact1: template: tmpl:1:5: executing "tmpl" at <.blah>: map has no entry for key "blah"`,
+			desc:          "invalid env template",
+			expectedErrAs: &tmpl.Error{},
 			ctx: testctx.NewWithCfg(config.Project{
 				Signs: []config.Sign{
 					{
@@ -524,12 +526,29 @@ func TestSignArtifacts(t *testing.T) {
 		}
 
 		t.Run(test.desc, func(t *testing.T) {
-			testSign(t, test.ctx, test.certificateNames, test.signaturePaths, test.signatureNames, test.user, test.expectedErrMsg, test.expectedErrIs)
+			testSign(
+				t,
+				test.ctx,
+				test.certificateNames,
+				test.signaturePaths,
+				test.signatureNames,
+				test.user,
+				test.expectedErrMsg,
+				test.expectedErrIs,
+				test.expectedErrAs,
+			)
 		})
 	}
 }
 
-func testSign(tb testing.TB, ctx *context.Context, certificateNames, signaturePaths, signatureNames []string, user, expectedErrMsg string, expectedErrIs error) {
+func testSign(
+	tb testing.TB,
+	ctx *context.Context,
+	certificateNames, signaturePaths, signatureNames []string,
+	user, expectedErrMsg string,
+	expectedErrIs error,
+	expectedErrAs any,
+) {
 	tb.Helper()
 	tmpdir := tb.TempDir()
 
@@ -639,6 +658,11 @@ func testSign(tb testing.TB, ctx *context.Context, certificateNames, signaturePa
 		return
 	}
 
+	if expectedErrAs != nil {
+		require.ErrorAs(tb, err, expectedErrAs)
+		return
+	}
+
 	require.NoError(tb, err)
 
 	// ensure all artifacts have an ID
diff --git a/internal/pipe/slack/slack_test.go b/internal/pipe/slack/slack_test.go
index 2502c641..bbeee3fb 100644
--- a/internal/pipe/slack/slack_test.go
+++ b/internal/pipe/slack/slack_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/internal/yaml"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/slack-go/slack"
@@ -29,7 +30,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `slack: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/teams/teams_test.go b/internal/pipe/teams/teams_test.go
index 14198136..129075eb 100644
--- a/internal/pipe/teams/teams_test.go
+++ b/internal/pipe/teams/teams_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -27,7 +28,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `teams: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/twitter/twitter_test.go b/internal/pipe/twitter/twitter_test.go
index 412a8384..1973064d 100644
--- a/internal/pipe/twitter/twitter_test.go
+++ b/internal/pipe/twitter/twitter_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -26,7 +27,7 @@ func TestAnnounceInvalidTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `twitter: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 func TestAnnounceMissingEnv(t *testing.T) {
diff --git a/internal/pipe/webhook/webhook_test.go b/internal/pipe/webhook/webhook_test.go
index 4f92078f..e9f47697 100644
--- a/internal/pipe/webhook/webhook_test.go
+++ b/internal/pipe/webhook/webhook_test.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/google/uuid"
 	"github.com/goreleaser/goreleaser/internal/testctx"
+	"github.com/goreleaser/goreleaser/internal/testlib"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/stretchr/testify/require"
 )
@@ -48,7 +49,7 @@ func TestAnnounceInvalidMessageTemplate(t *testing.T) {
 			},
 		},
 	})
-	require.EqualError(t, Pipe{}.Announce(ctx), `webhook: template: tmpl:1: unexpected "}" in operand`)
+	testlib.RequireTemplateError(t, Pipe{}.Announce(ctx))
 }
 
 type WebHookServerMockMessage struct {
diff --git a/internal/testlib/tmpl.go b/internal/testlib/tmpl.go
index 5d64fca8..f00f7f61 100644
--- a/internal/testlib/tmpl.go
+++ b/internal/testlib/tmpl.go
@@ -3,6 +3,7 @@ package testlib
 import (
 	"testing"
 
+	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/stretchr/testify/require"
 )
 
@@ -11,6 +12,5 @@ func RequireTemplateError(tb testing.TB, err error) {
 	tb.Helper()
 
 	require.Error(tb, err)
-	require.Contains(tb, err.Error(), "template:")
-	require.Regexp(tb, "bad character|map has no entry|unexpected \"}\" in operand", err.Error())
+	require.ErrorAs(tb, err, &tmpl.Error{})
 }
diff --git a/internal/tmpl/tmpl_test.go b/internal/tmpl/tmpl_test.go
index 4248962f..6f9bf2f4 100644
--- a/internal/tmpl/tmpl_test.go
+++ b/internal/tmpl/tmpl_test.go
@@ -138,7 +138,8 @@ func TestWithArtifact(t *testing.T) {
 	t.Run("template using artifact Fields with no artifact", func(t *testing.T) {
 		t.Parallel()
 		result, err := New(ctx).Apply("{{ .Os }}")
-		require.EqualError(t, err, `template: tmpl:1:3: executing "tmpl" at <.Os>: map has no entry for key "Os"`)
+		require.ErrorAs(t, err, &Error{})
+		require.EqualError(t, err, `template: failed to apply "{{ .Os }}": map has no entry for key "Os"`)
 		require.Empty(t, result)
 	})
 }
@@ -357,16 +358,18 @@ func TestApplySingleEnvOnly(t *testing.T) {
 }
 
 func TestInvalidTemplate(t *testing.T) {
-	ctx := testctx.New(testctx.WithCurrentTag("v1.1.1"))
+	ctx := testctx.New()
 	_, err := New(ctx).Apply("{{{.Foo}")
-	require.EqualError(t, err, "template: tmpl:1: unexpected \"{\" in command")
+	require.ErrorAs(t, err, &Error{})
+	require.EqualError(t, err, `template: failed to apply "{{{.Foo}": unexpected "{" in command`)
 }
 
 func TestEnvNotFound(t *testing.T) {
 	ctx := testctx.New(testctx.WithCurrentTag("v1.2.4"))
 	result, err := New(ctx).Apply("{{.Env.FOO}}")
 	require.Empty(t, result)
-	require.EqualError(t, err, `template: tmpl:1:6: executing "tmpl" at <.Env.FOO>: map has no entry for key "FOO"`)
+	require.ErrorAs(t, err, &Error{})
+	require.EqualError(t, err, `template: failed to apply "{{.Env.FOO}}": map has no entry for key "FOO"`)
 }
 
 func TestWithExtraFields(t *testing.T) {