Repository: goreleaser/goreleaser
Pull request #2838 (2022-01-19 14:51:04)

feat: arch user repository integration
### TODO

- [x] generate the pkgbuild file based on config/artifacts
- [x] clone, update and commit to a git repo using a configurable ssh key
- [x] docs
- [x] make `package` templateable
- [ ] publish goreleaser pkgbuild itself (likely on next release)

### Questions

- it seems that AUR defaults to master, although I couldn't find any references for that... is that assumption correct?
- the auth method is a bit cumbersome, is there a better way?
- are the docs clear enough?
- not sure about the `aurs` field in the config file, but seemed the most appropriate 
- should we also upload PKGBUILDs et al to the release?

---
**This feature is being sponsored by [Charm](https://charm.sh) ✨**

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2838?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2838](https://codecov.io/gh/goreleaser/goreleaser/pull/2838?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (caa553d) into [main](https://codecov.io/gh/goreleaser/goreleaser/commit/2c6b51cc652ccf1831d173841f119b551909c06b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (2c6b51c) will **decrease** coverage by `0.22%`.
> The diff coverage is `78.54%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2838/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/2838?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##             main    #2838      +/-   ##
==========================================
- Coverage   84.55%   84.33%   -0.23%     
==========================================
  Files         110      111       +1     
  Lines        8400     8731     +331     
==========================================
+ Hits         7103     7363     +260     
- Misses       1051     1103      +52     
- Partials      246      265      +19     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2838?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2838/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `87.15% <ø> (ø)` | |
| [internal/pipe/aur/aur.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2838/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9hdXIvYXVyLmdv) | `78.41% <78.41%> (ø)` | |
| [internal/artifact/artifact.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2838/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvYXJ0aWZhY3QvYXJ0aWZhY3QuZ28=) | `97.73% <100.00%> (+0.02%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2838?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/2838?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [2c6b51c...caa553d](https://codecov.io/gh/goreleaser/goreleaser/pull/2838?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).

Foxboron: Yo, I'm one of the AUR moderators and general Go packager in Arch Linux. I looked over the code and it seems fine. I tried answering the questions at the top of the PR as well. Feel free to ask if you wonder about anything else.

>it seems that AUR defaults to master, although I couldn't find any references for that... is that assumption correct?

Yes, and this won't change as the underlying AUR repository is using git namespaces where the branches are hardcoded to `master`. The docs should probably be updated to reference that.

>the auth method is a bit cumbersome, is there a better way?

There isn't. SSH keys is the only way and I'm not aware of any changes being introduced in the Python rewrite.

> are the docs clear enough?

I think there needs to be a link to the [AUR submission guidelines](https://wiki.archlinux.org/title/AUR_submission_guidelines#Rules_of_submission). Rest of the links are a good guideline.
caarlos0: Hey @Foxboron, many thanks for the review! I think I addressed all your points, feel free to take another look if you can 🙏

I also made a few more improvements:

- enforce -bin suffix on pkgname
- maintainers can be a list
- more tests 🙏
Foxboron: It looks fine on my end. I like the fact you enforce `-bin` on `pkgname`.  The only thing that might be missing is `validpgpkeys`, but I'm not sure how often people sign their releaser artfifacts with `goreleaser`. Rest looks good to me :)

Patch:
diff --git a/.editorconfig b/.editorconfig
index 223d2991..cedad1af 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
 insert_final_newline = true
 charset = utf-8
 
-[*.{md,yml}]
+[*.{md,yml,yaml}]
 indent_size = 2
 indent_style = space
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 2383b8a6..2f3e0ada 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -161,6 +161,33 @@ rigs:
     description: Deliver Go binaries as fast and easily as possible
     license: MIT
 
+aurs:
+  - homepage:  https://goreleaser.com
+    description: Deliver Go binaries as fast and easily as possible
+    maintainers:
+      - 'Fernandez Ludovic <lfernandez dot dev at gmail dot com>'
+      - 'Carlos Alexandro Becker <carlos at becker dot software>'
+    license: MIT
+    private_key: '{{ .Env.AUR_KEY }}'
+    git_url: 'ssh://aur@aur.archlinux.org/goreleaser-bin.git'
+    package: |-
+      # bin
+      install -Dm755 "./goreleaser" "${pkgdir}/usr/bin/goreleaser"
+
+      # license
+      install -Dm644 "./LICENSE.md" "${pkgdir}/usr/share/licenses/goreleaser/LICENSE"
+
+      # completions
+      mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
+      mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
+      mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
+      install -Dm644 "./completions/goreleaser.bash" "${pkgdir}/usr/share/bash-completion/completions/goreleaser"
+      install -Dm644 "./completions/goreleaser.zsh" "${pkgdir}/usr/share/zsh/site-functions/_goreleaser"
+      install -Dm644 "./completions/goreleaser.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/goreleaser.fish"
+
+      # man pages
+      install -Dm644 "./manpages/goreleaser.1.gz" "${pkgdir}/usr/share/man/man1/goreleaser.1.gz"
+
 scoop:
   bucket:
     owner: goreleaser
diff --git a/go.mod b/go.mod
index 1bcad909..eb45260d 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/caarlos0/env/v6 v6.9.1
 	github.com/caarlos0/go-reddit/v3 v3.0.1
 	github.com/caarlos0/go-shellwords v1.0.12
+	github.com/charmbracelet/keygen v0.1.2
 	github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb
 	github.com/dghubble/oauth1 v0.7.1
 	github.com/fatih/color v1.13.0
@@ -32,6 +33,7 @@ require (
 	github.com/ulikunitz/xz v0.5.10
 	github.com/xanzy/go-gitlab v0.52.2
 	gocloud.dev v0.24.0
+	golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	gopkg.in/mail.v2 v2.3.1
@@ -118,6 +120,7 @@ require (
 	github.com/mattn/go-colorable v0.1.12 // indirect
 	github.com/mattn/go-ieproxy v0.0.1 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
@@ -128,7 +131,6 @@ require (
 	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
 	github.com/xanzy/ssh-agent v0.3.1 // indirect
 	go.opencensus.io v0.23.0 // indirect
-	golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
 	golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
 	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
 	golang.org/x/text v0.3.7 // indirect
diff --git a/go.sum b/go.sum
index 40558638..74908bae 100644
--- a/go.sum
+++ b/go.sum
@@ -268,6 +268,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/charmbracelet/keygen v0.1.2 h1:Gr/gdIOjDIxCTRVXpwa9tsXPoJPS2eGNehPoMnZLvTQ=
+github.com/charmbracelet/keygen v0.1.2/go.mod h1:kFQ3Cvop12fXWX1K29vxDxV9x8ujG4wBSXq//GySSSk=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -639,6 +641,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
+github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index dea42482..8b8ce9a1 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -57,6 +57,10 @@ const (
 	BrewTap
 	// GoFishRig is an uploadable Rigs rig food file.
 	GoFishRig
+	// PkgBuild is an Arch Linux AUR PKGBUILD file.
+	PkgBuild
+	// SrcInfo is an Arch Linux AUR .SRCINFO file.
+	SrcInfo
 	// KrewPluginManifest is a krew plugin manifest file.
 	KrewPluginManifest
 	// ScoopManifest is an uploadable scoop manifest file.
@@ -99,6 +103,10 @@ func (t Type) String() string {
 		return "Scoop Manifest"
 	case SBOM:
 		return "SBOM"
+	case PkgBuild:
+		return "PKGBUILD"
+	case SrcInfo:
+		return "SRCINFO"
 	default:
 		return "unknown"
 	}
@@ -244,6 +252,15 @@ func (artifacts Artifacts) List() []*Artifact {
 	return artifacts.items
 }
 
+// GroupByID groups the artifacts by their ID.
+func (artifacts Artifacts) GroupByID() map[string][]*Artifact {
+	result := map[string][]*Artifact{}
+	for _, a := range artifacts.items {
+		result[a.ID()] = append(result[a.ID()], a)
+	}
+	return result
+}
+
 // GroupByPlatform groups the artifacts by their platform.
 func (artifacts Artifacts) GroupByPlatform() map[string][]*Artifact {
 	result := map[string][]*Artifact{}
diff --git a/internal/git/git.go b/internal/git/git.go
index ff228110..1fa0d8eb 100644
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -16,8 +16,7 @@ func IsRepo() bool {
 	return err == nil && strings.TrimSpace(out) == "true"
 }
 
-// Run runs a git command and returns its output or errors.
-func Run(args ...string) (string, error) {
+func RunWithEnv(env []string, args ...string) (string, error) {
 	// TODO: use exex.CommandContext here and refactor.
 	extraArgs := []string{
 		"-c", "log.showSignature=false",
@@ -31,6 +30,7 @@ func Run(args ...string) (string, error) {
 
 	cmd.Stdout = &stdout
 	cmd.Stderr = &stderr
+	cmd.Env = append(cmd.Env, env...)
 
 	log.WithField("args", args).Debug("running git")
 	err := cmd.Run()
@@ -46,6 +46,11 @@ func Run(args ...string) (string, error) {
 	return stdout.String(), nil
 }
 
+// Run runs a git command and returns its output or errors.
+func Run(args ...string) (string, error) {
+	return RunWithEnv([]string{}, args...)
+}
+
 // Clean the output.
 func Clean(output string, err error) (string, error) {
 	output = strings.ReplaceAll(strings.Split(output, "\n")[0], "'", "")
diff --git a/internal/golden/golden.go b/internal/golden/golden.go
index 87a777bc..f5cb75af 100644
--- a/internal/golden/golden.go
+++ b/internal/golden/golden.go
@@ -16,7 +16,12 @@ const golden = ".golden"
 
 func RequireEqual(tb testing.TB, out []byte) {
 	tb.Helper()
-	doRequireEqual(tb, out, "", golden)
+	RequireEqualExt(tb, out, "")
+}
+
+func RequireEqualExt(tb testing.TB, out []byte, ext string) {
+	tb.Helper()
+	doRequireEqual(tb, out, ext, golden)
 }
 
 func RequireEqualTxt(tb testing.TB, out []byte) {
diff --git a/internal/pipe/aur/aur.go b/internal/pipe/aur/aur.go
new file mode 100644
index 00000000..f97e0fe7
--- /dev/null
+++ b/internal/pipe/aur/aur.go
@@ -0,0 +1,476 @@
+package aur
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"text/template"
+
+	"github.com/apex/log"
+	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/client"
+	"github.com/goreleaser/goreleaser/internal/commitauthor"
+	"github.com/goreleaser/goreleaser/internal/git"
+	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"golang.org/x/crypto/ssh"
+)
+
+const (
+	pkgBuildExtra     = "AURConfig"
+	defaultSSHCommand = "ssh -i {{ .KeyPath }} -F /dev/null"
+	defaultCommitMsg  = "Update to {{ .Tag }}"
+)
+
+var ErrNoArchivesFound = errors.New("no linux archives found")
+
+// Pipe for arch linux's AUR pkgbuild.
+type Pipe struct{}
+
+func (Pipe) String() string                 { return "arch user repositories" }
+func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.AURs) == 0 }
+
+func (Pipe) Default(ctx *context.Context) error {
+	for i := range ctx.Config.AURs {
+		pkg := &ctx.Config.AURs[i]
+
+		pkg.CommitAuthor = commitauthor.Default(pkg.CommitAuthor)
+		if pkg.CommitMessageTemplate == "" {
+			pkg.CommitMessageTemplate = defaultCommitMsg
+		}
+		if pkg.Name == "" {
+			pkg.Name = ctx.Config.ProjectName
+		}
+		if !strings.HasSuffix(pkg.Name, "-bin") {
+			pkg.Name += "-bin"
+		}
+		if len(pkg.Conflicts) == 0 {
+			pkg.Conflicts = []string{ctx.Config.ProjectName}
+		}
+		if len(pkg.Provides) == 0 {
+			pkg.Provides = []string{ctx.Config.ProjectName}
+		}
+		if pkg.Rel == "" {
+			pkg.Rel = "1"
+		}
+		if pkg.GitSSHCommand == "" {
+			pkg.GitSSHCommand = defaultSSHCommand
+		}
+	}
+
+	return nil
+}
+
+func (Pipe) Run(ctx *context.Context) error {
+	cli, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+
+	return runAll(ctx, cli)
+}
+
+func runAll(ctx *context.Context, cli client.Client) error {
+	for _, pkgbuild := range ctx.Config.AURs {
+		err := doRun(ctx, pkgbuild, cli)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func doRun(ctx *context.Context, pkgbuild config.AUR, cl client.Client) error {
+	name, err := tmpl.New(ctx).Apply(pkgbuild.Name)
+	if err != nil {
+		return err
+	}
+	pkgbuild.Name = name
+
+	filters := []artifact.Filter{
+		artifact.ByGoos("linux"),
+		artifact.Or(
+			artifact.ByGoarch("amd64"),
+			artifact.ByGoarch("arm64"),
+			artifact.ByGoarch("386"),
+			artifact.And(
+				artifact.ByGoarch("arm"),
+				artifact.Or(
+					artifact.ByGoarm("7"),
+					artifact.ByGoarm("6"),
+				),
+			),
+		),
+		artifact.Or(
+			artifact.ByType(artifact.UploadableArchive),
+			artifact.ByType(artifact.UploadableBinary),
+		),
+	}
+	if len(pkgbuild.IDs) > 0 {
+		filters = append(filters, artifact.ByIDs(pkgbuild.IDs...))
+	}
+
+	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
+	if len(archives) == 0 {
+		return ErrNoArchivesFound
+	}
+
+	pkg, err := tmpl.New(ctx).Apply(pkgbuild.Package)
+	if err != nil {
+		return err
+	}
+	if strings.TrimSpace(pkg) == "" {
+		art := archives[0]
+		switch art.Type {
+		case artifact.UploadableBinary:
+			name := art.Name
+			bin := art.ExtraOr(artifact.ExtraBinary, art.Name).(string)
+			pkg = fmt.Sprintf(`install -Dm755 "./%s "${pkgdir}/usr/bin/%s"`, name, bin)
+		case artifact.UploadableArchive:
+			for _, bin := range art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string) {
+				pkg = fmt.Sprintf(`install -Dm755 "./%s" "${pkgdir}/usr/bin/%[1]s"`, bin)
+				break
+			}
+		}
+		log.Warnf("guessing package to be %q", pkg)
+	}
+	pkgbuild.Package = pkg
+
+	for _, info := range []struct {
+		name, tpl, ext string
+		kind           artifact.Type
+	}{
+		{
+			name: "PKGBUILD",
+			tpl:  pkgBuildTemplate,
+			ext:  ".pkgbuild",
+			kind: artifact.PkgBuild,
+		},
+		{
+			name: ".SRCINFO",
+			tpl:  srcInfoTemplate,
+			ext:  ".srcinfo",
+			kind: artifact.SrcInfo,
+		},
+	} {
+		pkgContent, err := buildPkgFile(ctx, pkgbuild, cl, archives, info.tpl)
+		if err != nil {
+			return err
+		}
+
+		path := filepath.Join(ctx.Config.Dist, "aur", pkgbuild.Name+info.ext)
+		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+			return fmt.Errorf("failed to write %s: %w", info.kind, err)
+		}
+		log.WithField("file", path).Info("writing")
+		if err := os.WriteFile(path, []byte(pkgContent), 0o644); err != nil { //nolint: gosec
+			return fmt.Errorf("failed to write %s: %w", info.kind, err)
+		}
+
+		ctx.Artifacts.Add(&artifact.Artifact{
+			Name: info.name,
+			Path: path,
+			Type: info.kind,
+			Extra: map[string]interface{}{
+				pkgBuildExtra:    pkgbuild,
+				artifact.ExtraID: pkgbuild.Name,
+			},
+		})
+	}
+
+	return nil
+}
+
+func buildPkgFile(ctx *context.Context, pkg config.AUR, client client.Client, artifacts []*artifact.Artifact, tpl string) (string, error) {
+	data, err := dataFor(ctx, pkg, client, artifacts)
+	if err != nil {
+		return "", err
+	}
+	return applyTemplate(ctx, tpl, data)
+}
+
+func fixLines(s string) string {
+	lines := strings.Split(s, "\n")
+	var result []string
+	for _, line := range lines {
+		l := strings.TrimSpace(line)
+		if l == "" {
+			result = append(result, "")
+			continue
+		}
+		result = append(result, "  "+l)
+	}
+	return strings.Join(result, "\n")
+}
+
+func applyTemplate(ctx *context.Context, tpl string, data templateData) (string, error) {
+	t := template.Must(
+		template.New(data.Name).
+			Funcs(template.FuncMap{
+				"fixLines": fixLines,
+				"pkgArray": toPkgBuildArray,
+			}).
+			Parse(tpl),
+	)
+
+	var out bytes.Buffer
+	if err := t.Execute(&out, data); err != nil {
+		return "", err
+	}
+
+	content, err := tmpl.New(ctx).Apply(out.String())
+	if err != nil {
+		return "", err
+	}
+	out.Reset()
+
+	// Sanitize the template output and get rid of trailing whitespace.
+	var (
+		r = strings.NewReader(content)
+		s = bufio.NewScanner(r)
+	)
+	for s.Scan() {
+		l := strings.TrimRight(s.Text(), " ")
+		_, _ = out.WriteString(l)
+		_ = out.WriteByte('\n')
+	}
+	if err := s.Err(); err != nil {
+		return "", err
+	}
+
+	return out.String(), nil
+}
+
+func toPkgBuildArray(ss []string) string {
+	result := make([]string, 0, len(ss))
+	for _, s := range ss {
+		result = append(result, fmt.Sprintf("'%s'", s))
+	}
+	return strings.Join(result, " ")
+}
+
+func toPkgBuildArch(arch string) string {
+	switch arch {
+	case "amd64":
+		return "x86_64"
+	case "386":
+		return "i686"
+	case "arm64":
+		return "aarch64"
+	case "arm6":
+		return "armv6h"
+	case "arm7":
+		return "armv7h"
+	default:
+		return "invalid" // should never get here
+	}
+}
+
+func dataFor(ctx *context.Context, cfg config.AUR, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) {
+	result := templateData{
+		Name:         cfg.Name,
+		Desc:         cfg.Description,
+		Homepage:     cfg.Homepage,
+		Version:      ctx.Version,
+		License:      cfg.License,
+		Rel:          cfg.Rel,
+		Maintainers:  cfg.Maintainers,
+		Contributors: cfg.Contributors,
+		Provides:     cfg.Provides,
+		Conflicts:    cfg.Conflicts,
+		Depends:      cfg.Depends,
+		OptDepends:   cfg.OptDepends,
+		Package:      cfg.Package,
+	}
+
+	for _, art := range artifacts {
+		sum, err := art.Checksum("sha256")
+		if err != nil {
+			return result, err
+		}
+
+		if cfg.URLTemplate == "" {
+			url, err := cl.ReleaseURLTemplate(ctx)
+			if err != nil {
+				return result, err
+			}
+			cfg.URLTemplate = url
+		}
+		url, err := tmpl.New(ctx).WithArtifact(art, map[string]string{}).Apply(cfg.URLTemplate)
+		if err != nil {
+			return result, err
+		}
+
+		releasePackage := releasePackage{
+			DownloadURL: url,
+			SHA256:      sum,
+			Arch:        toPkgBuildArch(art.Goarch + art.Goarm),
+		}
+		result.ReleasePackages = append(result.ReleasePackages, releasePackage)
+		result.Arches = append(result.Arches, releasePackage.Arch)
+	}
+
+	sort.Strings(result.Arches)
+	sort.Slice(result.ReleasePackages, func(i, j int) bool {
+		return result.ReleasePackages[i].Arch < result.ReleasePackages[j].Arch
+	})
+	return result, nil
+}
+
+// Publish the PKGBUILD and .SRCINFO files to the AUR repository.
+func (Pipe) Publish(ctx *context.Context) error {
+	skips := pipe.SkipMemento{}
+	for _, pkgs := range ctx.Artifacts.Filter(
+		artifact.Or(
+			artifact.ByType(artifact.PkgBuild),
+			artifact.ByType(artifact.SrcInfo),
+		),
+	).GroupByID() {
+		err := doPublish(ctx, pkgs)
+		if err != nil && pipe.IsSkip(err) {
+			skips.Remember(err)
+			continue
+		}
+		if err != nil {
+			return err
+		}
+	}
+	return skips.Evaluate()
+}
+
+func doPublish(ctx *context.Context, pkgs []*artifact.Artifact) error {
+	cfg := pkgs[0].Extra[pkgBuildExtra].(config.AUR)
+
+	if strings.TrimSpace(cfg.SkipUpload) == "true" {
+		return pipe.Skip("pkgbuild.skip_upload is set")
+	}
+
+	if strings.TrimSpace(cfg.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
+		return pipe.Skip("prerelease detected with 'auto' upload, skipping aur publish")
+	}
+
+	key, err := tmpl.New(ctx).Apply(cfg.PrivateKey)
+	if err != nil {
+		return err
+	}
+
+	key, err = keyPath(key)
+	if err != nil {
+		return err
+	}
+
+	url, err := tmpl.New(ctx).Apply(cfg.GitURL)
+	if err != nil {
+		return err
+	}
+
+	if url == "" {
+		return pipe.Skip("pkgbuild.git_url is empty")
+	}
+
+	sshcmd, err := tmpl.New(ctx).WithExtraFields(tmpl.Fields{
+		"KeyPath": key,
+	}).Apply(cfg.GitSSHCommand)
+	if err != nil {
+		return err
+	}
+
+	msg, err := tmpl.New(ctx).Apply(cfg.CommitMessageTemplate)
+	if err != nil {
+		return err
+	}
+
+	author, err := commitauthor.Get(ctx, cfg.CommitAuthor)
+	if err != nil {
+		return err
+	}
+
+	parent := filepath.Join(ctx.Config.Dist, "aur", "repos")
+	cwd := filepath.Join(parent, cfg.Name)
+
+	if err := os.MkdirAll(parent, 0o755); err != nil {
+		return err
+	}
+
+	env := []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", sshcmd)}
+
+	if err := runGitCmds(parent, env, [][]string{
+		{"clone", url, cfg.Name},
+	}); err != nil {
+		return fmt.Errorf("failed to setup local AUR repo: %w", err)
+	}
+
+	if err := runGitCmds(cwd, env, [][]string{
+		// setup auth et al
+		{"config", "--local", "user.name", author.Name},
+		{"config", "--local", "user.email", author.Email},
+		{"config", "--local", "commit.gpgSign", "false"},
+		{"config", "--local", "init.defaultBranch", "master"},
+	}); err != nil {
+		return fmt.Errorf("failed to setup local AUR repo: %w", err)
+	}
+
+	for _, pkg := range pkgs {
+		bts, err := os.ReadFile(pkg.Path)
+		if err != nil {
+			return fmt.Errorf("failed to read %s: %w", pkg.Name, err)
+		}
+
+		if err := os.WriteFile(filepath.Join(cwd, pkg.Name), bts, 0o644); err != nil {
+			return fmt.Errorf("failed to write %s: %w", pkg.Name, err)
+		}
+	}
+
+	log.WithField("repo", url).WithField("name", cfg.Name).Info("pushing")
+	if err := runGitCmds(cwd, env, [][]string{
+		{"add", "-A", "."},
+		{"commit", "-m", msg},
+		{"push", "origin", "HEAD"},
+	}); err != nil {
+		return fmt.Errorf("failed to push %q (%q): %w", cfg.Name, url, err)
+	}
+
+	return nil
+}
+
+func keyPath(key string) (string, error) {
+	if key == "" {
+		return "", pipe.Skip("pkgbuild.private_key is empty")
+	}
+	if _, err := ssh.ParsePrivateKey([]byte(key)); err == nil {
+		f, err := os.CreateTemp("", "id_*")
+		if err != nil {
+			return "", fmt.Errorf("failed to store private key: %w", err)
+		}
+		defer f.Close()
+		if _, err := fmt.Fprint(f, key); err != nil {
+			return "", fmt.Errorf("failed to store private key: %w", err)
+		}
+		if err := os.Chmod(f.Name(), 0o400); err != nil {
+			return "", fmt.Errorf("failed to store private key: %w", err)
+		}
+		return f.Name(), nil
+	}
+
+	if _, err := os.Stat(key); os.IsNotExist(err) {
+		return "", fmt.Errorf("key %q does not exist", key)
+	}
+	return key, nil
+}
+
+func runGitCmds(cwd string, env []string, cmds [][]string) error {
+	for _, cmd := range cmds {
+		args := append([]string{"-C", cwd}, cmd...)
+		if _, err := git.Clean(git.RunWithEnv(env, args...)); err != nil {
+			return fmt.Errorf("%q failed: %w", strings.Join(cmd, " "), err)
+		}
+	}
+	return nil
+}
diff --git a/internal/pipe/aur/doc.go b/internal/pipe/aur/doc.go
new file mode 100644
index 00000000..c6127444
--- /dev/null
+++ b/internal/pipe/aur/doc.go
@@ -0,0 +1,2 @@
+// Package aur provides the Arch User Repository (AUR) integration to GoReleaser.
+package aur
diff --git a/internal/pipe/aur/tmpl.go b/internal/pipe/aur/tmpl.go
new file mode 100644
index 00000000..1dc4491b
--- /dev/null
+++ b/internal/pipe/aur/tmpl.go
@@ -0,0 +1,97 @@
+package aur
+
+type templateData struct {
+	Name            string
+	Desc            string
+	Homepage        string
+	Version         string
+	License         string
+	ReleasePackages []releasePackage
+	Maintainers     []string
+	Contributors    []string
+	Provides        []string
+	Conflicts       []string
+	Depends         []string
+	OptDepends      []string
+	Arches          []string
+	Rel             string
+	Package         string
+}
+
+type releasePackage struct {
+	DownloadURL string
+	SHA256      string
+	Arch        string
+}
+
+const pkgBuildTemplate = `# This file was generated by GoReleaser. DO NOT EDIT.
+
+{{- range .Maintainers }}
+# Maintainer: {{ . }}
+{{- end }}
+{{- range .Contributors }}
+# Contributor: {{ . }}
+{{- end }}
+
+pkgname='{{ .Name }}'
+pkgver={{ .Version }}
+pkgrel={{ .Rel }}
+pkgdesc='{{ .Desc }}'
+url='{{ .Homepage }}'
+arch=({{ pkgArray .Arches }})
+license=('{{ .License }}')
+{{- with .Provides }}
+provides=({{ pkgArray . }})
+{{- end }}
+{{- with .Conflicts }}
+conflicts=({{ pkgArray . }})
+{{- end }}
+{{- with .Depends }}
+depends=({{ pkgArray . }})
+{{- end }}
+{{- with .OptDepends }}
+optdepends=({{ pkgArray . }})
+{{- end }}
+
+{{ range .ReleasePackages -}}
+source_{{ .Arch }}=('{{ .DownloadURL }}')
+sha256sums_{{ .Arch }}=('{{ .SHA256 }}')
+{{ printf "" }}
+{{ end }}
+
+{{-  with .Package -}}
+package() {
+{{ fixLines . }}
+}
+{{ end }}`
+
+const srcInfoTemplate = `pkgbase = {{ .Name }}
+	pkgdesc = {{ .Desc }}
+	pkgver = {{ .Version }}
+	pkgrel = {{ .Rel }}
+	{{ with .Homepage -}}
+	url = {{ . }}
+	{{ end -}}
+	{{ with .License -}}
+	license = {{ . }}
+	{{ end -}}
+	{{ range .OptDepends -}}
+	optdepends = {{ . }}
+	{{ end -}}
+	{{ range .Depends -}}
+	depends = {{ . }}
+	{{ end -}}
+	{{ range .Conflicts -}}
+	conflicts = {{ . }}
+	{{ end -}}
+	{{ range .Provides -}}
+	provides = {{ . }}
+	{{ end -}}
+	{{ range .ReleasePackages -}}
+	arch = {{ .Arch }}
+	source_{{ .Arch }} = {{ .DownloadURL }}
+	sha256sums_{{ .Arch }} = {{ .SHA256 }}
+	{{ end -}}
+{{ printf "\n" -}}
+pkgname = {{ .Name }}
+`
diff --git a/internal/pipe/publish/publish.go b/internal/pipe/publish/publish.go
index 028915c2..9c986313 100644
--- a/internal/pipe/publish/publish.go
+++ b/internal/pipe/publish/publish.go
@@ -8,6 +8,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/middleware/logging"
 	"github.com/goreleaser/goreleaser/internal/middleware/skip"
 	"github.com/goreleaser/goreleaser/internal/pipe/artifactory"
+	"github.com/goreleaser/goreleaser/internal/pipe/aur"
 	"github.com/goreleaser/goreleaser/internal/pipe/blob"
 	"github.com/goreleaser/goreleaser/internal/pipe/brew"
 	"github.com/goreleaser/goreleaser/internal/pipe/custompublishers"
@@ -43,8 +44,9 @@ var publishers = []Publisher{
 	snapcraft.Pipe{},
 	// This should be one of the last steps
 	release.Pipe{},
-	// brew and scoop use the release URL, so, they should be last
+	// brew et al use the release URL, so, they should be last
 	brew.Pipe{},
+	aur.Pipe{},
 	gofish.Pipe{},
 	krew.Pipe{},
 	scoop.Pipe{},
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index 719bfe59..4d3f830d 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -7,6 +7,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/announce"
 	"github.com/goreleaser/goreleaser/internal/pipe/archive"
 	"github.com/goreleaser/goreleaser/internal/pipe/artifacts"
+	"github.com/goreleaser/goreleaser/internal/pipe/aur"
 	"github.com/goreleaser/goreleaser/internal/pipe/before"
 	"github.com/goreleaser/goreleaser/internal/pipe/brew"
 	"github.com/goreleaser/goreleaser/internal/pipe/build"
@@ -74,6 +75,7 @@ var Pipeline = append(
 	sourcearchive.Pipe{}, // archive the source code using git-archive
 	nfpm.Pipe{},          // archive via fpm (deb, rpm) using "native" go impl
 	snapcraft.Pipe{},     // archive via snapcraft (snap)
+	aur.Pipe{},           // create arch linux aur pkgbuild
 	brew.Pipe{},          // create brew tap
 	gofish.Pipe{},        // create gofish rig
 	krew.Pipe{},          // krew plugins
diff --git a/pkg/config/config.go b/pkg/config/config.go
index e59a5380..f5e23451 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -107,6 +107,29 @@ func (a HomebrewDependency) JSONSchemaType() *jsonschema.Type {
 	}
 }
 
+type AUR struct {
+	Name                  string       `yaml:"name,omitempty"`
+	IDs                   []string     `yaml:"ids,omitempty"`
+	CommitAuthor          CommitAuthor `yaml:"commit_author,omitempty"`
+	CommitMessageTemplate string       `yaml:"commit_msg_template,omitempty"`
+	Description           string       `yaml:"description,omitempty"`
+	Homepage              string       `yaml:"homepage,omitempty"`
+	License               string       `yaml:"license,omitempty"`
+	SkipUpload            string       `yaml:"skip_upload,omitempty"`
+	URLTemplate           string       `yaml:"url_template,omitempty"`
+	Maintainers           []string     `yaml:"maintainers,omitempty"`
+	Contributors          []string     `yaml:"contributors,omitempty"`
+	Provides              []string     `yaml:"provides,omitempty"`
+	Conflicts             []string     `yaml:"conflicts,omitempty"`
+	Depends               []string     `yaml:"depends,omitempty"`
+	OptDepends            []string     `yaml:"optdepends,omitempty"`
+	Rel                   string       `yaml:"rel,omitempty"`
+	Package               string       `yaml:"package,omitempty"`
+	GitURL                string       `yaml:"git_url,omitempty"`
+	GitSSHCommand         string       `yaml:"git_ssh_command,omitempty"`
+	PrivateKey            string       `yaml:"private_key,omitempty"`
+}
+
 // GoFish contains the gofish section.
 type GoFish struct {
 	Name                  string       `yaml:"name,omitempty"`
@@ -827,6 +850,7 @@ type Project struct {
 	Milestones      []Milestone      `yaml:"milestones,omitempty"`
 	Brews           []Homebrew       `yaml:"brews,omitempty"`
 	Rigs            []GoFish         `yaml:"rigs,omitempty"`
+	AURs            []AUR            `yaml:"aurs,omitempty"`
 	Krews           []Krew           `yaml:"krews,omitempty"`
 	Scoop           Scoop            `yaml:"scoop,omitempty"`
 	Builds          []Build          `yaml:"builds,omitempty"`
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index 3e1b9714..61632cb1 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/goreleaser/goreleaser/internal/pipe/archive"
 	"github.com/goreleaser/goreleaser/internal/pipe/artifactory"
+	"github.com/goreleaser/goreleaser/internal/pipe/aur"
 	"github.com/goreleaser/goreleaser/internal/pipe/blob"
 	"github.com/goreleaser/goreleaser/internal/pipe/brew"
 	"github.com/goreleaser/goreleaser/internal/pipe/build"
@@ -69,6 +70,7 @@ var Defaulters = []Defaulter{
 	docker.ManifestPipe{},
 	artifactory.Pipe{},
 	blob.Pipe{},
+	aur.Pipe{},
 	brew.Pipe{},
 	krew.Pipe{},
 	gofish.Pipe{},
diff --git a/www/docs/customization/aur.md b/www/docs/customization/aur.md
new file mode 100644
index 00000000..58daa1f5
--- /dev/null
+++ b/www/docs/customization/aur.md
@@ -0,0 +1,155 @@
+# Arch User Repositories
+
+After releasing to GitHub or GitLab, GoReleaser can generate and publish
+a `PKGBUILD` to an _Arch User Repository_.
+
+!!! warning
+    Before going further on this, make sure to read
+    [AUR's Submission Guidelines](https://wiki.archlinux.org/title/AUR_submission_guidelines).
+
+This page describes the available options.
+
+```yaml
+# .goreleaser.yaml
+aurs:
+  -
+    # The package name.
+    #
+    # Defaults to the Project Name with a -bin suffix.
+    #
+    # Note that since this integration does not create a PKGBUILD to build from
+    # source, per Arch's guidelines.
+    # That said, GoReleaser will enforce a `-bin` suffix if its not present.
+    name: package-bin
+
+    # Artifact IDs to filter for.
+    #
+    # Defaults to empty, which includes all artifacts.
+    ids:
+      - foo
+      - bar
+
+    # Your app's homepage.
+    # Default is empty.
+    homepage: "https://example.com/"
+
+    # Template of your app's description.
+    # Default is empty.
+    description: "Software to create fast and easy drum rolls."
+
+    # The maintainers of the package.
+    # Defaults to empty.
+    maintainers:
+      - 'Foo Bar <foo at bar dot com>'
+
+    # The contributors of the package.
+    # Defaults to empty.
+    contributors:
+      - 'Foo Zaz <foo at zaz dot com>'
+
+    # SPDX identifier of your app's license.
+    # Default is empty.
+    license: "MIT"
+
+    # The SSH private key that should be used to commit to the Git repository.
+    # This can either be a path or the key contents.
+    #
+    # WARNING: do not expose your private key in the config file!
+    private_key: '{{ .Env.AUR_KEY }}'
+
+    # The AUR Git URL for this package.
+    # Defaults to empty.
+    git_url: 'ssh://aur@aur.archlinux.org/mypackage-bin.git'
+
+    # Setting this will prevent goreleaser to actually try to commit the updated
+    # formula - instead, the formula file will be stored on the dist folder only,
+    # leaving the responsibility of publishing it to the user.
+    #
+    # If set to auto, the release will not be uploaded to the homebrew tap
+    # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1.
+    #
+    # Default is false.
+    skip_upload: true
+
+    # List of additional packages that the software provides the features of.
+    #
+    # Defaults to the project name.
+    provides:
+      - mybin
+
+    # List of packages that conflict with, or cause problems with the package.
+    #
+    # Defaults to the project name.
+    conflicts:
+      - mybin
+
+    # List of packages that must be installed to install this.
+    #
+    # Defaults to empty.
+    depends:
+      - curl
+
+    # List of packages that are not needed for the software to function,
+    # but provide additional features.
+    #
+    # Must be in the format `package: short description of the extra functionality`.
+    #
+    # Defaults to empty.
+    optdepends:
+      - 'wget: for downloading things'
+
+    # Custom package instructions.
+    #
+    # Defaults to `install -Dm755 "./PROJECT_NAME" "${pkgdir}/usr/bin/PROJECT_NAME",
+    # which is not always correct.
+    #
+    # We recommend you override this, installing the binary, license and
+    # everything else your package needs.
+    package: |-
+      # bin
+      install -Dm755 "./mybin" "${pkgdir}/usr/bin/mybin"
+
+      # license
+      install -Dm644 "./LICENSE.md" "${pkgdir}/usr/share/licenses/mybin/LICENSE"
+
+      # completions
+      mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
+      mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
+      mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
+      install -Dm644 "./completions/mybin.bash" "${pkgdir}/usr/share/bash-completion/completions/mybin"
+      install -Dm644 "./completions/mybin.zsh" "${pkgdir}/usr/share/zsh/site-functions/_mybin"
+      install -Dm644 "./completions/mybin.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/mybin.fish"
+
+      # man pages
+      install -Dm644 "./manpages/mybin.1.gz" "${pkgdir}/usr/share/man/man1/mybin.1.gz"
+
+    # Git author used to commit to the repository.
+    # Defaults are shown below.
+    commit_author:
+      name: goreleaserbot
+      email: goreleaser@carlosbecker.com
+
+    # Commit message template.
+    # Defaults to `Update to {{ .Tag }}`.
+    commit_msg_template: "pkgbuild updates"
+
+    # The value to be passed to `GIT_SSH_COMMAND`.
+    # This is mainly used to specify the SSH private key used to pull/push to
+    # the Git URL.
+    #
+    # Defaults to `ssh -i {{ .KeyPath }} -F /dev/null`.
+    git_ssh_command: 'ssh -i {{ .Env.KEY }} -o SomeOption=yes'
+
+    # Template for the url which is determined by the given Token
+    # (github, gitlab or gitea).
+    #
+    # Default depends on the client.
+    url_template: "http://github.mycompany.com/foo/bar/releases/{{ .Tag }}/{{ .ArtifactName }}"
+```
+
+!!! tip
+    Learn more about the [name template engine](/customization/templates/).
+
+!!! tip
+    For more info about what each field does, please refer to
+    [Arch's PKGBUILD reference](https://wiki.archlinux.org/title/PKGBUILD).
diff --git a/www/docs/customization/homebrew.md b/www/docs/customization/homebrew.md
index 437ce1b0..503fdc25 100644
--- a/www/docs/customization/homebrew.md
+++ b/www/docs/customization/homebrew.md
@@ -42,10 +42,9 @@ brews:
       # Optionally a token can be provided, if it differs from the token provided to GoReleaser
       token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
 
-    # Template for the url which is determined by the given Token (github or gitlab)
-    # Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
-    # Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/-/releases/{{ .Tag }}/downloads/{{ .ArtifactName }}"
-    # Default for gitea is "https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
+    # Template for the url which is determined by the given Token (github, gitlab or gitea)
+    #
+    # Default depends on the client.
     url_template: "http://github.mycompany.com/foo/bar/releases/{{ .Tag }}/{{ .ArtifactName }}"
 
     # Allows you to set a custom download strategy. Note that you'll need
diff --git a/www/mkdocs.yml b/www/mkdocs.yml
index 81bfd2c0..b4c7cfd0 100644
--- a/www/mkdocs.yml
+++ b/www/mkdocs.yml
@@ -100,6 +100,7 @@ nav:
     - customization/blob.md
     - customization/fury.md
     - customization/homebrew.md
+    - customization/aur.md
     - customization/gofish.md
     - customization/krew.md
     - customization/scoop.md


Test patch:
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
index 753c2e68..dcc35ef1 100644
--- a/internal/artifact/artifact_test.go
+++ b/internal/artifact/artifact_test.go
@@ -386,6 +386,8 @@ func TestTypeToString(t *testing.T) {
 		KrewPluginManifest,
 		ScoopManifest,
 		SBOM,
+		PkgBuild,
+		SrcInfo,
 	} {
 		t.Run(a.String(), func(t *testing.T) {
 			require.NotEqual(t, "unknown", a.String())
diff --git a/internal/pipe/aur/aur_test.go b/internal/pipe/aur/aur_test.go
new file mode 100644
index 00000000..6fcd05e4
--- /dev/null
+++ b/internal/pipe/aur/aur_test.go
@@ -0,0 +1,757 @@
+package aur
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/charmbracelet/keygen"
+	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/client"
+	"github.com/goreleaser/goreleaser/internal/git"
+	"github.com/goreleaser/goreleaser/internal/golden"
+	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDescription(t *testing.T) {
+	require.NotEmpty(t, Pipe{}.String())
+}
+
+func createTemplateData() templateData {
+	return templateData{
+		Name:       "test-bin",
+		Desc:       "Some desc",
+		Homepage:   "https://example.com",
+		Conflicts:  []string{"nope"},
+		Depends:    []string{"nope"},
+		Arches:     []string{"x86_64", "i686", "aarch64", "armv6h", "armv7h"},
+		Rel:        "1",
+		Provides:   []string{"test"},
+		OptDepends: []string{"nfpm"},
+		Maintainers: []string{
+			"Ciclano <ciclano@example.com>",
+			"Cicrano <cicrano@example.com>",
+		},
+		Contributors: []string{
+			"Fulano <fulano@example.com>",
+			"Beltrano <beltrano@example.com>",
+		},
+		License: "MIT",
+		Version: "0.1.3",
+		Package: `# bin
+		install -Dm755 "./goreleaser" "${pkgdir}/usr/bin/goreleaser"
+
+		# license
+		install -Dm644 "./LICENSE.md" "${pkgdir}/usr/share/licenses/goreleaser/LICENSE"
+
+		# completions
+		mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
+		mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
+		mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
+		install -Dm644 "./completions/goreleaser.bash" "${pkgdir}/usr/share/bash-completion/completions/goreleaser"
+		install -Dm644 "./completions/goreleaser.zsh" "${pkgdir}/usr/share/zsh/site-functions/_goreleaser"
+		install -Dm644 "./completions/goreleaser.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/goreleaser.fish"
+
+		# man pages
+		install -Dm644 "./manpages/goreleaser.1.gz" "${pkgdir}/usr/share/man/man1/goreleaser.1.gz"`,
+		ReleasePackages: []releasePackage{
+			{
+				Arch:        "x86_64",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+			{
+				Arch:        "armv6h",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_Arm6.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+			{
+				Arch:        "aarch64",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_Arm64.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+			{
+				Arch:        "i686",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_386.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+			{
+				Arch:        "armv7h",
+				DownloadURL: "https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_arm7.tar.gz",
+				SHA256:      "1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67",
+			},
+		},
+	}
+}
+
+func TestFullPkgBuild(t *testing.T) {
+	data := createTemplateData()
+	pkg, err := applyTemplate(context.New(config.Project{
+		ProjectName: "foo",
+	}), pkgBuildTemplate, data)
+	require.NoError(t, err)
+
+	golden.RequireEqual(t, []byte(pkg))
+}
+
+func TestPkgBuildSimple(t *testing.T) {
+	pkg, err := applyTemplate(context.New(config.Project{}), pkgBuildTemplate, createTemplateData())
+	require.NoError(t, err)
+	require.Contains(t, pkg, `# Maintainer: Ciclano <ciclano@example.com>`)
+	require.Contains(t, pkg, `# Maintainer: Cicrano <cicrano@example.com>`)
+	require.Contains(t, pkg, `# Contributor: Fulano <fulano@example.com>`)
+	require.Contains(t, pkg, `# Contributor: Beltrano <beltrano@example.com>`)
+	require.Contains(t, pkg, `pkgname='test-bin'`)
+	require.Contains(t, pkg, `url='https://example.com'`)
+	require.Contains(t, pkg, `source_x86_64=('https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz')`)
+	require.Contains(t, pkg, `sha256sums_x86_64=('1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67')`)
+	require.Contains(t, pkg, `pkgver=0.1.3`)
+}
+
+func TestFullSrcInfo(t *testing.T) {
+	data := createTemplateData()
+	data.License = "MIT"
+	pkg, err := applyTemplate(context.New(config.Project{
+		ProjectName: "foo",
+	}), srcInfoTemplate, data)
+	require.NoError(t, err)
+
+	golden.RequireEqual(t, []byte(pkg))
+}
+
+func TestSrcInfoSimple(t *testing.T) {
+	pkg, err := applyTemplate(context.New(config.Project{}), srcInfoTemplate, createTemplateData())
+	require.NoError(t, err)
+	require.Contains(t, pkg, `pkgbase = test-bin`)
+	require.Contains(t, pkg, `pkgname = test-bin`)
+	require.Contains(t, pkg, `url = https://example.com`)
+	require.Contains(t, pkg, `source_x86_64 = https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz`)
+	require.Contains(t, pkg, `sha256sums_x86_64 = 1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67`)
+	require.Contains(t, pkg, `pkgver = 0.1.3`)
+}
+
+func TestFullPipe(t *testing.T) {
+	type testcase struct {
+		prepare              func(ctx *context.Context)
+		expectedRunError     string
+		expectedPublishError string
+	}
+	for name, tt := range map[string]testcase{
+		"default": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitHub
+				ctx.Config.AURs[0].Homepage = "https://github.com/goreleaser"
+			},
+		},
+		"with-more-opts": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitHub
+				ctx.Config.AURs[0].Homepage = "https://github.com/goreleaser"
+				ctx.Config.AURs[0].Maintainers = []string{"me"}
+				ctx.Config.AURs[0].Contributors = []string{"me as well"}
+				ctx.Config.AURs[0].Depends = []string{"curl", "bash"}
+				ctx.Config.AURs[0].OptDepends = []string{"wget: stuff", "foo: bar"}
+				ctx.Config.AURs[0].Provides = []string{"git", "svn"}
+				ctx.Config.AURs[0].Conflicts = []string{"libcurl", "cvs", "blah"}
+			},
+		},
+		"default-gitlab": {
+			prepare: func(ctx *context.Context) {
+				ctx.TokenType = context.TokenTypeGitLab
+				ctx.Config.AURs[0].Homepage = "https://gitlab.com/goreleaser"
+			},
+		},
+		"invalid-name-template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].Name = "{{ .Asdsa }"
+			},
+			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+		"invalid-package-template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].Package = "{{ .Asdsa }"
+			},
+			expectedRunError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+		"invalid-commit-template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].CommitMessageTemplate = "{{ .Asdsa }"
+			},
+			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+		"invalid-key-template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].PrivateKey = "{{ .Asdsa }"
+			},
+			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+		"no-key": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].PrivateKey = ""
+			},
+			expectedPublishError: `pkgbuild.private_key is empty`,
+		},
+		"key-not-found": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].PrivateKey = "testdata/nope"
+			},
+			expectedPublishError: `key "testdata/nope" does not exist`,
+		},
+		"invalid-git-url-template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].GitURL = "{{ .Asdsa }"
+			},
+			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+		"no-git-url": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].GitURL = ""
+			},
+			expectedPublishError: `pkgbuild.git_url is empty`,
+		},
+		"invalid-ssh-cmd-template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].GitSSHCommand = "{{ .Asdsa }"
+			},
+			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+		"invalid-commit-author-template": {
+			prepare: func(ctx *context.Context) {
+				ctx.Config.AURs[0].CommitAuthor.Name = "{{ .Asdsa }"
+			},
+			expectedPublishError: `template: tmpl:1: unexpected "}" in operand`,
+		},
+	} {
+		t.Run(name, func(t *testing.T) {
+			url := makeBareRepo(t)
+			key := makeKey(t)
+
+			folder := t.TempDir()
+			ctx := &context.Context{
+				Git: context.GitInfo{
+					CurrentTag: "v1.0.1",
+				},
+				Version:   "1.0.1",
+				Artifacts: artifact.New(),
+				Env: map[string]string{
+					"FOO": "foo_is_bar",
+				},
+				Config: config.Project{
+					Dist:        folder,
+					ProjectName: name,
+					AURs: []config.AUR{
+						{
+							Name:        name,
+							IDs:         []string{"foo"},
+							PrivateKey:  key,
+							License:     "MIT",
+							GitURL:      url,
+							Description: "A run pipe test fish food and FOO={{ .Env.FOO }}",
+						},
+					},
+				},
+			}
+			tt.prepare(ctx)
+			ctx.Artifacts.Add(&artifact.Artifact{
+				Name:   "bar_bin.tar.gz",
+				Path:   "doesnt matter",
+				Goos:   "linux",
+				Goarch: "amd64",
+				Type:   artifact.UploadableArchive,
+				Extra: map[string]interface{}{
+					artifact.ExtraID:       "bar",
+					artifact.ExtraFormat:   "tar.gz",
+					artifact.ExtraBinaries: []string{"bar"},
+				},
+			})
+			path := filepath.Join(folder, "bin.tar.gz")
+			ctx.Artifacts.Add(&artifact.Artifact{
+				Name:   "bin.tar.gz",
+				Path:   path,
+				Goos:   "linux",
+				Goarch: "amd64",
+				Type:   artifact.UploadableArchive,
+				Extra: map[string]interface{}{
+					artifact.ExtraID:       "foo",
+					artifact.ExtraFormat:   "tar.gz",
+					artifact.ExtraBinaries: []string{"name"},
+				},
+			})
+
+			f, err := os.Create(path)
+			require.NoError(t, err)
+			require.NoError(t, f.Close())
+			client := client.NewMock()
+
+			require.NoError(t, Pipe{}.Default(ctx))
+
+			if tt.expectedRunError != "" {
+				require.EqualError(t, runAll(ctx, client), tt.expectedRunError)
+				return
+			}
+			require.NoError(t, runAll(ctx, client))
+
+			if tt.expectedPublishError != "" {
+				require.EqualError(t, Pipe{}.Publish(ctx), tt.expectedPublishError)
+				return
+			}
+			require.NoError(t, Pipe{}.Publish(ctx))
+
+			requireEqualRepoFiles(t, folder, name, url)
+		})
+	}
+}
+
+func TestRunPipe(t *testing.T) {
+	url := makeBareRepo(t)
+	key := makeKey(t)
+
+	folder := t.TempDir()
+	ctx := &context.Context{
+		TokenType: context.TokenTypeGitHub,
+		Git: context.GitInfo{
+			CurrentTag: "v1.0.1",
+		},
+		Version:   "1.0.1",
+		Artifacts: artifact.New(),
+		Env: map[string]string{
+			"FOO": "foo_is_bar",
+		},
+		Config: config.Project{
+			Dist:        folder,
+			ProjectName: "foo",
+			AURs: []config.AUR{
+				{
+					License:     "MIT",
+					Description: "A run pipe test pkgbuild and FOO={{ .Env.FOO }}",
+					Homepage:    "https://github.com/goreleaser",
+					IDs:         []string{"foo"},
+					GitURL:      url,
+					PrivateKey:  key,
+				},
+			},
+			GitHubURLs: config.GitHubURLs{
+				Download: "https://github.com",
+			},
+			Release: config.Release{
+				GitHub: config.Repo{
+					Owner: "test",
+					Name:  "test",
+				},
+			},
+		},
+	}
+	for _, a := range []struct {
+		name   string
+		goos   string
+		goarch string
+		goarm  string
+	}{
+		{
+			name:   "bin",
+			goos:   "darwin",
+			goarch: "amd64",
+		},
+		{
+			name:   "bin",
+			goos:   "darwin",
+			goarch: "arm64",
+		},
+		{
+			name:   "bin",
+			goos:   "windows",
+			goarch: "arm64",
+		},
+		{
+			name:   "bin",
+			goos:   "windows",
+			goarch: "amd64",
+		},
+		{
+			name:   "bin",
+			goos:   "linux",
+			goarch: "386",
+		},
+		{
+			name:   "bin",
+			goos:   "linux",
+			goarch: "amd64",
+		},
+		{
+			name:   "arm64",
+			goos:   "linux",
+			goarch: "arm64",
+		},
+		{
+			name:   "armv5",
+			goos:   "linux",
+			goarch: "arm",
+			goarm:  "5",
+		},
+		{
+			name:   "armv6",
+			goos:   "linux",
+			goarch: "arm",
+			goarm:  "6",
+		},
+		{
+			name:   "armv7",
+			goos:   "linux",
+			goarch: "arm",
+			goarm:  "7",
+		},
+	} {
+		path := filepath.Join(folder, fmt.Sprintf("%s.tar.gz", a.name))
+		ctx.Artifacts.Add(&artifact.Artifact{
+			Name:   fmt.Sprintf("%s.tar.gz", a.name),
+			Path:   path,
+			Goos:   a.goos,
+			Goarch: a.goarch,
+			Goarm:  a.goarm,
+			Type:   artifact.UploadableArchive,
+			Extra: map[string]interface{}{
+				artifact.ExtraID:       "foo",
+				artifact.ExtraFormat:   "tar.gz",
+				artifact.ExtraBinaries: []string{"foo"},
+			},
+		})
+		f, err := os.Create(path)
+		require.NoError(t, err)
+		require.NoError(t, f.Close())
+	}
+
+	client := client.NewMock()
+
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.NoError(t, runAll(ctx, client))
+	require.NoError(t, Pipe{}.Publish(ctx))
+
+	requireEqualRepoFiles(t, folder, "foo", url)
+}
+
+func TestRunPipeNoBuilds(t *testing.T) {
+	ctx := &context.Context{
+		TokenType: context.TokenTypeGitHub,
+		Config: config.Project{
+			ProjectName: "foo",
+			AURs:        []config.AUR{{}},
+		},
+	}
+	client := client.NewMock()
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Equal(t, ErrNoArchivesFound, runAll(ctx, client))
+	require.False(t, client.CreatedFile)
+}
+
+func TestRunPipeBinaryRelease(t *testing.T) {
+	url := makeBareRepo(t)
+	key := makeKey(t)
+	folder := t.TempDir()
+	ctx := &context.Context{
+		Git: context.GitInfo{
+			CurrentTag: "v1.2.1",
+		},
+		Version:   "1.2.1",
+		Artifacts: artifact.New(),
+		Config: config.Project{
+			Dist:        folder,
+			ProjectName: "foo",
+			AURs: []config.AUR{{
+				GitURL:     url,
+				PrivateKey: key,
+			}},
+		},
+	}
+
+	path := filepath.Join(folder, "dist/foo_linux_amd64/foo")
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "foo_linux_amd64",
+		Path:   path,
+		Goos:   "linux",
+		Goarch: "amd64",
+		Type:   artifact.UploadableBinary,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:     "foo",
+			artifact.ExtraFormat: "binary",
+			artifact.ExtraBinary: "foo",
+		},
+	})
+
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+
+	client := client.NewMock()
+	require.NoError(t, runAll(ctx, client))
+	require.NoError(t, Pipe{}.Publish(ctx))
+
+	requireEqualRepoFiles(t, folder, "foo", url)
+}
+
+func TestRunPipeNoUpload(t *testing.T) {
+	folder := t.TempDir()
+	ctx := context.New(config.Project{
+		Dist:        folder,
+		ProjectName: "foo",
+		Release:     config.Release{},
+		AURs:        []config.AUR{{}},
+	})
+	ctx.TokenType = context.TokenTypeGitHub
+	ctx.Git = context.GitInfo{CurrentTag: "v1.0.1"}
+	path := filepath.Join(folder, "whatever.tar.gz")
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin",
+		Path:   path,
+		Goos:   "linux",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "foo",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"foo"},
+		},
+	})
+
+	require.NoError(t, Pipe{}.Default(ctx))
+	client := client.NewMock()
+
+	assertNoPublish := func(t *testing.T) {
+		t.Helper()
+		require.NoError(t, runAll(ctx, client))
+		testlib.AssertSkipped(t, Pipe{}.Publish(ctx))
+		require.False(t, client.CreatedFile)
+	}
+	t.Run("skip upload true", func(t *testing.T) {
+		ctx.Config.AURs[0].SkipUpload = "true"
+		ctx.Semver.Prerelease = ""
+		assertNoPublish(t)
+	})
+	t.Run("skip upload auto", func(t *testing.T) {
+		ctx.Config.AURs[0].SkipUpload = "auto"
+		ctx.Semver.Prerelease = "beta1"
+		assertNoPublish(t)
+	})
+}
+
+func TestRunEmptyTokenType(t *testing.T) {
+	folder := t.TempDir()
+	ctx := context.New(config.Project{
+		Dist:        folder,
+		ProjectName: "foo",
+		Release:     config.Release{},
+		Rigs: []config.GoFish{
+			{
+				Rig: config.RepoRef{
+					Owner: "test",
+					Name:  "test",
+				},
+			},
+		},
+	})
+	ctx.Git = context.GitInfo{CurrentTag: "v1.0.1"}
+	path := filepath.Join(folder, "whatever.tar.gz")
+	f, err := os.Create(path)
+	require.NoError(t, err)
+	require.NoError(t, f.Close())
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Name:   "bin",
+		Path:   path,
+		Goos:   "darwin",
+		Goarch: "amd64",
+		Type:   artifact.UploadableArchive,
+		Extra: map[string]interface{}{
+			artifact.ExtraID:       "foo",
+			artifact.ExtraFormat:   "tar.gz",
+			artifact.ExtraBinaries: []string{"foo"},
+		},
+	})
+	client := client.NewMock()
+	require.NoError(t, runAll(ctx, client))
+}
+
+func TestDefault(t *testing.T) {
+	t.Run("empty", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitHub,
+			Config: config.Project{
+				ProjectName: "myproject",
+				AURs: []config.AUR{
+					{},
+				},
+			},
+		}
+		require.NoError(t, Pipe{}.Default(ctx))
+		require.Equal(t, config.AUR{
+			Name:                  "myproject-bin",
+			Conflicts:             []string{"myproject"},
+			Provides:              []string{"myproject"},
+			Rel:                   "1",
+			CommitMessageTemplate: defaultCommitMsg,
+			GitSSHCommand:         defaultSSHCommand,
+			CommitAuthor: config.CommitAuthor{
+				Name:  "goreleaserbot",
+				Email: "goreleaser@carlosbecker.com",
+			},
+		}, ctx.Config.AURs[0])
+	})
+
+	t.Run("name-without-bin-suffix", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitHub,
+			Config: config.Project{
+				ProjectName: "myproject",
+				AURs: []config.AUR{
+					{
+						Name: "foo",
+					},
+				},
+			},
+		}
+		require.NoError(t, Pipe{}.Default(ctx))
+		require.Equal(t, config.AUR{
+			Name:                  "foo-bin",
+			Conflicts:             []string{"myproject"},
+			Provides:              []string{"myproject"},
+			Rel:                   "1",
+			CommitMessageTemplate: defaultCommitMsg,
+			GitSSHCommand:         defaultSSHCommand,
+			CommitAuthor: config.CommitAuthor{
+				Name:  "goreleaserbot",
+				Email: "goreleaser@carlosbecker.com",
+			},
+		}, ctx.Config.AURs[0])
+	})
+
+	t.Run("partial", func(t *testing.T) {
+		ctx := &context.Context{
+			TokenType: context.TokenTypeGitHub,
+			Config: config.Project{
+				ProjectName: "myproject",
+				AURs: []config.AUR{
+					{
+						Conflicts: []string{"somethingelse"},
+					},
+				},
+			},
+		}
+		require.NoError(t, Pipe{}.Default(ctx))
+		require.Equal(t, config.AUR{
+			Name:                  "myproject-bin",
+			Conflicts:             []string{"somethingelse"},
+			Provides:              []string{"myproject"},
+			Rel:                   "1",
+			CommitMessageTemplate: defaultCommitMsg,
+			GitSSHCommand:         defaultSSHCommand,
+			CommitAuthor: config.CommitAuthor{
+				Name:  "goreleaserbot",
+				Email: "goreleaser@carlosbecker.com",
+			},
+		}, ctx.Config.AURs[0])
+	})
+}
+
+func TestSkip(t *testing.T) {
+	t.Run("skip", func(t *testing.T) {
+		require.True(t, Pipe{}.Skip(context.New(config.Project{})))
+	})
+
+	t.Run("dont skip", func(t *testing.T) {
+		ctx := context.New(config.Project{
+			AURs: []config.AUR{
+				{},
+			},
+		})
+		require.False(t, Pipe{}.Skip(ctx))
+	})
+}
+
+func TestKeyPath(t *testing.T) {
+	t.Run("with valid path", func(t *testing.T) {
+		path := makeKey(t)
+		result, err := keyPath(path)
+		require.NoError(t, err)
+		require.Equal(t, path, result)
+	})
+	t.Run("with invalid path", func(t *testing.T) {
+		result, err := keyPath("testdata/nope")
+		require.EqualError(t, err, `key "testdata/nope" does not exist`)
+		require.Equal(t, "", result)
+	})
+	t.Run("with key", func(t *testing.T) {
+		for _, algo := range []keygen.KeyType{keygen.Ed25519, keygen.RSA} {
+			t.Run(string(algo), func(t *testing.T) {
+				path := makeKey(t, algo)
+				bts, err := os.ReadFile(path)
+				require.NoError(t, err)
+
+				result, err := keyPath(string(bts))
+				require.NoError(t, err)
+
+				resultbts, err := os.ReadFile(result)
+				require.NoError(t, err)
+				require.Equal(t, string(bts), string(resultbts))
+			})
+		}
+	})
+	t.Run("empty", func(t *testing.T) {
+		result, err := keyPath("")
+		require.EqualError(t, err, `pkgbuild.private_key is empty`)
+		require.Equal(t, "", result)
+	})
+}
+
+func makeBareRepo(tb testing.TB) string {
+	tb.Helper()
+	dir := tb.TempDir()
+	_, err := git.Run(
+		"-C", dir,
+		"-c", "init.defaultBranch=master",
+		"init",
+		"--bare",
+		".",
+	)
+	require.NoError(tb, err)
+	return dir
+}
+
+func makeKey(tb testing.TB, algo ...keygen.KeyType) string {
+	tb.Helper()
+
+	if len(algo) == 0 {
+		algo = append(algo, keygen.Ed25519)
+	}
+	dir := tb.TempDir()
+	k, err := keygen.NewWithWrite(dir, "id", nil, algo[0])
+	require.NoError(tb, err)
+	return filepath.Join(dir, k.Filename)
+}
+
+func requireEqualRepoFiles(tb testing.TB, folder, name, url string) {
+	tb.Helper()
+	dir := tb.TempDir()
+	_, err := git.Run("-C", dir, "clone", url, "repo")
+	require.NoError(tb, err)
+
+	for reponame, ext := range map[string]string{
+		"PKGBUILD": ".pkgbuild",
+		".SRCINFO": ".srcinfo",
+	} {
+		path := filepath.Join(folder, "aur", name+"-bin"+ext)
+		bts, err := os.ReadFile(path)
+		require.NoError(tb, err)
+		golden.RequireEqualExt(tb, bts, ext)
+
+		bts, err = os.ReadFile(filepath.Join(dir, "repo", reponame))
+		require.NoError(tb, err)
+		golden.RequireEqualExt(tb, bts, ext)
+	}
+}
diff --git a/internal/pipe/aur/testdata/TestFullPipe/default-gitlab.pkgbuild.golden b/internal/pipe/aur/testdata/TestFullPipe/default-gitlab.pkgbuild.golden
new file mode 100644
index 00000000..7c19c2bd
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullPipe/default-gitlab.pkgbuild.golden
@@ -0,0 +1,18 @@
+# This file was generated by GoReleaser. DO NOT EDIT.
+
+pkgname='default-gitlab-bin'
+pkgver=1.0.1
+pkgrel=1
+pkgdesc='A run pipe test fish food and FOO=foo_is_bar'
+url='https://gitlab.com/goreleaser'
+arch=('x86_64')
+license=('MIT')
+provides=('default-gitlab')
+conflicts=('default-gitlab')
+
+source_x86_64=('https://dummyhost/download/v1.0.1/bin.tar.gz')
+sha256sums_x86_64=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+package() {
+  install -Dm755 "./name" "${pkgdir}/usr/bin/name"
+}
diff --git a/internal/pipe/aur/testdata/TestFullPipe/default-gitlab.srcinfo.golden b/internal/pipe/aur/testdata/TestFullPipe/default-gitlab.srcinfo.golden
new file mode 100644
index 00000000..5dfd2637
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullPipe/default-gitlab.srcinfo.golden
@@ -0,0 +1,13 @@
+pkgbase = default-gitlab-bin
+	pkgdesc = A run pipe test fish food and FOO=foo_is_bar
+	pkgver = 1.0.1
+	pkgrel = 1
+	url = https://gitlab.com/goreleaser
+	license = MIT
+	conflicts = default-gitlab
+	provides = default-gitlab
+	arch = x86_64
+	source_x86_64 = https://dummyhost/download/v1.0.1/bin.tar.gz
+	sha256sums_x86_64 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	
+pkgname = default-gitlab-bin
diff --git a/internal/pipe/aur/testdata/TestFullPipe/default.pkgbuild.golden b/internal/pipe/aur/testdata/TestFullPipe/default.pkgbuild.golden
new file mode 100644
index 00000000..0d99bb19
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullPipe/default.pkgbuild.golden
@@ -0,0 +1,18 @@
+# This file was generated by GoReleaser. DO NOT EDIT.
+
+pkgname='default-bin'
+pkgver=1.0.1
+pkgrel=1
+pkgdesc='A run pipe test fish food and FOO=foo_is_bar'
+url='https://github.com/goreleaser'
+arch=('x86_64')
+license=('MIT')
+provides=('default')
+conflicts=('default')
+
+source_x86_64=('https://dummyhost/download/v1.0.1/bin.tar.gz')
+sha256sums_x86_64=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+package() {
+  install -Dm755 "./name" "${pkgdir}/usr/bin/name"
+}
diff --git a/internal/pipe/aur/testdata/TestFullPipe/default.srcinfo.golden b/internal/pipe/aur/testdata/TestFullPipe/default.srcinfo.golden
new file mode 100644
index 00000000..f75f1dc8
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullPipe/default.srcinfo.golden
@@ -0,0 +1,13 @@
+pkgbase = default-bin
+	pkgdesc = A run pipe test fish food and FOO=foo_is_bar
+	pkgver = 1.0.1
+	pkgrel = 1
+	url = https://github.com/goreleaser
+	license = MIT
+	conflicts = default
+	provides = default
+	arch = x86_64
+	source_x86_64 = https://dummyhost/download/v1.0.1/bin.tar.gz
+	sha256sums_x86_64 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	
+pkgname = default-bin
diff --git a/internal/pipe/aur/testdata/TestFullPipe/with-more-opts.pkgbuild.golden b/internal/pipe/aur/testdata/TestFullPipe/with-more-opts.pkgbuild.golden
new file mode 100644
index 00000000..d4ef3315
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullPipe/with-more-opts.pkgbuild.golden
@@ -0,0 +1,22 @@
+# This file was generated by GoReleaser. DO NOT EDIT.
+# Maintainer: me
+# Contributor: me as well
+
+pkgname='with-more-opts-bin'
+pkgver=1.0.1
+pkgrel=1
+pkgdesc='A run pipe test fish food and FOO=foo_is_bar'
+url='https://github.com/goreleaser'
+arch=('x86_64')
+license=('MIT')
+provides=('git' 'svn')
+conflicts=('libcurl' 'cvs' 'blah')
+depends=('curl' 'bash')
+optdepends=('wget: stuff' 'foo: bar')
+
+source_x86_64=('https://dummyhost/download/v1.0.1/bin.tar.gz')
+sha256sums_x86_64=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+package() {
+  install -Dm755 "./name" "${pkgdir}/usr/bin/name"
+}
diff --git a/internal/pipe/aur/testdata/TestFullPipe/with-more-opts.srcinfo.golden b/internal/pipe/aur/testdata/TestFullPipe/with-more-opts.srcinfo.golden
new file mode 100644
index 00000000..f946585d
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullPipe/with-more-opts.srcinfo.golden
@@ -0,0 +1,20 @@
+pkgbase = with-more-opts-bin
+	pkgdesc = A run pipe test fish food and FOO=foo_is_bar
+	pkgver = 1.0.1
+	pkgrel = 1
+	url = https://github.com/goreleaser
+	license = MIT
+	optdepends = wget: stuff
+	optdepends = foo: bar
+	depends = curl
+	depends = bash
+	conflicts = libcurl
+	conflicts = cvs
+	conflicts = blah
+	provides = git
+	provides = svn
+	arch = x86_64
+	source_x86_64 = https://dummyhost/download/v1.0.1/bin.tar.gz
+	sha256sums_x86_64 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	
+pkgname = with-more-opts-bin
diff --git a/internal/pipe/aur/testdata/TestFullPkgBuild.golden b/internal/pipe/aur/testdata/TestFullPkgBuild.golden
new file mode 100644
index 00000000..c1ce5ff5
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullPkgBuild.golden
@@ -0,0 +1,51 @@
+# This file was generated by GoReleaser. DO NOT EDIT.
+# Maintainer: Ciclano <ciclano@example.com>
+# Maintainer: Cicrano <cicrano@example.com>
+# Contributor: Fulano <fulano@example.com>
+# Contributor: Beltrano <beltrano@example.com>
+
+pkgname='test-bin'
+pkgver=0.1.3
+pkgrel=1
+pkgdesc='Some desc'
+url='https://example.com'
+arch=('x86_64' 'i686' 'aarch64' 'armv6h' 'armv7h')
+license=('MIT')
+provides=('test')
+conflicts=('nope')
+depends=('nope')
+optdepends=('nfpm')
+
+source_x86_64=('https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz')
+sha256sums_x86_64=('1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67')
+
+source_armv6h=('https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_Arm6.tar.gz')
+sha256sums_armv6h=('1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67')
+
+source_aarch64=('https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_Arm64.tar.gz')
+sha256sums_aarch64=('1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67')
+
+source_i686=('https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_386.tar.gz')
+sha256sums_i686=('1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67')
+
+source_armv7h=('https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_arm7.tar.gz')
+sha256sums_armv7h=('1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67')
+
+package() {
+  # bin
+  install -Dm755 "./goreleaser" "${pkgdir}/usr/bin/goreleaser"
+
+  # license
+  install -Dm644 "./LICENSE.md" "${pkgdir}/usr/share/licenses/goreleaser/LICENSE"
+
+  # completions
+  mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
+  mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
+  mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
+  install -Dm644 "./completions/goreleaser.bash" "${pkgdir}/usr/share/bash-completion/completions/goreleaser"
+  install -Dm644 "./completions/goreleaser.zsh" "${pkgdir}/usr/share/zsh/site-functions/_goreleaser"
+  install -Dm644 "./completions/goreleaser.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/goreleaser.fish"
+
+  # man pages
+  install -Dm644 "./manpages/goreleaser.1.gz" "${pkgdir}/usr/share/man/man1/goreleaser.1.gz"
+}
diff --git a/internal/pipe/aur/testdata/TestFullSrcInfo.golden b/internal/pipe/aur/testdata/TestFullSrcInfo.golden
new file mode 100644
index 00000000..6f3d86b3
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestFullSrcInfo.golden
@@ -0,0 +1,27 @@
+pkgbase = test-bin
+	pkgdesc = Some desc
+	pkgver = 0.1.3
+	pkgrel = 1
+	url = https://example.com
+	license = MIT
+	optdepends = nfpm
+	depends = nope
+	conflicts = nope
+	provides = test
+	arch = x86_64
+	source_x86_64 = https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_x86_64.tar.gz
+	sha256sums_x86_64 = 1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67
+	arch = armv6h
+	source_armv6h = https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_Arm6.tar.gz
+	sha256sums_armv6h = 1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67
+	arch = aarch64
+	source_aarch64 = https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_Arm64.tar.gz
+	sha256sums_aarch64 = 1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67
+	arch = i686
+	source_i686 = https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_386.tar.gz
+	sha256sums_i686 = 1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67
+	arch = armv7h
+	source_armv7h = https://github.com/caarlos0/test/releases/download/v0.1.3/test_Linux_arm7.tar.gz
+	sha256sums_armv7h = 1633f61598ab0791e213135923624eb342196b3494909c91899bcd0560f84c67
+	
+pkgname = test-bin
diff --git a/internal/pipe/aur/testdata/TestRunPipe.pkgbuild.golden b/internal/pipe/aur/testdata/TestRunPipe.pkgbuild.golden
new file mode 100644
index 00000000..34d3089c
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestRunPipe.pkgbuild.golden
@@ -0,0 +1,30 @@
+# This file was generated by GoReleaser. DO NOT EDIT.
+
+pkgname='foo-bin'
+pkgver=1.0.1
+pkgrel=1
+pkgdesc='A run pipe test pkgbuild and FOO=foo_is_bar'
+url='https://github.com/goreleaser'
+arch=('aarch64' 'armv6h' 'armv7h' 'i686' 'x86_64')
+license=('MIT')
+provides=('foo')
+conflicts=('foo')
+
+source_aarch64=('https://dummyhost/download/v1.0.1/arm64.tar.gz')
+sha256sums_aarch64=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+source_armv6h=('https://dummyhost/download/v1.0.1/armv6.tar.gz')
+sha256sums_armv6h=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+source_armv7h=('https://dummyhost/download/v1.0.1/armv7.tar.gz')
+sha256sums_armv7h=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+source_i686=('https://dummyhost/download/v1.0.1/bin.tar.gz')
+sha256sums_i686=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+source_x86_64=('https://dummyhost/download/v1.0.1/bin.tar.gz')
+sha256sums_x86_64=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+package() {
+  install -Dm755 "./foo" "${pkgdir}/usr/bin/foo"
+}
diff --git a/internal/pipe/aur/testdata/TestRunPipe.srcinfo.golden b/internal/pipe/aur/testdata/TestRunPipe.srcinfo.golden
new file mode 100644
index 00000000..8cc6caa0
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestRunPipe.srcinfo.golden
@@ -0,0 +1,25 @@
+pkgbase = foo-bin
+	pkgdesc = A run pipe test pkgbuild and FOO=foo_is_bar
+	pkgver = 1.0.1
+	pkgrel = 1
+	url = https://github.com/goreleaser
+	license = MIT
+	conflicts = foo
+	provides = foo
+	arch = aarch64
+	source_aarch64 = https://dummyhost/download/v1.0.1/arm64.tar.gz
+	sha256sums_aarch64 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	arch = armv6h
+	source_armv6h = https://dummyhost/download/v1.0.1/armv6.tar.gz
+	sha256sums_armv6h = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	arch = armv7h
+	source_armv7h = https://dummyhost/download/v1.0.1/armv7.tar.gz
+	sha256sums_armv7h = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	arch = i686
+	source_i686 = https://dummyhost/download/v1.0.1/bin.tar.gz
+	sha256sums_i686 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	arch = x86_64
+	source_x86_64 = https://dummyhost/download/v1.0.1/bin.tar.gz
+	sha256sums_x86_64 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	
+pkgname = foo-bin
diff --git a/internal/pipe/aur/testdata/TestRunPipeBinaryRelease.pkgbuild.golden b/internal/pipe/aur/testdata/TestRunPipeBinaryRelease.pkgbuild.golden
new file mode 100644
index 00000000..c8a311ae
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestRunPipeBinaryRelease.pkgbuild.golden
@@ -0,0 +1,18 @@
+# This file was generated by GoReleaser. DO NOT EDIT.
+
+pkgname='foo-bin'
+pkgver=1.2.1
+pkgrel=1
+pkgdesc=''
+url=''
+arch=('x86_64')
+license=('')
+provides=('foo')
+conflicts=('foo')
+
+source_x86_64=('https://dummyhost/download/v1.2.1/foo_linux_amd64')
+sha256sums_x86_64=('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
+
+package() {
+  install -Dm755 "./foo_linux_amd64 "${pkgdir}/usr/bin/foo"
+}
diff --git a/internal/pipe/aur/testdata/TestRunPipeBinaryRelease.srcinfo.golden b/internal/pipe/aur/testdata/TestRunPipeBinaryRelease.srcinfo.golden
new file mode 100644
index 00000000..8f07ed45
--- /dev/null
+++ b/internal/pipe/aur/testdata/TestRunPipeBinaryRelease.srcinfo.golden
@@ -0,0 +1,11 @@
+pkgbase = foo-bin
+	pkgdesc =
+	pkgver = 1.2.1
+	pkgrel = 1
+	conflicts = foo
+	provides = foo
+	arch = x86_64
+	source_x86_64 = https://dummyhost/download/v1.2.1/foo_linux_amd64
+	sha256sums_x86_64 = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+	
+pkgname = foo-bin