Repository: goreleaser/goreleaser
Pull request #2803 (2022-01-03 01:59:02)

fix: do not filter and sort github-native changelog
also refactored implementation and added more tests

refs #2802 

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2803?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2803](https://codecov.io/gh/goreleaser/goreleaser/pull/2803?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (9074dbe) into [main](https://codecov.io/gh/goreleaser/goreleaser/commit/bd33dc37ec450adde42e139a147e45e7a7ffde0f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (bd33dc3) will **increase** coverage by `0.01%`.
> The diff coverage is `95.00%`.

> :exclamation: Current head 9074dbe differs from pull request most recent head b1c043c. Consider uploading reports for the commit b1c043c to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2803/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/2803?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##             main    #2803      +/-   ##
==========================================
+ Coverage   84.91%   84.93%   +0.01%     
==========================================
  Files         106      106              
  Lines        8240     8250      +10     
==========================================
+ Hits         6997     7007      +10     
- Misses       1011     1012       +1     
+ Partials      232      231       -1     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2803?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [internal/pipe/changelog/changelog.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2803/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9jaGFuZ2Vsb2cvY2hhbmdlbG9nLmdv) | `90.47% <95.00%> (+0.39%)` | :arrow_up: |

------

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


Patch:
diff --git a/Dockerfile b/Dockerfile
index fab44fe3..ea70cc61 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.17.5-alpine
+FROM golang:1.17.6-alpine
 
 RUN apk add --no-cache bash \
 	curl \
diff --git a/internal/client/gitlab.go b/internal/client/gitlab.go
index d3d518b1..582744d5 100644
--- a/internal/client/gitlab.go
+++ b/internal/client/gitlab.go
@@ -377,19 +377,41 @@ func (c *gitlabClient) Upload(
 		projectID = ctx.Config.Release.GitLab.Owner + "/" + projectID
 	}
 
-	log.WithField("file", file.Name()).Debug("uploading file")
-	projectFile, _, err := c.client.Projects.UploadFile(
-		projectID,
-		file.Name(),
-		nil,
-	)
-	if err != nil {
-		return err
+	var baseLinkURL string
+	if ctx.Config.GitLabURLs.UsePackageRegistry {
+		log.WithField("file", file.Name()).Debug("uploading file as generic package")
+		_, _, err := c.client.GenericPackages.PublishPackageFile(
+			projectID,
+			ctx.Config.ProjectName,
+			ctx.Version,
+			artifact.Name,
+			file,
+			nil,
+		)
+		if err != nil {
+			return err
+		}
+		baseLinkURL, err = c.client.GenericPackages.FormatPackageURL(
+			projectID, ctx.Config.ProjectName, ctx.Version, artifact.Name)
+		if err != nil {
+			return err
+		}
+	} else {
+		log.WithField("file", file.Name()).Debug("uploading file as attachment")
+		projectFile, _, err := c.client.Projects.UploadFile(
+			projectID,
+			file.Name(),
+			nil,
+		)
+		if err != nil {
+			return err
+		}
+		baseLinkURL = projectFile.URL
 	}
 
 	log.WithFields(log.Fields{
 		"file": file.Name(),
-		"url":  projectFile.URL,
+		"url":  baseLinkURL,
 	}).Debug("uploaded file")
 
 	// search for project details based on projectID
@@ -398,14 +420,18 @@ func (c *gitlabClient) Upload(
 		return err
 	}
 
-	gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
-	if err != nil {
-		return fmt.Errorf("templating GitLab Download URL: %w", err)
-	}
-
-	linkURL := gitlabBaseURL + "/" + projectDetails.PathWithNamespace + projectFile.URL
 	name := artifact.Name
 	filename := "/" + name
+	var linkURL string
+	if ctx.Config.GitLabURLs.UsePackageRegistry {
+		linkURL = c.client.BaseURL().String() + baseLinkURL
+	} else {
+		gitlabBaseURL, err := tmpl.New(ctx).Apply(ctx.Config.GitLabURLs.Download)
+		if err != nil {
+			return fmt.Errorf("templating GitLab Download URL: %w", err)
+		}
+		linkURL = gitlabBaseURL + "/" + projectDetails.PathWithNamespace + baseLinkURL
+	}
 	releaseLink, _, err := c.client.ReleaseLinks.CreateReleaseLink(
 		projectID,
 		releaseID,
diff --git a/internal/exec/exec.go b/internal/exec/exec.go
index 335dbfeb..3f6d773b 100644
--- a/internal/exec/exec.go
+++ b/internal/exec/exec.go
@@ -11,6 +11,7 @@ import (
 	"github.com/apex/log"
 	"github.com/caarlos0/go-shellwords"
 	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/extrafiles"
 	"github.com/goreleaser/goreleaser/internal/gio"
 	"github.com/goreleaser/goreleaser/internal/logext"
 	"github.com/goreleaser/goreleaser/internal/pipe"
@@ -43,6 +44,20 @@ func Execute(ctx *context.Context, publishers []config.Publisher) error {
 func executePublisher(ctx *context.Context, publisher config.Publisher) error {
 	log.Debugf("filtering %d artifacts", len(ctx.Artifacts.List()))
 	artifacts := filterArtifacts(ctx.Artifacts, publisher)
+
+	extraFiles, err := extrafiles.Find(ctx, publisher.ExtraFiles)
+	if err != nil {
+		return err
+	}
+
+	for name, path := range extraFiles {
+		artifacts = append(artifacts, &artifact.Artifact{
+			Name: name,
+			Path: path,
+			Type: artifact.UploadableFile,
+		})
+	}
+
 	log.Debugf("will execute custom publisher with %d artifacts", len(artifacts))
 
 	g := semerrgroup.New(ctx.Parallelism)
diff --git a/internal/pipe/changelog/changelog.go b/internal/pipe/changelog/changelog.go
index 84bb28f9..15cb2c30 100644
--- a/internal/pipe/changelog/changelog.go
+++ b/internal/pipe/changelog/changelog.go
@@ -14,7 +14,6 @@ import (
 	"github.com/goreleaser/goreleaser/internal/client"
 	"github.com/goreleaser/goreleaser/internal/git"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
-	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
@@ -23,6 +22,19 @@ var ErrInvalidSortDirection = errors.New("invalid sort direction")
 
 const li = "* "
 
+type useChangelog string
+
+func (u useChangelog) formatable() bool {
+	return u != "github-native"
+}
+
+const (
+	useGit          = "git"
+	useGitHub       = "github"
+	useGitLab       = "gitlab"
+	useGitHubNative = "github-native"
+)
+
 // Pipe for checksums.
 type Pipe struct{}
 
@@ -60,55 +72,11 @@ func (Pipe) Run(ctx *context.Context) error {
 		return err
 	}
 
-	changelogStringJoiner := "\n"
-	if ctx.TokenType == context.TokenTypeGitLab || ctx.TokenType == context.TokenTypeGitea {
-		// We need two or more whitespace to let markdown interpret
-		// it as newline. See https://docs.gitlab.com/ee/user/markdown.html#newlines for details
-		log.Debug("is gitlab or gitea changelog")
-		changelogStringJoiner = "   \n"
-	}
-
-	changelogElements := []string{
-		"## Changelog",
-	}
-
-	if ctx.Config.Changelog.Use == "github-native" {
-		changelogElements = []string{strings.Join(entries, changelogStringJoiner)}
-	} else if shouldGroup(ctx.Config.Changelog) {
-		log.Debug("grouping entries")
-		groups := ctx.Config.Changelog.Groups
-
-		sort.Slice(groups, func(i, j int) bool { return groups[i].Order < groups[j].Order })
-		for _, group := range groups {
-			items := make([]string, 0)
-			if group.Regexp == "" {
-				// If no regexp is provided, we purge all strikethrough entries and add remaining entries to the list
-				items = getAllNonEmpty(entries)
-				// clear array
-				entries = nil
-			} else {
-				regex, err := regexp.Compile(group.Regexp)
-				if err != nil {
-					return fmt.Errorf("failed to group into %q: %w", group.Title, err)
-				}
-				for i, entry := range entries {
-					match := regex.MatchString(entry)
-					if match {
-						items = append(items, li+entry)
-						// Striking out the matched entry
-						entries[i] = ""
-					}
-				}
-			}
-			if len(items) > 0 {
-				changelogElements = append(changelogElements, fmt.Sprintf("### %s", group.Title))
-				changelogElements = append(changelogElements, strings.Join(items, changelogStringJoiner))
-			}
-		}
-	} else {
-		log.Debug("not grouping entries")
-		changelogElements = append(changelogElements, strings.Join(getAllNonEmpty(entries), changelogStringJoiner))
+	changes, err := formatChangelog(ctx, entries)
+	if err != nil {
+		return err
 	}
+	changelogElements := []string{changes}
 
 	if header != "" {
 		changelogElements = append([]string{header}, changelogElements...)
@@ -127,11 +95,60 @@ func (Pipe) Run(ctx *context.Context) error {
 	return os.WriteFile(path, []byte(ctx.ReleaseNotes), 0o644) //nolint: gosec
 }
 
-func shouldGroup(cfg config.Changelog) bool {
-	return len(cfg.Groups) > 0 && cfg.Use != "github-native"
+func formatChangelog(ctx *context.Context, entries []string) (string, error) {
+	newLine := "\n"
+	if ctx.TokenType == context.TokenTypeGitLab || ctx.TokenType == context.TokenTypeGitea {
+		// We need two or more whitespace to let markdown interpret
+		// it as newline. See https://docs.gitlab.com/ee/user/markdown.html#newlines for details
+		log.Debug("is gitlab or gitea changelog")
+		newLine = "   \n"
+	}
+
+	if !useChangelog(ctx.Config.Changelog.Use).formatable() {
+		return strings.Join(entries, newLine), nil
+	}
+
+	result := []string{"## Changelog"}
+	if len(ctx.Config.Changelog.Groups) == 0 {
+		log.Debug("not grouping entries")
+		return strings.Join(append(result, filterAndPrefixItems(entries)...), newLine), nil
+	}
+
+	log.Debug("grouping entries")
+	groups := ctx.Config.Changelog.Groups
+
+	sort.Slice(groups, func(i, j int) bool { return groups[i].Order < groups[j].Order })
+	for _, group := range groups {
+		items := make([]string, 0)
+		if group.Regexp == "" {
+			// If no regexp is provided, we purge all strikethrough entries and add remaining entries to the list
+			items = filterAndPrefixItems(entries)
+			// clear array
+			entries = nil
+		} else {
+			regex, err := regexp.Compile(group.Regexp)
+			if err != nil {
+				return "", fmt.Errorf("failed to group into %q: %w", group.Title, err)
+			}
+			for i, entry := range entries {
+				match := regex.MatchString(entry)
+				if match {
+					items = append(items, li+entry)
+					// Striking out the matched entry
+					entries[i] = ""
+				}
+			}
+		}
+		if len(items) > 0 {
+			result = append(result, fmt.Sprintf("### %s", group.Title))
+			result = append(result, items...)
+		}
+	}
+
+	return strings.Join(result, newLine), nil
 }
 
-func getAllNonEmpty(ss []string) []string {
+func filterAndPrefixItems(ss []string) []string {
 	var r []string
 	for _, s := range ss {
 		if s != "" {
@@ -171,6 +188,9 @@ func buildChangelog(ctx *context.Context) ([]string, error) {
 	if lastLine := entries[len(entries)-1]; strings.TrimSpace(lastLine) == "" {
 		entries = entries[0 : len(entries)-1]
 	}
+	if !useChangelog(ctx.Config.Changelog.Use).formatable() {
+		return entries, nil
+	}
 	entries, err = filterEntries(ctx, entries)
 	if err != nil {
 		return entries, err
@@ -243,15 +263,15 @@ func doGetChangelog(ctx *context.Context, prev, tag string) (string, error) {
 
 func getChangeloger(ctx *context.Context) (changeloger, error) {
 	switch ctx.Config.Changelog.Use {
-	case "git":
+	case useGit:
 		fallthrough
 	case "":
 		return gitChangeloger{}, nil
-	case "github":
+	case useGitHub:
 		fallthrough
-	case "gitlab":
+	case useGitLab:
 		return newSCMChangeloger(ctx)
-	case "github-native":
+	case useGitHubNative:
 		return newGithubChangeloger(ctx)
 	default:
 		return nil, fmt.Errorf("invalid changelog.use: %q", ctx.Config.Changelog.Use)
diff --git a/internal/pipe/universalbinary/universalbinary.go b/internal/pipe/universalbinary/universalbinary.go
index b7638f05..1654270b 100644
--- a/internal/pipe/universalbinary/universalbinary.go
+++ b/internal/pipe/universalbinary/universalbinary.go
@@ -34,6 +34,9 @@ func (Pipe) Default(ctx *context.Context) error {
 		if unibin.ID == "" {
 			unibin.ID = ctx.Config.ProjectName
 		}
+		if len(unibin.IDs) == 0 {
+			unibin.IDs = []string{unibin.ID}
+		}
 		if unibin.NameTemplate == "" {
 			unibin.NameTemplate = "{{ .ProjectName }}"
 		}
@@ -228,6 +231,6 @@ func filterFor(unibin config.UniversalBinary) artifact.Filter {
 	return artifact.And(
 		artifact.ByType(artifact.Binary),
 		artifact.ByGoos("darwin"),
-		artifact.ByIDs(unibin.ID),
+		artifact.ByIDs(unibin.IDs...),
 	)
 }
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7b19ff63..13be2014 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -25,9 +25,10 @@ type GitHubURLs struct {
 
 // GitLabURLs holds the URLs to be used when using gitlab ce/enterprise.
 type GitLabURLs struct {
-	API           string `yaml:"api,omitempty"`
-	Download      string `yaml:"download,omitempty"`
-	SkipTLSVerify bool   `yaml:"skip_tls_verify,omitempty"`
+	API                string `yaml:"api,omitempty"`
+	Download           string `yaml:"download,omitempty"`
+	SkipTLSVerify      bool   `yaml:"skip_tls_verify,omitempty"`
+	UsePackageRegistry bool   `yaml:"use_package_registry,omitempty"`
 }
 
 // GiteaURLs holds the URLs to be used when using gitea.
@@ -434,7 +435,8 @@ func (f File) JSONSchemaType() *jsonschema.Type {
 
 // UniversalBinary setups macos universal binaries.
 type UniversalBinary struct {
-	ID           string          `yaml:"id,omitempty"`
+	ID           string          `yaml:"id,omitempty"` // deprecated
+	IDs          []string        `yaml:"ids,omitempty"`
 	NameTemplate string          `yaml:"name_template,omitempty"`
 	Replace      bool            `yaml:"replace,omitempty"`
 	Hooks        BuildHookConfig `yaml:"hooks,omitempty"`
@@ -799,13 +801,14 @@ type Upload struct {
 
 // Publisher configuration.
 type Publisher struct {
-	Name      string   `yaml:"name,omitempty"`
-	IDs       []string `yaml:"ids,omitempty"`
-	Checksum  bool     `yaml:"checksum,omitempty"`
-	Signature bool     `yaml:"signature,omitempty"`
-	Dir       string   `yaml:"dir,omitempty"`
-	Cmd       string   `yaml:"cmd,omitempty"`
-	Env       []string `yaml:"env,omitempty"`
+	Name       string      `yaml:"name,omitempty"`
+	IDs        []string    `yaml:"ids,omitempty"`
+	Checksum   bool        `yaml:"checksum,omitempty"`
+	Signature  bool        `yaml:"signature,omitempty"`
+	Dir        string      `yaml:"dir,omitempty"`
+	Cmd        string      `yaml:"cmd,omitempty"`
+	Env        []string    `yaml:"env,omitempty"`
+	ExtraFiles []ExtraFile `yaml:"extra_files,omitempty"`
 }
 
 // Source configuration.
diff --git a/www/docs/ci/azurepipelines.md b/www/docs/ci/azurepipelines.md
index 7f8e356f..4fc57498 100644
--- a/www/docs/ci/azurepipelines.md
+++ b/www/docs/ci/azurepipelines.md
@@ -24,7 +24,7 @@ Following inputs can be used
 | `version`**¹**   | String  | `latest`     | GoReleaser version                                               |
 | `args`           | String  |              | Arguments to pass to GoReleaser                                  |
 | `workdir`        | String  | `$(Build.SourcesDirectory)`          | Working directory (below repository root)                        |
-| `install-only`   | Bool    | `false`      | Just install GoReleaser                                          |
+| `installOnly`   | Bool    | `false`      | Just install GoReleaser                                          |
 
 > **¹** Can be a fixed version like `v0.132.0` or a max satisfying semver one like `~> 0.132`. In this case this will return `v0.132.1`.
 > For the `pro` version, add `-pro` to the string
diff --git a/www/docs/customization/publishers.md b/www/docs/customization/publishers.md
index 34fd8fcc..3a97fd1d 100644
--- a/www/docs/customization/publishers.md
+++ b/www/docs/customization/publishers.md
@@ -114,6 +114,19 @@ publishers:
     # Environment variables
     env:
       - API_TOKEN=secret-token
+
+    # You can publish extra pre-existing files.
+    # The filename published will be the last part of the path (base).
+    # If another file with the same name exists, the last one found will be used.
+    # These globs can also include templates.
+    #
+    # Defaults to empty.
+    extra_files:
+      - glob: ./path/to/file.txt
+      - glob: ./glob/**/to/**/file/**/*
+      - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
+      - glob: ./single_file.txt
+        name_template: file.txt # note that this only works if glob matches 1 file only
 ```
 
 These settings should allow you to push your artifacts to any number of endpoints
diff --git a/www/docs/customization/universalbinaries.md b/www/docs/customization/universalbinaries.md
index 51b39545..01337e77 100644
--- a/www/docs/customization/universalbinaries.md
+++ b/www/docs/customization/universalbinaries.md
@@ -9,11 +9,18 @@ Here's how to use it:
 # .goreleaser.yaml
 universal_binaries:
 -
-  # ID of the source build
+  # ID of resulting universal binary.
   #
   # Defaults to the project name.
   id: foo
 
+  # IDs to use to filter the built binaries.
+  #
+  # Defaults to the `id` field.
+  ids:
+  - build1
+  - build2
+
   # Universal binary name template.
   #
   # You will want to change this if you have multiple builds!
diff --git a/www/docs/scm/gitlab.md b/www/docs/scm/gitlab.md
index 20c6f716..6ff1d6f7 100644
--- a/www/docs/scm/gitlab.md
+++ b/www/docs/scm/gitlab.md
@@ -28,6 +28,9 @@ gitlab_urls:
   download: https://gitlab.company.com
   # set to true if you use a self-signed certificate
   skip_tls_verify: false
+  # set to true if you want to upload to the Package Registry rather than attachments
+  # Only works with GitLab 13.5+
+  use_package_registry: false
 ```
 
 If none are set, they default to GitLab's public URLs.
@@ -37,6 +40,19 @@ If none are set, they default to GitLab's public URLs.
     on [release](https://docs.gitlab.com/ee/user/project/releases/index.html) functionality
     and [direct asset linking](https://docs.gitlab.com/ee/user/project/releases/index.html#permanent-links-to-release-assets).
 
+## Generic Package Registry
+
+GitLab introduced the [Generic Package Registry](https://docs.gitlab.com/ee/user/packages/package_registry/index.html) in Gitlab 13.5.
+
+Normally, `goreleaser` uploads release files as "attachments", which may have [administrative limits](https://docs.gitlab.com/ee/user/admin_area/settings/account_and_limit_settings.html).  Notably, hosted gitlab.com instances have a 10MB attachment limit which cannot be changed.
+
+Uploading to the Generic Package Registry does not have this restriction.  To use it instead, set `use_package_registry` to `true`.
+
+```yaml
+# .goreleaser.yml
+gitlab_urls:
+  use_package_registry: true
+```
 
 ## Example release
 
diff --git a/www/docs/static/schema-pro.json b/www/docs/static/schema-pro.json
index abe0b882..8c438221 100644
--- a/www/docs/static/schema-pro.json
+++ b/www/docs/static/schema-pro.json
@@ -1814,6 +1814,12 @@
 							"type": "string"
 						},
 						"type": "array"
+					},
+					"extra_files": {
+						"items": {
+							"$ref": "#/definitions/ExtraFile"
+						},
+						"type": "array"
 					}
 				},
 				"additionalProperties": false,
diff --git a/www/docs/static/schema.json b/www/docs/static/schema.json
index a6ba680b..73a97510 100644
--- a/www/docs/static/schema.json
+++ b/www/docs/static/schema.json
@@ -702,6 +702,9 @@
 					},
 					"skip_tls_verify": {
 						"type": "boolean"
+					},
+					"use_package_registry": {
+						"type": "boolean"
 					}
 				},
 				"additionalProperties": false,
@@ -1652,6 +1655,12 @@
 							"type": "string"
 						},
 						"type": "array"
+					},
+					"extra_files": {
+						"items": {
+							"$ref": "#/definitions/ExtraFile"
+						},
+						"type": "array"
 					}
 				},
 				"additionalProperties": false,
@@ -2210,6 +2219,12 @@
 					"id": {
 						"type": "string"
 					},
+					"ids": {
+						"items": {
+							"type": "string"
+						},
+						"type": "array"
+					},
 					"name_template": {
 						"type": "string"
 					},


Test patch:
diff --git a/internal/exec/exec_test.go b/internal/exec/exec_test.go
index 10ee698e..29f6f714 100644
--- a/internal/exec/exec_test.go
+++ b/internal/exec/exec_test.go
@@ -202,6 +202,59 @@ func TestExecute(t *testing.T) {
 			},
 			nil,
 		},
+		{
+			"extra files",
+			[]config.Publisher{
+				{
+					Name: "test",
+					Cmd:  MockCmd + " {{ .ArtifactName }}",
+					Env: []string{
+						MarshalMockEnv(&MockData{
+							AnyOf: []MockCall{
+								{ExpectedArgs: []string{"a.deb"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"a.ubi"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"a.tar"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"a.txt"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"foo/bar"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"foo/bar:amd64"}, ExitCode: 0, ExpectedEnv: osEnv()},
+							},
+						}),
+					},
+					ExtraFiles: []config.ExtraFile{
+						{Glob: filepath.Join("testdata", "*.txt")},
+					},
+				},
+			},
+			nil,
+		},
+		{
+			"extra files with rename",
+			[]config.Publisher{
+				{
+					Name: "test",
+					Cmd:  MockCmd + " {{ .ArtifactName }}",
+					Env: []string{
+						MarshalMockEnv(&MockData{
+							AnyOf: []MockCall{
+								{ExpectedArgs: []string{"a.deb"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"a.ubi"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"a.tar"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"b.txt"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"foo/bar"}, ExitCode: 0, ExpectedEnv: osEnv()},
+								{ExpectedArgs: []string{"foo/bar:amd64"}, ExitCode: 0, ExpectedEnv: osEnv()},
+							},
+						}),
+					},
+					ExtraFiles: []config.ExtraFile{
+						{
+							Glob:         filepath.Join("testdata", "*.txt"),
+							NameTemplate: "b.txt",
+						},
+					},
+				},
+			},
+			nil,
+		},
 		{
 			"try dir templating",
 			[]config.Publisher{
diff --git a/internal/exec/testdata/a.txt b/internal/exec/testdata/a.txt
new file mode 100644
index 00000000..34c92c2d
--- /dev/null
+++ b/internal/exec/testdata/a.txt
@@ -0,0 +1,1 @@
+lorem ipsum
\ No newline at end of file
diff --git a/internal/pipe/changelog/changelog_test.go b/internal/pipe/changelog/changelog_test.go
index fd9c91f0..fd7f6c27 100644
--- a/internal/pipe/changelog/changelog_test.go
+++ b/internal/pipe/changelog/changelog_test.go
@@ -69,6 +69,7 @@ func TestChangelog(t *testing.T) {
 	ctx := context.New(config.Project{
 		Dist: folder,
 		Changelog: config.Changelog{
+			Use: "git",
 			Filters: config.Filters{
 				Exclude: []string{
 					"docs:",
@@ -419,7 +420,7 @@ func TestChangeLogWithoutReleaseFooter(t *testing.T) {
 func TestGetChangelogGitHub(t *testing.T) {
 	ctx := context.New(config.Project{
 		Changelog: config.Changelog{
-			Use: "github",
+			Use: useGitHub,
 		},
 	})
 
@@ -441,11 +442,16 @@ func TestGetChangelogGitHub(t *testing.T) {
 func TestGetChangelogGitHubNative(t *testing.T) {
 	ctx := context.New(config.Project{
 		Changelog: config.Changelog{
-			Use: "github-native",
+			Use: useGitHubNative,
 		},
 	})
 
-	expected := "**Full Changelog**: https://github.com/gorelease/goreleaser/compare/v0.180.1...v0.180.2"
+	expected := `## What's changed
+
+* Foo bar test
+
+**Full Changelog**: https://github.com/gorelease/goreleaser/compare/v0.180.1...v0.180.2
+`
 	mock := client.NewMock()
 	mock.ReleaseNotes = expected
 	l := githubNativeChangeloger{
@@ -467,20 +473,20 @@ func TestGetChangeloger(t *testing.T) {
 		require.IsType(t, c, gitChangeloger{})
 	})
 
-	t.Run("git", func(t *testing.T) {
+	t.Run(useGit, func(t *testing.T) {
 		c, err := getChangeloger(context.New(config.Project{
 			Changelog: config.Changelog{
-				Use: "git",
+				Use: useGit,
 			},
 		}))
 		require.NoError(t, err)
 		require.IsType(t, c, gitChangeloger{})
 	})
 
-	t.Run("github", func(t *testing.T) {
+	t.Run(useGitHub, func(t *testing.T) {
 		ctx := context.New(config.Project{
 			Changelog: config.Changelog{
-				Use: "github",
+				Use: useGitHub,
 			},
 		})
 		ctx.TokenType = context.TokenTypeGitHub
@@ -489,10 +495,10 @@ func TestGetChangeloger(t *testing.T) {
 		require.IsType(t, c, &scmChangeloger{})
 	})
 
-	t.Run("github-native", func(t *testing.T) {
+	t.Run(useGitHubNative, func(t *testing.T) {
 		ctx := context.New(config.Project{
 			Changelog: config.Changelog{
-				Use: "github-native",
+				Use: useGitHubNative,
 			},
 		})
 		ctx.TokenType = context.TokenTypeGitHub
@@ -501,10 +507,10 @@ func TestGetChangeloger(t *testing.T) {
 		require.IsType(t, c, &githubNativeChangeloger{})
 	})
 
-	t.Run("gitlab", func(t *testing.T) {
+	t.Run(useGitLab, func(t *testing.T) {
 		ctx := context.New(config.Project{
 			Changelog: config.Changelog{
-				Use: "gitlab",
+				Use: useGitLab,
 			},
 		})
 		ctx.TokenType = context.TokenTypeGitLab
@@ -617,30 +623,84 @@ func TestGroupBadRegex(t *testing.T) {
 	require.EqualError(t, Pipe{}.Run(ctx), `failed to group into "Something": error parsing regexp: missing closing ]: `+"`"+`[(\w`+"`")
 }
 
-func TestShouldGroup(t *testing.T) {
-	t.Run("with groups", func(t *testing.T) {
-		t.Run("github-native", func(t *testing.T) {
-			require.False(t, shouldGroup(config.Changelog{
-				Use:    "github-native",
-				Groups: []config.ChangeLogGroup{{}},
-			}))
-		})
-		for _, u := range []string{"git", "github", "gitlab"} {
-			t.Run(u, func(t *testing.T) {
-				require.True(t, shouldGroup(config.Changelog{
-					Use:    u,
-					Groups: []config.ChangeLogGroup{{}},
-				}))
+func TestChangelogFormat(t *testing.T) {
+	t.Run("without groups", func(t *testing.T) {
+		makeConf := func(u string) config.Project {
+			return config.Project{Changelog: config.Changelog{Use: u}}
+		}
+
+		for _, use := range []string{useGit, useGitHub, useGitLab} {
+			t.Run(use, func(t *testing.T) {
+				out, err := formatChangelog(
+					context.New(makeConf(use)),
+					[]string{
+						"aea123 foo",
+						"aef653 bar",
+					},
+				)
+				require.NoError(t, err)
+				require.Equal(t, `## Changelog
+* aea123 foo
+* aef653 bar`, out)
 			})
 		}
+
+		t.Run(useGitHubNative, func(t *testing.T) {
+			out, err := formatChangelog(
+				context.New(makeConf(useGitHubNative)),
+				[]string{
+					"# What's changed",
+					"* aea123 foo",
+					"* aef653 bar",
+				},
+			)
+			require.NoError(t, err)
+			require.Equal(t, `# What's changed
+* aea123 foo
+* aef653 bar`, out)
+		})
 	})
 
-	t.Run("without groups", func(t *testing.T) {
-		for _, u := range []string{"git", "github", "gitlab", "github-native"} {
-			t.Run(u, func(t *testing.T) {
-				require.False(t, shouldGroup(config.Changelog{
+	t.Run("with groups", func(t *testing.T) {
+		makeConf := func(u string) config.Project {
+			return config.Project{
+				Changelog: config.Changelog{
 					Use: u,
-				}))
+					Groups: []config.ChangeLogGroup{
+						{Title: "catch-all"},
+					},
+				},
+			}
+		}
+
+		t.Run(useGitHubNative, func(t *testing.T) {
+			out, err := formatChangelog(
+				context.New(makeConf(useGitHubNative)),
+				[]string{
+					"# What's changed",
+					"* aea123 foo",
+					"* aef653 bar",
+				},
+			)
+			require.NoError(t, err)
+			require.Equal(t, `# What's changed
+* aea123 foo
+* aef653 bar`, out)
+		})
+		for _, use := range []string{useGit, useGitHub, useGitLab} {
+			t.Run(use, func(t *testing.T) {
+				out, err := formatChangelog(
+					context.New(makeConf(use)),
+					[]string{
+						"aea123 foo",
+						"aef653 bar",
+					},
+				)
+				require.NoError(t, err)
+				require.Equal(t, `## Changelog
+### catch-all
+* aea123 foo
+* aef653 bar`, out)
 			})
 		}
 	})
diff --git a/internal/pipe/universalbinary/universalbinary_test.go b/internal/pipe/universalbinary/universalbinary_test.go
index 17aca2df..ec81a132 100644
--- a/internal/pipe/universalbinary/universalbinary_test.go
+++ b/internal/pipe/universalbinary/universalbinary_test.go
@@ -32,6 +32,24 @@ func TestDefault(t *testing.T) {
 		require.NoError(t, Pipe{}.Default(ctx))
 		require.Equal(t, config.UniversalBinary{
 			ID:           "proj",
+			IDs:          []string{"proj"},
+			NameTemplate: "{{ .ProjectName }}",
+		}, ctx.Config.UniversalBinaries[0])
+	})
+
+	t.Run("given ids", func(t *testing.T) {
+		ctx := &context.Context{
+			Config: config.Project{
+				ProjectName: "proj",
+				UniversalBinaries: []config.UniversalBinary{
+					{IDs: []string{"foo"}},
+				},
+			},
+		}
+		require.NoError(t, Pipe{}.Default(ctx))
+		require.Equal(t, config.UniversalBinary{
+			ID:           "proj",
+			IDs:          []string{"foo"},
 			NameTemplate: "{{ .ProjectName }}",
 		}, ctx.Config.UniversalBinaries[0])
 	})
@@ -48,6 +66,7 @@ func TestDefault(t *testing.T) {
 		require.NoError(t, Pipe{}.Default(ctx))
 		require.Equal(t, config.UniversalBinary{
 			ID:           "foo",
+			IDs:          []string{"foo"},
 			NameTemplate: "{{ .ProjectName }}",
 		}, ctx.Config.UniversalBinaries[0])
 	})
@@ -64,6 +83,7 @@ func TestDefault(t *testing.T) {
 		require.NoError(t, Pipe{}.Default(ctx))
 		require.Equal(t, config.UniversalBinary{
 			ID:           "proj",
+			IDs:          []string{"proj"},
 			NameTemplate: "foo",
 		}, ctx.Config.UniversalBinaries[0])
 	})
@@ -111,6 +131,7 @@ func TestRun(t *testing.T) {
 		UniversalBinaries: []config.UniversalBinary{
 			{
 				ID:           "foo",
+				IDs:          []string{"foo"},
 				NameTemplate: "foo",
 				Replace:      true,
 			},
@@ -123,6 +144,7 @@ func TestRun(t *testing.T) {
 		UniversalBinaries: []config.UniversalBinary{
 			{
 				ID:           "foo",
+				IDs:          []string{"foo"},
 				NameTemplate: "foo",
 			},
 		},
@@ -133,6 +155,7 @@ func TestRun(t *testing.T) {
 		UniversalBinaries: []config.UniversalBinary{
 			{
 				ID:           "notfoo",
+				IDs:          []string{"notfoo"},
 				NameTemplate: "notfoo",
 			},
 		},
@@ -143,6 +166,7 @@ func TestRun(t *testing.T) {
 		UniversalBinaries: []config.UniversalBinary{
 			{
 				ID:           "foo",
+				IDs:          []string{"foo"},
 				NameTemplate: "foo",
 			},
 		},
@@ -153,6 +177,7 @@ func TestRun(t *testing.T) {
 		UniversalBinaries: []config.UniversalBinary{
 			{
 				ID:           "foo",
+				IDs:          []string{"foo"},
 				NameTemplate: "foo",
 				Hooks: config.BuildHookConfig{
 					Pre: []config.Hook{