Repository: goreleaser/goreleaser
Pull request #942 (2019-01-27 20:08:57)

feat: multiple archives
this is just the first step... not all features will work...

the idea is to have `ids` on things...  unfortunately this does not look natural on YAML, and would probably look better on HCL...

anyway, for now, you can have several builds:

```yaml
builds:
- id: foo
- id: bar
```

and then the archives reference the builds:

```yaml
archives:
- id: 'foobar' 
  builds: ['foo', 'bar']
```

later, when we have multiple everything-else, the user will reference the archive ids.

By default, all ids are set to `default`, this still requires some work though...

refs #705

syntaqx: Would be cool to see backwards-compatibility in the form of many/one marshalability:

```go
type Archives []Archive

func (a *Archives) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var archives []Archive
	if err := unmarshal(&archives); err != nil {
		var archive Archive
		if err := unmarshal(&archive); err != nil {
			return err
		}
		archives = []Archive{archive}
	}

	*a = archives
	return nil
}
```

Then you can allow for the `archive` or `archives` key to be used as the same value, providing people flexibility in their usage. If they only want a single archive, being able to use the singular verb without an array is nice, as well as having both keys behave identically.
stale[bot]: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

codecov-io: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/942?src=pr&el=h1) Report
> Merging [#942](https://codecov.io/gh/goreleaser/goreleaser/pull/942?src=pr&el=desc) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/3d74b1fe8eefbba6b9cb8e1143e9d56f1c3b1c17?src=pr&el=desc) will **increase** coverage by `0.13%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/942/graphs/tree.svg?width=650&token=3bJvv6b7hN&height=150&src=pr)](https://codecov.io/gh/goreleaser/goreleaser/pull/942?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #942      +/-   ##
==========================================
+ Coverage   88.17%   88.31%   +0.13%     
==========================================
  Files          52       52              
  Lines        2528     2558      +30     
==========================================
+ Hits         2229     2259      +30     
  Misses        236      236              
  Partials       63       63
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/942?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/942/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `100% <ø> (ø)` | :arrow_up: |
| [internal/pipe/build/build.go](https://codecov.io/gh/goreleaser/goreleaser/pull/942/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9idWlsZC9idWlsZC5nbw==) | `97.56% <100%> (ø)` | :arrow_up: |
| [internal/pipe/archive/archive.go](https://codecov.io/gh/goreleaser/goreleaser/pull/942/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9hcmNoaXZlL2FyY2hpdmUuZ28=) | `96.22% <100%> (+0.6%)` | :arrow_up: |
| [internal/artifact/artifact.go](https://codecov.io/gh/goreleaser/goreleaser/pull/942/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYXJ0aWZhY3QvYXJ0aWZhY3QuZ28=) | `82.4% <100%> (+1.4%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/942?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/942?src=pr&el=footer). Last update [3d74b1f...d845ea0](https://codecov.io/gh/goreleaser/goreleaser/pull/942?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

caarlos0: FWIW: I plan to merge and release this next weekend.

It will be a soft release, so nothing should be affected. 

After that I'll start working on the other pipes to support this, and once I'm done, finally tag and release 1.0.0 :) 
frapposelli: This is great, thanks @caarlos0

Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index b8323fbb..e5bb5e59 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -61,17 +61,18 @@ dockers:
 
   extra_files:
   - scripts/entrypoint.sh
-archive:
-  name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
-  replacements:
-    darwin: Darwin
-    linux: Linux
-    windows: Windows
-    386: i386
-    amd64: x86_64
-  format_overrides:
-  - goos: windows
-    format: zip
+archives:
+  -
+    name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
+    replacements:
+      darwin: Darwin
+      linux: Linux
+      windows: Windows
+      386: i386
+      amd64: x86_64
+    format_overrides:
+    - goos: windows
+      format: zip
 brew:
   github:
     owner: goreleaser
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index 0ece17bb..32906934 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -193,6 +193,18 @@ func ByType(t Type) Filter {
 	}
 }
 
+// ByIDs filter artifacts by an `ID` extra field.
+func ByIDs(ids ...string) Filter {
+	var filters = make([]Filter, 0, len(ids))
+	for _, id := range ids {
+		id := id
+		filters = append(filters, func(a Artifact) bool {
+			return a.ExtraOr("ID", "") == id
+		})
+	}
+	return Or(filters...)
+}
+
 // Or performs an OR between all given filters
 func Or(filters ...Filter) Filter {
 	return func(a Artifact) bool {
diff --git a/internal/pipe/archive/archive.go b/internal/pipe/archive/archive.go
index 2161f53b..47a74e00 100644
--- a/internal/pipe/archive/archive.go
+++ b/internal/pipe/archive/archive.go
@@ -7,19 +7,22 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"reflect"
 	"strings"
 	"sync"
 
+	"github.com/goreleaser/goreleaser/internal/deprecate"
+
 	"github.com/apex/log"
 	"github.com/campoy/unique"
-	zglob "github.com/mattn/go-zglob"
-	"golang.org/x/sync/errgroup"
-
 	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/semerrgroup"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/archive"
+	archivelib "github.com/goreleaser/goreleaser/pkg/archive"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
+	zglob "github.com/mattn/go-zglob"
 )
 
 const (
@@ -39,26 +42,50 @@ func (Pipe) String() string {
 
 // Default sets the pipe defaults
 func (Pipe) Default(ctx *context.Context) error {
-	var archive = &ctx.Config.Archive
-	if archive.Format == "" {
-		archive.Format = "tar.gz"
-	}
-	if len(archive.Files) == 0 {
-		archive.Files = []string{
-			"licence*",
-			"LICENCE*",
-			"license*",
-			"LICENSE*",
-			"readme*",
-			"README*",
-			"changelog*",
-			"CHANGELOG*",
+	var ids = map[string]int{}
+	if len(ctx.Config.Archives) == 0 {
+		ctx.Config.Archives = append(ctx.Config.Archives, ctx.Config.Archive)
+		if !reflect.DeepEqual(ctx.Config.Archive, config.Archive{}) {
+			deprecate.Notice("archive")
 		}
 	}
-	if archive.NameTemplate == "" {
-		archive.NameTemplate = defaultNameTemplate
-		if archive.Format == "binary" {
-			archive.NameTemplate = defaultBinaryNameTemplate
+	for i := range ctx.Config.Archives {
+		var archive = &ctx.Config.Archives[i]
+		if archive.Format == "" {
+			archive.Format = "tar.gz"
+		}
+		if archive.ID == "" {
+			archive.ID = "default"
+		}
+		if len(archive.Files) == 0 {
+			archive.Files = []string{
+				"licence*",
+				"LICENCE*",
+				"license*",
+				"LICENSE*",
+				"readme*",
+				"README*",
+				"changelog*",
+				"CHANGELOG*",
+			}
+		}
+		if archive.NameTemplate == "" {
+			archive.NameTemplate = defaultNameTemplate
+			if archive.Format == "binary" {
+				archive.NameTemplate = defaultBinaryNameTemplate
+			}
+		}
+		if len(archive.Builds) == 0 {
+			for _, build := range ctx.Config.Builds {
+				archive.Builds = append(archive.Builds, build.ID)
+			}
+		}
+		ids[archive.ID]++
+	}
+
+	for id, cont := range ids {
+		if cont > 1 {
+			return fmt.Errorf("found %d archives with the ID '%s', please fix your config", cont, id)
 		}
 	}
 	return nil
@@ -66,26 +93,34 @@ func (Pipe) Default(ctx *context.Context) error {
 
 // Run the pipe
 func (Pipe) Run(ctx *context.Context) error {
-	var g errgroup.Group // TODO: use semerrgroup here
-	var filtered = ctx.Artifacts.Filter(artifact.ByType(artifact.Binary))
-	for group, artifacts := range filtered.GroupByPlatform() {
-		log.Debugf("group %s has %d binaries", group, len(artifacts))
-		artifacts := artifacts
-		g.Go(func() error {
-			if packageFormat(ctx, artifacts[0].Goos) == "binary" {
-				return skip(ctx, artifacts)
-			}
-			return create(ctx, artifacts)
-		})
+	var g = semerrgroup.New(ctx.Parallelism)
+	for _, archive := range ctx.Config.Archives {
+		archive := archive
+		var filtered = ctx.Artifacts.Filter(
+			artifact.And(
+				artifact.ByType(artifact.Binary),
+				artifact.ByIDs(archive.Builds...),
+			),
+		)
+		for group, artifacts := range filtered.GroupByPlatform() {
+			log.Debugf("group %s has %d binaries", group, len(artifacts))
+			artifacts := artifacts
+			g.Go(func() error {
+				if packageFormat(archive, artifacts[0].Goos) == "binary" {
+					return skip(ctx, archive, artifacts)
+				}
+				return create(ctx, archive, artifacts)
+			})
+		}
 	}
 	return g.Wait()
 }
 
-func create(ctx *context.Context, binaries []artifact.Artifact) error {
-	var format = packageFormat(ctx, binaries[0].Goos)
+func create(ctx *context.Context, archive config.Archive, binaries []artifact.Artifact) error {
+	var format = packageFormat(archive, binaries[0].Goos)
 	folder, err := tmpl.New(ctx).
-		WithArtifact(binaries[0], ctx.Config.Archive.Replacements).
-		Apply(ctx.Config.Archive.NameTemplate)
+		WithArtifact(binaries[0], archive.Replacements).
+		Apply(archive.NameTemplate)
 	if err != nil {
 		return err
 	}
@@ -107,16 +142,16 @@ func create(ctx *context.Context, binaries []artifact.Artifact) error {
 	log.Info("creating")
 
 	wrap, err := tmpl.New(ctx).
-		WithArtifact(binaries[0], ctx.Config.Archive.Replacements).
-		Apply(wrapFolder(ctx.Config.Archive))
+		WithArtifact(binaries[0], archive.Replacements).
+		Apply(wrapFolder(archive))
 	if err != nil {
 		return err
 	}
 
-	var a = NewEnhancedArchive(archive.New(archiveFile), wrap)
+	var a = NewEnhancedArchive(archivelib.New(archiveFile), wrap)
 	defer a.Close() // nolint: errcheck
 
-	files, err := findFiles(ctx)
+	files, err := findFiles(archive)
 	if err != nil {
 		return fmt.Errorf("failed to find files to archive: %s", err.Error())
 	}
@@ -139,6 +174,7 @@ func create(ctx *context.Context, binaries []artifact.Artifact) error {
 		Goarm:  binaries[0].Goarm,
 		Extra: map[string]interface{}{
 			"Builds": binaries,
+			"ID":     archive.ID,
 		},
 	})
 	return nil
@@ -155,12 +191,12 @@ func wrapFolder(a config.Archive) string {
 	}
 }
 
-func skip(ctx *context.Context, binaries []artifact.Artifact) error {
+func skip(ctx *context.Context, archive config.Archive, binaries []artifact.Artifact) error {
 	for _, binary := range binaries {
 		log.WithField("binary", binary.Name).Info("skip archiving")
 		name, err := tmpl.New(ctx).
-			WithArtifact(binary, ctx.Config.Archive.Replacements).
-			Apply(ctx.Config.Archive.NameTemplate)
+			WithArtifact(binary, archive.Replacements).
+			Apply(archive.NameTemplate)
 		if err != nil {
 			return err
 		}
@@ -173,14 +209,15 @@ func skip(ctx *context.Context, binaries []artifact.Artifact) error {
 			Goarm:  binary.Goarm,
 			Extra: map[string]interface{}{
 				"Builds": []artifact.Artifact{binary},
+				"ID":     archive.ID,
 			},
 		})
 	}
 	return nil
 }
 
-func findFiles(ctx *context.Context) (result []string, err error) {
-	for _, glob := range ctx.Config.Archive.Files {
+func findFiles(archive config.Archive) (result []string, err error) {
+	for _, glob := range archive.Files {
 		files, err := zglob.Glob(glob)
 		if err != nil {
 			return result, fmt.Errorf("globbing failed for pattern %s: %s", glob, err.Error())
@@ -194,13 +231,13 @@ func findFiles(ctx *context.Context) (result []string, err error) {
 	return
 }
 
-func packageFormat(ctx *context.Context, platform string) string {
-	for _, override := range ctx.Config.Archive.FormatOverrides {
+func packageFormat(archive config.Archive, platform string) string {
+	for _, override := range archive.FormatOverrides {
 		if strings.HasPrefix(platform, override.Goos) {
 			return override.Format
 		}
 	}
-	return ctx.Config.Archive.Format
+	return archive.Format
 }
 
 // NewEnhancedArchive enhances a pre-existing archive.Archive instance
diff --git a/internal/pipe/build/build.go b/internal/pipe/build/build.go
index b4f993dc..50177952 100644
--- a/internal/pipe/build/build.go
+++ b/internal/pipe/build/build.go
@@ -53,7 +53,7 @@ func (Pipe) Default(ctx *context.Context) error {
 	}
 	for id, cont := range ids {
 		if cont > 1 {
-			return fmt.Errorf("there are more than %d builds with the ID '%s', please fix your config", cont, id)
+			return fmt.Errorf("found %d builds with the ID '%s', please fix your config", cont, id)
 		}
 	}
 	return nil
diff --git a/internal/static/config.go b/internal/static/config.go
index d70e3872..66637d56 100644
--- a/internal/static/config.go
+++ b/internal/static/config.go
@@ -13,8 +13,8 @@ before:
 builds:
 - env:
   - CGO_ENABLED=0
-archive:
-  replacements:
+archives:
+- replacements:
     darwin: Darwin
     linux: Linux
     windows: Windows
diff --git a/pkg/config/config.go b/pkg/config/config.go
index f3510bc9..232d9f40 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -148,13 +148,14 @@ type FormatOverride struct {
 
 // Archive config used for the archive
 type Archive struct {
-	NameTemplate string            `yaml:"name_template,omitempty"`
-	Replacements map[string]string `yaml:",omitempty"`
-
-	Format          string           `yaml:",omitempty"`
-	FormatOverrides []FormatOverride `yaml:"format_overrides,omitempty"`
-	WrapInDirectory string           `yaml:"wrap_in_directory,omitempty"`
-	Files           []string         `yaml:",omitempty"`
+	ID              string            `yaml:",omitempty"`
+	Builds          []string          `yaml:",omitempty"`
+	NameTemplate    string            `yaml:"name_template,omitempty"`
+	Replacements    map[string]string `yaml:",omitempty"`
+	Format          string            `yaml:",omitempty"`
+	FormatOverrides []FormatOverride  `yaml:"format_overrides,omitempty"`
+	WrapInDirectory string            `yaml:"wrap_in_directory,omitempty"`
+	Files           []string          `yaml:",omitempty"`
 }
 
 // Release config used for the GitHub release
@@ -312,7 +313,8 @@ type Project struct {
 	Brew          Homebrew  `yaml:",omitempty"`
 	Scoop         Scoop     `yaml:",omitempty"`
 	Builds        []Build   `yaml:",omitempty"`
-	Archive       Archive   `yaml:",omitempty"`
+	Archive       Archive   `yaml:",omitempty"` // TODO: remove this
+	Archives      []Archive `yaml:",omitempty"`
 	NFPM          NFPM      `yaml:",omitempty"`
 	Snapcraft     Snapcraft `yaml:",omitempty"`
 	Snapshot      Snapshot  `yaml:",omitempty"`
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index 8946675e..6bb3e3ab 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -39,8 +39,8 @@ var Defaulters = []Defaulter{
 	snapshot.Pipe{},
 	release.Pipe{},
 	project.Pipe{},
-	archive.Pipe{},
 	build.Pipe{},
+	archive.Pipe{},
 	nfpm.Pipe{},
 	snapcraft.Pipe{},
 	checksums.Pipe{},
diff --git a/www/content/archive.md b/www/content/archive.md
index 72ff18e9..72c40c06 100644
--- a/www/content/archive.md
+++ b/www/content/archive.md
@@ -6,63 +6,72 @@ weight: 40
 ---
 
 The binaries built will be archived together with the `README` and `LICENSE` files into a
-`tar.gz` file. In the `archive` section you can customize the archive name,
+`tar.gz` file. In the `archives` section you can customize the archive name,
 additional files, and format.
 
-Here is a commented `archive` section with all fields specified:
+Here is a commented `archives` section with all fields specified:
 
 ```yml
 # .goreleaser.yml
-archive:
-  # Archive name template.
-  # Defaults:
-  # - if format is `tar.gz`, `gz` or `zip`:
-  #   - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}`
-  # - if format is `binary`:
-  #   - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}`
-  name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
-
-  # Replacements for GOOS and GOARCH in the archive name.
-  # Keys should be valid GOOSs or GOARCHs.
-  # Values are the respective replacements.
-  # Default is empty.
-  replacements:
-    amd64: 64-bit
-    386: 32-bit
-    darwin: macOS
-    linux: Tux
-
-  # Set to true, if you want all files in the archive to be in a single directory.
-  # If set to true and you extract the archive 'goreleaser_Linux_arm64.tar.gz',
-  # you get a folder 'goreleaser_Linux_arm64'.
-  # If set to false, all files are extracted separately.
-  # You can also set it to a custom folder name (templating is supported).
-  # Default is false.
-  wrap_in_directory: true
-
-  # Archive format. Valid options are `tar.gz`, `gz`, `zip` and `binary`.
-  # If format is `binary`, no archives are created and the binaries are instead
-  # uploaded directly.
-  # Default is `tar.gz`.
-  format: zip
-
-  # Can be used to change the archive formats for specific GOOSs.
-  # Most common use case is to archive as zip on Windows.
-  # Default is empty.
-  format_overrides:
-    - goos: windows
-      format: zip
-
-  # Additional files/globs you want to add to the archive.
-  # Defaults are any files matching `LICENCE*`, `LICENSE*`,
-  # `README*` and `CHANGELOG*` (case-insensitive).
-  files:
-    - LICENSE.txt
-    - README.md
-    - CHANGELOG.md
-    - docs/*
-    - design/*.png
-    - templates/**/*
+archives:
+  -
+    # ID of this archive.
+    # Defaults to `default`.
+    id: my-archive
+
+    # Builds reference which build instances should be archived in this archive.
+    builds:
+    - default
+
+    # Archive name template.
+    # Defaults:
+    # - if format is `tar.gz`, `gz` or `zip`:
+    #   - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}`
+    # - if format is `binary`:
+    #   - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}`
+    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
+
+    # Replacements for GOOS and GOARCH in the archive name.
+    # Keys should be valid GOOSs or GOARCHs.
+    # Values are the respective replacements.
+    # Default is empty.
+    replacements:
+      amd64: 64-bit
+      386: 32-bit
+      darwin: macOS
+      linux: Tux
+
+    # Set to true, if you want all files in the archive to be in a single directory.
+    # If set to true and you extract the archive 'goreleaser_Linux_arm64.tar.gz',
+    # you get a folder 'goreleaser_Linux_arm64'.
+    # If set to false, all files are extracted separately.
+    # You can also set it to a custom folder name (templating is supported).
+    # Default is false.
+    wrap_in_directory: true
+
+    # Archive format. Valid options are `tar.gz`, `gz`, `zip` and `binary`.
+    # If format is `binary`, no archives are created and the binaries are instead
+    # uploaded directly.
+    # Default is `tar.gz`.
+    format: zip
+
+    # Can be used to change the archive formats for specific GOOSs.
+    # Most common use case is to archive as zip on Windows.
+    # Default is empty.
+    format_overrides:
+      - goos: windows
+        format: zip
+
+    # Additional files/globs you want to add to the archive.
+    # Defaults are any files matching `LICENCE*`, `LICENSE*`,
+    # `README*` and `CHANGELOG*` (case-insensitive).
+    files:
+      - LICENSE.txt
+      - README.md
+      - CHANGELOG.md
+      - docs/*
+      - design/*.png
+      - templates/**/*
 ```
 
 > Learn more about the [name template engine](/templates).
@@ -80,8 +89,8 @@ A working hack is to use something like this:
 
 ```yaml
 # goreleaser.yml
-archive:
-  files:
+archives:
+- files:
   - none*
 ```
 
diff --git a/www/content/deprecations.md b/www/content/deprecations.md
index 9562e152..a31fc640 100644
--- a/www/content/deprecations.md
+++ b/www/content/deprecations.md
@@ -33,6 +33,27 @@ to this:
 
 -->
 
+### archive
+
+> since 2019-04-16
+
+We now allow multiple archives, so the `archive` statement will be removed.
+
+Change this:
+
+```yaml
+archive:
+  format: zip
+```
+
+to this:
+
+```yaml
+archives:
+  - id: foo
+    format: zip
+```
+
 ### docker.binary
 
 > since 2018-10-01
diff --git a/www/content/tutorials.md b/www/content/tutorials.md
index 5dde0dcd..2cac9a40 100644
--- a/www/content/tutorials.md
+++ b/www/content/tutorials.md
@@ -11,6 +11,7 @@ Tutorials made by the community.
 - [Fast and easy Go binaries delivery](https://carlosbecker.com/posts/goreleaser/)
 - [GoReleaser Docker Support](https://carlosbecker.com/posts/goreleaser-docker/)
 - [GoReleaser: build and push Snapcraft packages from TravisCI](https://carlosbecker.com/posts/goreleaser-snap-travis/)
+- [Painless Github releases with Drone and GoReleaser](https://medium.com/@stepanvrany/painless-github-releases-with-drone-and-goreleaser-853bbbccd0c0)
 - [Shipping Rust Binaries with GoReleaser](https://medium.com/@jondot/shipping-rust-binaries-with-goreleaser-d5aa42a46be0)
 
 Want to add your tutorial here? Please do! Click on the `improve this page`


Test patch:
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
index 09af1885..ab9ce304 100644
--- a/internal/artifact/artifact_test.go
+++ b/internal/artifact/artifact_test.go
@@ -195,3 +195,40 @@ func TestExtraOr(t *testing.T) {
 	require.Equal(t, "foo", a.ExtraOr("Foo", "bar"))
 	require.Equal(t, "bar", a.ExtraOr("Foobar", "bar"))
 }
+
+func TestByIDs(t *testing.T) {
+	var data = []Artifact{
+		{
+			Name: "foo",
+			Extra: map[string]interface{}{
+				"ID": "foo",
+			},
+		},
+		{
+			Name: "bar",
+			Extra: map[string]interface{}{
+				"ID": "bar",
+			},
+		},
+		{
+			Name: "foobar",
+			Extra: map[string]interface{}{
+				"ID": "foo",
+			},
+		},
+		{
+			Name: "check",
+			Extra: map[string]interface{}{
+				"ID": "check",
+			},
+		},
+	}
+	var artifacts = New()
+	for _, a := range data {
+		artifacts.Add(a)
+	}
+
+	require.Len(t, artifacts.Filter(ByIDs("check")).items, 1)
+	require.Len(t, artifacts.Filter(ByIDs("foo")).items, 2)
+	require.Len(t, artifacts.Filter(ByIDs("foo", "bar")).items, 3)
+}
diff --git a/internal/pipe/archive/archive_test.go b/internal/pipe/archive/archive_test.go
index 71681780..8639c430 100644
--- a/internal/pipe/archive/archive_test.go
+++ b/internal/pipe/archive/archive_test.go
@@ -44,16 +44,20 @@ func TestRunPipe(t *testing.T) {
 				config.Project{
 					Dist:        dist,
 					ProjectName: "foobar",
-					Archive: config.Archive{
-						NameTemplate: defaultNameTemplate,
-						Files: []string{
-							"README.*",
-							"./foo/**/*",
-						},
-						FormatOverrides: []config.FormatOverride{
-							{
-								Goos:   "windows",
-								Format: "zip",
+					Archives: []config.Archive{
+						{
+							ID:           "defaultarch",
+							Builds:       []string{"default"},
+							NameTemplate: defaultNameTemplate,
+							Files: []string{
+								"README.*",
+								"./foo/**/*",
+							},
+							FormatOverrides: []config.FormatOverride{
+								{
+									Goos:   "windows",
+									Format: "zip",
+								},
 							},
 						},
 					},
@@ -67,6 +71,7 @@ func TestRunPipe(t *testing.T) {
 				Type:   artifact.Binary,
 				Extra: map[string]interface{}{
 					"Binary": "mybin",
+					"ID":     "default",
 				},
 			}
 			var windowsBuild = artifact.Artifact{
@@ -78,15 +83,19 @@ func TestRunPipe(t *testing.T) {
 				Extra: map[string]interface{}{
 					"Binary":    "mybin",
 					"Extension": ".exe",
+					"ID":        "default",
 				},
 			}
 			ctx.Artifacts.Add(darwinBuild)
 			ctx.Artifacts.Add(windowsBuild)
 			ctx.Version = "0.0.1"
 			ctx.Git.CurrentTag = "v0.0.1"
-			ctx.Config.Archive.Format = format
+			ctx.Config.Archives[0].Format = format
 			require.NoError(tt, Pipe{}.Run(ctx))
 			var archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableArchive))
+			for _, arch := range archives.List() {
+				require.Equal(t, "defaultarch", arch.Extra["ID"].(string), "all archives should have the archive ID set")
+			}
 			require.Len(tt, archives.List(), 2)
 			darwin := archives.Filter(artifact.ByGoos("darwin")).List()[0]
 			windows := archives.Filter(artifact.ByGoos("windows")).List()[0]
@@ -175,9 +184,12 @@ func TestRunPipeBinary(t *testing.T) {
 	var ctx = context.New(
 		config.Project{
 			Dist: dist,
-			Archive: config.Archive{
-				Format:       "binary",
-				NameTemplate: defaultBinaryNameTemplate,
+			Archives: []config.Archive{
+				{
+					Format:       "binary",
+					NameTemplate: defaultBinaryNameTemplate,
+					Builds:       []string{"default"},
+				},
 			},
 		},
 	)
@@ -191,6 +203,7 @@ func TestRunPipeBinary(t *testing.T) {
 		Type:   artifact.Binary,
 		Extra: map[string]interface{}{
 			"Binary": "mybin",
+			"ID":     "default",
 		},
 	})
 	ctx.Artifacts.Add(artifact.Artifact{
@@ -202,6 +215,7 @@ func TestRunPipeBinary(t *testing.T) {
 		Extra: map[string]interface{}{
 			"Binary": "mybin",
 			"Ext":    ".exe",
+			"ID":     "default",
 		},
 	})
 	require.NoError(t, Pipe{}.Run(ctx))
@@ -217,9 +231,12 @@ func TestRunPipeDistRemoved(t *testing.T) {
 	var ctx = context.New(
 		config.Project{
 			Dist: "/path/nope",
-			Archive: config.Archive{
-				NameTemplate: "nope",
-				Format:       "zip",
+			Archives: []config.Archive{
+				{
+					NameTemplate: "nope",
+					Format:       "zip",
+					Builds:       []string{"default"},
+				},
 			},
 		},
 	)
@@ -233,6 +250,7 @@ func TestRunPipeDistRemoved(t *testing.T) {
 		Extra: map[string]interface{}{
 			"Binary":    "mybin",
 			"Extension": ".exe",
+			"ID":        "default",
 		},
 	})
 	require.EqualError(t, Pipe{}.Run(ctx), `failed to create directory /path/nope/nope.zip: open /path/nope/nope.zip: no such file or directory`)
@@ -249,11 +267,14 @@ func TestRunPipeInvalidGlob(t *testing.T) {
 	var ctx = context.New(
 		config.Project{
 			Dist: dist,
-			Archive: config.Archive{
-				NameTemplate: "foo",
-				Format:       "zip",
-				Files: []string{
-					"[x-]",
+			Archives: []config.Archive{
+				{
+					Builds:       []string{"default"},
+					NameTemplate: "foo",
+					Format:       "zip",
+					Files: []string{
+						"[x-]",
+					},
 				},
 			},
 		},
@@ -267,6 +288,7 @@ func TestRunPipeInvalidGlob(t *testing.T) {
 		Type:   artifact.Binary,
 		Extra: map[string]interface{}{
 			"Binary": "mybin",
+			"ID":     "default",
 		},
 	})
 	require.EqualError(t, Pipe{}.Run(ctx), `failed to find files to archive: globbing failed for pattern [x-]: file does not exist`)
@@ -283,9 +305,12 @@ func TestRunPipeInvalidNameTemplate(t *testing.T) {
 	var ctx = context.New(
 		config.Project{
 			Dist: dist,
-			Archive: config.Archive{
-				NameTemplate: "foo{{ .fff }",
-				Format:       "zip",
+			Archives: []config.Archive{
+				{
+					Builds:       []string{"default"},
+					NameTemplate: "foo{{ .fff }",
+					Format:       "zip",
+				},
 			},
 		},
 	)
@@ -298,6 +323,7 @@ func TestRunPipeInvalidNameTemplate(t *testing.T) {
 		Type:   artifact.Binary,
 		Extra: map[string]interface{}{
 			"Binary": "mybin",
+			"ID":     "default",
 		},
 	})
 	require.EqualError(t, Pipe{}.Run(ctx), `template: tmpl:1: unexpected "}" in operand`)
@@ -314,10 +340,13 @@ func TestRunPipeInvalidWrapInDirectoryTemplate(t *testing.T) {
 	var ctx = context.New(
 		config.Project{
 			Dist: dist,
-			Archive: config.Archive{
-				NameTemplate:    "foo",
-				WrapInDirectory: "foo{{ .fff }",
-				Format:          "zip",
+			Archives: []config.Archive{
+				{
+					Builds:          []string{"default"},
+					NameTemplate:    "foo",
+					WrapInDirectory: "foo{{ .fff }",
+					Format:          "zip",
+				},
 			},
 		},
 	)
@@ -330,6 +359,7 @@ func TestRunPipeInvalidWrapInDirectoryTemplate(t *testing.T) {
 		Type:   artifact.Binary,
 		Extra: map[string]interface{}{
 			"Binary": "mybin",
+			"ID":     "default",
 		},
 	})
 	require.EqualError(t, Pipe{}.Run(ctx), `template: tmpl:1: unexpected "}" in operand`)
@@ -348,15 +378,18 @@ func TestRunPipeWrap(t *testing.T) {
 	var ctx = context.New(
 		config.Project{
 			Dist: dist,
-			Archive: config.Archive{
-				NameTemplate:    "foo",
-				WrapInDirectory: "foo_{{ .Os }}",
-				Format:          "tar.gz",
-				Replacements: map[string]string{
-					"darwin": "macOS",
-				},
-				Files: []string{
-					"README.*",
+			Archives: []config.Archive{
+				{
+					Builds:          []string{"default"},
+					NameTemplate:    "foo",
+					WrapInDirectory: "foo_{{ .Os }}",
+					Format:          "tar.gz",
+					Replacements: map[string]string{
+						"darwin": "macOS",
+					},
+					Files: []string{
+						"README.*",
+					},
 				},
 			},
 		},
@@ -370,6 +403,7 @@ func TestRunPipeWrap(t *testing.T) {
 		Type:   artifact.Binary,
 		Extra: map[string]interface{}{
 			"Binary": "mybin",
+			"ID":     "default",
 		},
 	})
 	require.NoError(t, Pipe{}.Run(ctx))
@@ -395,61 +429,69 @@ func TestRunPipeWrap(t *testing.T) {
 func TestDefault(t *testing.T) {
 	var ctx = &context.Context{
 		Config: config.Project{
-			Archive: config.Archive{},
+			Archives: []config.Archive{},
 		},
 	}
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.NotEmpty(t, ctx.Config.Archive.NameTemplate)
-	require.Equal(t, "tar.gz", ctx.Config.Archive.Format)
-	require.NotEmpty(t, ctx.Config.Archive.Files)
+	require.NotEmpty(t, ctx.Config.Archives[0].NameTemplate)
+	require.Equal(t, "tar.gz", ctx.Config.Archives[0].Format)
+	require.NotEmpty(t, ctx.Config.Archives[0].Files)
 }
 
 func TestDefaultSet(t *testing.T) {
 	var ctx = &context.Context{
 		Config: config.Project{
-			Archive: config.Archive{
-				NameTemplate: "foo",
-				Format:       "zip",
-				Files: []string{
-					"foo",
+			Archives: []config.Archive{
+				{
+					Builds:       []string{"default"},
+					NameTemplate: "foo",
+					Format:       "zip",
+					Files: []string{
+						"foo",
+					},
 				},
 			},
 		},
 	}
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.Equal(t, "foo", ctx.Config.Archive.NameTemplate)
-	require.Equal(t, "zip", ctx.Config.Archive.Format)
-	require.Equal(t, "foo", ctx.Config.Archive.Files[0])
+	require.Equal(t, "foo", ctx.Config.Archives[0].NameTemplate)
+	require.Equal(t, "zip", ctx.Config.Archives[0].Format)
+	require.Equal(t, "foo", ctx.Config.Archives[0].Files[0])
 }
 
 func TestDefaultFormatBinary(t *testing.T) {
 	var ctx = &context.Context{
 		Config: config.Project{
-			Archive: config.Archive{
-				Format: "binary",
+			Archives: []config.Archive{
+				{
+					Format: "binary",
+				},
 			},
 		},
 	}
 	require.NoError(t, Pipe{}.Default(ctx))
-	require.Equal(t, defaultBinaryNameTemplate, ctx.Config.Archive.NameTemplate)
+	require.Equal(t, defaultBinaryNameTemplate, ctx.Config.Archives[0].NameTemplate)
 }
 
 func TestFormatFor(t *testing.T) {
 	var ctx = &context.Context{
 		Config: config.Project{
-			Archive: config.Archive{
-				Format: "tar.gz",
-				FormatOverrides: []config.FormatOverride{
-					{
-						Goos:   "windows",
-						Format: "zip",
+			Archives: []config.Archive{
+				{
+					Builds: []string{"default"},
+					Format: "tar.gz",
+					FormatOverrides: []config.FormatOverride{
+						{
+							Goos:   "windows",
+							Format: "zip",
+						},
 					},
 				},
 			},
 		},
 	}
-	require.Equal(t, "zip", packageFormat(ctx, "windows"))
-	require.Equal(t, "tar.gz", packageFormat(ctx, "linux"))
+	require.Equal(t, "zip", packageFormat(ctx.Config.Archives[0], "windows"))
+	require.Equal(t, "tar.gz", packageFormat(ctx.Config.Archives[0], "linux"))
 }
 
 func TestBinaryOverride(t *testing.T) {
@@ -471,15 +513,18 @@ func TestBinaryOverride(t *testing.T) {
 				config.Project{
 					Dist:        dist,
 					ProjectName: "foobar",
-					Archive: config.Archive{
-						NameTemplate: defaultNameTemplate,
-						Files: []string{
-							"README.*",
-						},
-						FormatOverrides: []config.FormatOverride{
-							{
-								Goos:   "windows",
-								Format: "binary",
+					Archives: []config.Archive{
+						{
+							Builds:       []string{"default"},
+							NameTemplate: defaultNameTemplate,
+							Files: []string{
+								"README.*",
+							},
+							FormatOverrides: []config.FormatOverride{
+								{
+									Goos:   "windows",
+									Format: "binary",
+								},
 							},
 						},
 					},
@@ -494,6 +539,7 @@ func TestBinaryOverride(t *testing.T) {
 				Type:   artifact.Binary,
 				Extra: map[string]interface{}{
 					"Binary": "mybin",
+					"ID":     "default",
 				},
 			})
 			ctx.Artifacts.Add(artifact.Artifact{
@@ -505,10 +551,11 @@ func TestBinaryOverride(t *testing.T) {
 				Extra: map[string]interface{}{
 					"Binary": "mybin",
 					"Ext":    ".exe",
+					"ID":     "default",
 				},
 			})
 			ctx.Version = "0.0.1"
-			ctx.Config.Archive.Format = format
+			ctx.Config.Archives[0].Format = format
 
 			require.NoError(tt, Pipe{}.Run(ctx))
 			var archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableArchive))
@@ -537,13 +584,16 @@ func TestRunPipeSameArchiveFilename(t *testing.T) {
 		config.Project{
 			Dist:        dist,
 			ProjectName: "foobar",
-			Archive: config.Archive{
-				NameTemplate: "same-filename",
-				Files: []string{
-					"README.*",
-					"./foo/**/*",
+			Archives: []config.Archive{
+				{
+					Builds:       []string{"default"},
+					NameTemplate: "same-filename",
+					Files: []string{
+						"README.*",
+						"./foo/**/*",
+					},
+					Format: "tar.gz",
 				},
-				Format: "tar.gz",
 			},
 		},
 	)
@@ -555,6 +605,7 @@ func TestRunPipeSameArchiveFilename(t *testing.T) {
 		Type:   artifact.Binary,
 		Extra: map[string]interface{}{
 			"Binary": "mybin",
+			"ID":     "default",
 		},
 	})
 	ctx.Artifacts.Add(artifact.Artifact{
@@ -566,6 +617,7 @@ func TestRunPipeSameArchiveFilename(t *testing.T) {
 		Extra: map[string]interface{}{
 			"Binary":    "mybin",
 			"Extension": ".exe",
+			"ID":        "default",
 		},
 	})
 	ctx.Version = "0.0.1"
@@ -610,3 +662,19 @@ func TestWrapInDirectory(t *testing.T) {
 		}))
 	})
 }
+
+func TestSeveralArchivesWithTheSameID(t *testing.T) {
+	var ctx = &context.Context{
+		Config: config.Project{
+			Archives: []config.Archive{
+				{
+					ID: "a",
+				},
+				{
+					ID: "a",
+				},
+			},
+		},
+	}
+	require.EqualError(t, Pipe{}.Default(ctx), "found 2 archives with the ID 'a', please fix your config")
+}
diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go
index bb55a11c..d187dfdc 100644
--- a/internal/pipe/build/build_test.go
+++ b/internal/pipe/build/build_test.go
@@ -235,7 +235,7 @@ func TestSeveralBuildsWithTheSameID(t *testing.T) {
 			},
 		},
 	}
-	assert.EqualError(t, Pipe{}.Default(ctx), "there are more than 2 builds with the ID 'a', please fix your config")
+	assert.EqualError(t, Pipe{}.Default(ctx), "found 2 builds with the ID 'a', please fix your config")
 }
 
 func TestDefaultPartialBuilds(t *testing.T) {
diff --git a/internal/pipe/defaults/defaults_test.go b/internal/pipe/defaults/defaults_test.go
index 43728571..4484cde1 100644
--- a/internal/pipe/defaults/defaults_test.go
+++ b/internal/pipe/defaults/defaults_test.go
@@ -34,13 +34,13 @@ func TestFillBasicData(t *testing.T) {
 	assert.Contains(t, ctx.Config.Builds[0].Goos, "linux")
 	assert.Contains(t, ctx.Config.Builds[0].Goarch, "386")
 	assert.Contains(t, ctx.Config.Builds[0].Goarch, "amd64")
-	assert.Equal(t, "tar.gz", ctx.Config.Archive.Format)
+	assert.Equal(t, "tar.gz", ctx.Config.Archives[0].Format)
 	assert.Contains(t, ctx.Config.Brew.Install, "bin.install \"goreleaser\"")
 	assert.Empty(t, ctx.Config.Dockers)
 	assert.Equal(t, "https://github.com", ctx.Config.GitHubURLs.Download)
-	assert.NotEmpty(t, ctx.Config.Archive.NameTemplate)
+	assert.NotEmpty(t, ctx.Config.Archives[0].NameTemplate)
 	assert.NotEmpty(t, ctx.Config.Builds[0].Ldflags)
-	assert.NotEmpty(t, ctx.Config.Archive.Files)
+	assert.NotEmpty(t, ctx.Config.Archives[0].Files)
 	assert.NotEmpty(t, ctx.Config.Dist)
 }