Repository: goreleaser/goreleaser
Pull request #1472 (2020-04-26 23:54:16)

feat: refactor main.go to spf13/cobra
closes https://github.com/goreleaser/goreleaser/issues/1436

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

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

```diff
@@            Coverage Diff             @@
##           master    #1472      +/-   ##
==========================================
+ Coverage   83.90%   85.92%   +2.01%     
==========================================
  Files          59       65       +6     
  Lines        3523     3594      +71     
==========================================
+ Hits         2956     3088     +132     
+ Misses        479      418      -61     
  Partials       88       88              
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/1472?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [main.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree#diff-bWFpbi5nbw==) | `50.00% <14.28%> (+1.85%)` | :arrow_up: |
| [cmd/init.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree#diff-Y21kL2luaXQuZ28=) | `90.47% <90.47%> (ø)` | |
| [cmd/check.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree#diff-Y21kL2NoZWNrLmdv) | `100.00% <100.00%> (ø)` | |
| [cmd/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree#diff-Y21kL2NvbmZpZy5nbw==) | `100.00% <100.00%> (ø)` | |
| [cmd/error.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree#diff-Y21kL2Vycm9yLmdv) | `100.00% <100.00%> (ø)` | |
| [cmd/release.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree#diff-Y21kL3JlbGVhc2UuZ28=) | `100.00% <100.00%> (ø)` | |
| [cmd/root.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree#diff-Y21kL3Jvb3QuZ28=) | `100.00% <100.00%> (ø)` | |
| ... and [4 more](https://codecov.io/gh/goreleaser/goreleaser/pull/1472/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/1472?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/1472?src=pr&el=footer). Last update [20e3261...8297ed9](https://codecov.io/gh/goreleaser/goreleaser/pull/1472?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/cmd/check.go b/cmd/check.go
new file mode 100644
index 00000000..a2a9cbb3
--- /dev/null
+++ b/cmd/check.go
@@ -0,0 +1,63 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/apex/log"
+	"github.com/caarlos0/ctrlc"
+	"github.com/fatih/color"
+	"github.com/goreleaser/goreleaser/internal/pipe/defaults"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+)
+
+type checkCmd struct {
+	cmd        *cobra.Command
+	config     string
+	deprecated bool
+}
+
+func newCheckCmd() *checkCmd {
+	var root = &checkCmd{}
+	var cmd = &cobra.Command{
+		Use:           "check",
+		Aliases:       []string{"c"},
+		Short:         "Checks if configuration is valid",
+		SilenceUsage:  true,
+		SilenceErrors: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cfg, err := loadConfig(root.config)
+			if err != nil {
+				return err
+			}
+			var ctx = context.New(cfg)
+			ctx.Deprecated = root.deprecated
+
+			if err := ctrlc.Default.Run(ctx, func() error {
+				log.Info(color.New(color.Bold).Sprint("checking config:"))
+				return defaults.Pipe{}.Run(ctx)
+			}); err != nil {
+				log.WithError(err).Error(color.New(color.Bold).Sprintf("config is invalid"))
+				return errors.Wrap(err, "invalid config")
+			}
+
+			if ctx.Deprecated {
+				return wrapErrorWithCode(
+					fmt.Errorf("config is valid, but uses deprecated properties, check logs above for details"),
+					2,
+					"",
+				)
+			}
+			log.Infof(color.New(color.Bold).Sprintf("config is valid"))
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVarP(&root.config, "config", "f", "", "Configuration file to check")
+	cmd.Flags().BoolVar(&root.deprecated, "deprecated", false, "Force print the deprecation message - tests only")
+	_ = cmd.Flags().MarkHidden("deprecated")
+
+	root.cmd = cmd
+	return root
+}
diff --git a/cmd/config.go b/cmd/config.go
new file mode 100644
index 00000000..0d86c50f
--- /dev/null
+++ b/cmd/config.go
@@ -0,0 +1,30 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/apex/log"
+	"github.com/goreleaser/goreleaser/pkg/config"
+)
+
+func loadConfig(path string) (config.Project, error) {
+	if path != "" {
+		return config.Load(path)
+	}
+	for _, f := range [4]string{
+		".goreleaser.yml",
+		".goreleaser.yaml",
+		"goreleaser.yml",
+		"goreleaser.yaml",
+	} {
+		proj, err := config.Load(f)
+		if err != nil && os.IsNotExist(err) {
+			continue
+		}
+		return proj, err
+	}
+	// the user didn't specify a config file and the known possible file names
+	// don't exist, so, return an empty config and a nil err.
+	log.Warn("could not find a config file, using defaults...")
+	return config.Project{}, nil
+}
diff --git a/cmd/error.go b/cmd/error.go
new file mode 100644
index 00000000..5ae02359
--- /dev/null
+++ b/cmd/error.go
@@ -0,0 +1,23 @@
+package cmd
+
+type exitError struct {
+	err     error
+	code    int
+	details string
+}
+
+func wrapErrorWithCode(err error, code int, details string) *exitError {
+	return &exitError{
+		err:     err,
+		code:    code,
+		details: details,
+	}
+}
+
+func wrapError(err error, log string) *exitError {
+	return wrapErrorWithCode(err, 1, log)
+}
+
+func (e *exitError) Error() string {
+	return e.err.Error()
+}
diff --git a/cmd/init.go b/cmd/init.go
new file mode 100644
index 00000000..a0ad0768
--- /dev/null
+++ b/cmd/init.go
@@ -0,0 +1,46 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/apex/log"
+	"github.com/fatih/color"
+	"github.com/goreleaser/goreleaser/internal/static"
+	"github.com/spf13/cobra"
+)
+
+type initCmd struct {
+	cmd    *cobra.Command
+	config string
+}
+
+func newInitCmd() *initCmd {
+	var root = &initCmd{}
+	var cmd = &cobra.Command{
+		Use:           "init",
+		Aliases:       []string{"i"},
+		Short:         "Generates a .goreleaser.yml file",
+		SilenceUsage:  true,
+		SilenceErrors: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			f, err := os.OpenFile(root.config, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, 0644)
+			if err != nil {
+				return err
+			}
+			defer f.Close()
+
+			log.Infof(color.New(color.Bold).Sprintf("Generating %s file", root.config))
+			if _, err := f.WriteString(static.ExampleConfig); err != nil {
+				return err
+			}
+
+			log.WithField("file", root.config).Info("config created; please edit accordingly to your needs")
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVarP(&root.config, "config", "f", ".goreleaser.yml", "Load configuration from file")
+
+	root.cmd = cmd
+	return root
+}
diff --git a/cmd/release.go b/cmd/release.go
new file mode 100644
index 00000000..3043a310
--- /dev/null
+++ b/cmd/release.go
@@ -0,0 +1,117 @@
+package cmd
+
+import (
+	"time"
+
+	"github.com/apex/log"
+	"github.com/caarlos0/ctrlc"
+	"github.com/fatih/color"
+	"github.com/goreleaser/goreleaser/internal/middleware"
+	"github.com/goreleaser/goreleaser/internal/pipeline"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/spf13/cobra"
+)
+
+type releaseCmd struct {
+	cmd  *cobra.Command
+	opts releaseOpts
+}
+
+type releaseOpts struct {
+	config        string
+	releaseNotes  string
+	releaseHeader string
+	releaseFooter string
+	snapshot      bool
+	skipPublish   bool
+	skipSign      bool
+	skipValidate  bool
+	rmDist        bool
+	deprecated    bool
+	parallelism   int
+	timeout       time.Duration
+}
+
+func newReleaseCmd() *releaseCmd {
+	var root = &releaseCmd{}
+	var cmd = &cobra.Command{
+		Use:           "release",
+		Aliases:       []string{"r"},
+		Short:         "Releases the current project",
+		SilenceUsage:  true,
+		SilenceErrors: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			start := time.Now()
+
+			log.Infof(color.New(color.Bold).Sprint("releasing..."))
+
+			ctx, err := releaseProject(root.opts)
+			if err != nil {
+				return wrapError(err, color.New(color.Bold).Sprintf("release failed after %0.2fs", time.Since(start).Seconds()))
+			}
+
+			if ctx.Deprecated {
+				log.Warn(color.New(color.Bold).Sprintf("your config is using deprecated properties, check logs above for details"))
+			}
+
+			log.Infof(color.New(color.Bold).Sprintf("release succeeded after %0.2fs", time.Since(start).Seconds()))
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVarP(&root.opts.config, "config", "f", "", "Load configuration from file")
+	cmd.Flags().StringVar(&root.opts.releaseNotes, "release-notes", "", "Load custom release notes from a markdown file")
+	cmd.Flags().StringVar(&root.opts.releaseHeader, "release-header", "", "Load custom release notes header from a markdown file")
+	cmd.Flags().StringVar(&root.opts.releaseFooter, "release-footer", "", "Load custom release notes footer from a markdown file")
+	cmd.Flags().BoolVar(&root.opts.snapshot, "snapshot", false, "Generate an unversioned snapshot release, skipping all validations and without publishing any artifacts")
+	cmd.Flags().BoolVar(&root.opts.skipPublish, "skip-publish", false, "Skips publishing artifacts")
+	cmd.Flags().BoolVar(&root.opts.skipSign, "skip-sign", false, "Skips signing the artifacts")
+	cmd.Flags().BoolVar(&root.opts.skipValidate, "skip-validate", false, "Skips several sanity checks")
+	cmd.Flags().BoolVar(&root.opts.rmDist, "rm-dist", false, "Remove the dist folder before building")
+	cmd.Flags().IntVarP(&root.opts.parallelism, "parallelism", "p", 4, "Amount tasks to run concurrently")
+	cmd.Flags().DurationVar(&root.opts.timeout, "timeout", 30*time.Minute, "Timeout to the entire release process")
+	cmd.Flags().BoolVar(&root.opts.deprecated, "deprecated", false, "Force print the deprecation message - tests only")
+	_ = cmd.Flags().MarkHidden("deprecated")
+
+	root.cmd = cmd
+	return root
+}
+
+func releaseProject(options releaseOpts) (*context.Context, error) {
+	cfg, err := loadConfig(options.config)
+	if err != nil {
+		return nil, err
+	}
+	ctx, cancel := context.NewWithTimeout(cfg, options.timeout)
+	defer cancel()
+	setupContext(ctx, options)
+	return ctx, ctrlc.Default.Run(ctx, func() error {
+		for _, pipe := range pipeline.Pipeline {
+			if err := middleware.Logging(
+				pipe.String(),
+				middleware.ErrHandler(pipe.Run),
+				middleware.DefaultInitialPadding,
+			)(ctx); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+func setupContext(ctx *context.Context, options releaseOpts) *context.Context {
+	ctx.Parallelism = options.parallelism
+	log.Debugf("parallelism: %v", ctx.Parallelism)
+	ctx.ReleaseNotes = options.releaseNotes
+	ctx.ReleaseHeader = options.releaseHeader
+	ctx.ReleaseFooter = options.releaseFooter
+	ctx.Snapshot = options.snapshot
+	ctx.SkipPublish = ctx.Snapshot || options.skipPublish
+	ctx.SkipValidate = ctx.Snapshot || options.skipValidate
+	ctx.SkipSign = options.skipSign
+	ctx.RmDist = options.rmDist
+
+	// test only
+	ctx.Deprecated = options.deprecated
+	return ctx
+}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 00000000..87bbe802
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,106 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/apex/log"
+	"github.com/apex/log/handlers/cli"
+	"github.com/fatih/color"
+	"github.com/spf13/cobra"
+)
+
+func Execute(version string, exit func(int), args []string) {
+	// enable colored output on travis
+	if os.Getenv("CI") != "" {
+		color.NoColor = false
+	}
+
+	log.SetHandler(cli.Default)
+
+	fmt.Println()
+	defer fmt.Println()
+	newRootCmd(version, exit).Execute(args)
+}
+
+func (cmd *rootCmd) Execute(args []string) {
+	cmd.cmd.SetArgs(args)
+
+	if shouldPrependRelease(cmd.cmd, args) {
+		cmd.cmd.SetArgs(append([]string{"release"}, args...))
+	}
+
+	if err := cmd.cmd.Execute(); err != nil {
+		var code = 1
+		var msg = "command failed"
+		if eerr, ok := err.(*exitError); ok {
+			code = eerr.code
+			if eerr.details != "" {
+				msg = eerr.details
+			}
+		}
+		log.WithError(err).Error(msg)
+		cmd.exit(code)
+	}
+}
+
+type rootCmd struct {
+	cmd   *cobra.Command
+	debug bool
+	exit  func(int)
+}
+
+func newRootCmd(version string, exit func(int)) *rootCmd {
+	var root = &rootCmd{
+		exit: exit,
+	}
+	var cmd = &cobra.Command{
+		Use:           "goreleaser",
+		Short:         "Deliver Go binaries as fast and easily as possible",
+		Version:       version,
+		SilenceUsage:  true,
+		SilenceErrors: true,
+		PersistentPreRun: func(cmd *cobra.Command, args []string) {
+			if root.debug {
+				log.SetLevel(log.DebugLevel)
+				log.Debug("debug logs enabled")
+			}
+		},
+	}
+
+	cmd.PersistentFlags().BoolVar(&root.debug, "debug", false, "Enable debug mode")
+	cmd.AddCommand(
+		newReleaseCmd().cmd,
+		newCheckCmd().cmd,
+		newInitCmd().cmd,
+	)
+
+	root.cmd = cmd
+	return root
+}
+
+func shouldPrependRelease(cmd *cobra.Command, args []string) bool {
+	// find current cmd, if its not root, it means the user actively
+	// set a command, so let it go
+	xmd, _, _ := cmd.Find(args)
+	if xmd != cmd {
+		return false
+	}
+
+	// if we have != 1 args, assume its a release
+	if len(args) != 1 {
+		return true
+	}
+
+	// given that its 1, check if its one of the valid standalone flags
+	// for the root cmd
+	for _, s := range []string{"-h", "--help", "-v", "--version"} {
+		if s == args[0] {
+			// if it is, we should run the root cmd
+			return false
+		}
+	}
+
+	// otherwise, we should probably prepend release
+	return true
+}
diff --git a/go.mod b/go.mod
index 6301c523..d4a3a6e7 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,9 @@ require (
 	github.com/mattn/go-zglob v0.0.1
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pkg/errors v0.9.1
+	github.com/spf13/cobra v1.0.0
 	github.com/stretchr/testify v1.5.1
+	github.com/tj/assert v0.0.0-20171129193455-018094318fb0
 	github.com/ulikunitz/xz v0.5.7
 	github.com/xanzy/go-gitlab v0.31.0
 	gocloud.dev v0.19.0
@@ -26,6 +28,5 @@ require (
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
 	google.golang.org/appengine v1.6.5 // indirect
-	gopkg.in/alecthomas/kingpin.v2 v2.2.6
 	gopkg.in/yaml.v2 v2.2.8
 )
diff --git a/go.sum b/go.sum
index 2e6fc708..0b035a7e 100644
--- a/go.sum
+++ b/go.sum
@@ -34,9 +34,12 @@ github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3s
 github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
 github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/apex/log v1.1.4 h1:3Zk+boorIQAAGBrHn0JUtAau4ihMamT4WdnfdnXM1zQ=
@@ -44,6 +47,7 @@ github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ
 github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
 github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
 github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@@ -51,6 +55,8 @@ github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
 github.com/aws/aws-sdk-go v1.25.11 h1:wUivbsVOH3LpHdC3Rl5i+FLHfg4sOmYgv4bvHe7+/Pg=
 github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
 github.com/caarlos0/ctrlc v1.0.0 h1:2DtF8GSIcajgffDFJzyG15vO+1PuBWOMUdFut7NnXhw=
@@ -61,13 +67,21 @@ github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RS
 github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
 github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4=
 github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
@@ -79,11 +93,17 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -129,7 +149,11 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/goreleaser/nfpm v1.2.1 h1:AEnu9XVmupRDTR930Z2rAs31Mj6sLIPxFcR9ESYvgDA=
 github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.9.2 h1:S+ef0492XaIknb8LMjcwgW2i3cNTzDYMmDrOThOJNWc=
 github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
@@ -143,19 +167,27 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
 github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
 github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jarcoal/httpmock v1.0.5 h1:cHtVEcTxRSX4J0je7mWPfc9BpDpqzXSJ5HbymZmyHck=
 github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -163,6 +195,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -179,14 +212,18 @@ github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvO
 github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
 github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -194,15 +231,38 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
 github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
 github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -211,10 +271,13 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
 github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
 github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
 github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
 github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
 github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
@@ -223,14 +286,21 @@ github.com/xanzy/go-gitlab v0.31.0 h1:+nHztQuCXGSMluKe5Q9IRaPdz6tO8O0gMkQ0vqGpiB
 github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 gocloud.dev v0.19.0 h1:EDRyaRAnMGSq/QBto486gWFxMLczAfIYUmusV7XLNBM=
 gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -249,6 +319,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -256,6 +327,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
@@ -280,8 +352,10 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -302,6 +376,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
diff --git a/main.go b/main.go
index dc7f0214..656de35f 100644
--- a/main.go
+++ b/main.go
@@ -2,21 +2,9 @@ package main
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
-	"time"
 
-	"github.com/apex/log"
-	"github.com/apex/log/handlers/cli"
-	"github.com/caarlos0/ctrlc"
-	"github.com/fatih/color"
-	"github.com/goreleaser/goreleaser/internal/middleware"
-	"github.com/goreleaser/goreleaser/internal/pipe/defaults"
-	"github.com/goreleaser/goreleaser/internal/pipeline"
-	"github.com/goreleaser/goreleaser/internal/static"
-	"github.com/goreleaser/goreleaser/pkg/config"
-	"github.com/goreleaser/goreleaser/pkg/context"
-	"gopkg.in/alecthomas/kingpin.v2"
+	"github.com/goreleaser/goreleaser/cmd"
 )
 
 // nolint: gochecknoglobals
@@ -27,186 +15,16 @@ var (
 	builtBy = ""
 )
 
-type releaseOptions struct {
-	Config        string
-	ReleaseNotes  string
-	ReleaseHeader string
-	ReleaseFooter string
-	Snapshot      bool
-	SkipPublish   bool
-	SkipSign      bool
-	SkipValidate  bool
-	RmDist        bool
-	Parallelism   int
-	Timeout       time.Duration
-}
-
 func main() {
-	// enable colored output on travis
-	if os.Getenv("CI") != "" {
-		color.NoColor = false
-	}
-	log.SetHandler(cli.Default)
-
-	fmt.Println()
-	defer fmt.Println()
-
-	var app = kingpin.New("goreleaser", "Deliver Go binaries as fast and easily as possible")
-	var debug = app.Flag("debug", "Enable debug mode").Bool()
-	var config = app.Flag("config", "Load configuration from file").Short('c').Short('f').PlaceHolder(".goreleaser.yml").String()
-	var initCmd = app.Command("init", "Generates a .goreleaser.yml file").Alias("i")
-	var checkCmd = app.Command("check", "Checks if configuration is valid").Alias("c")
-	var releaseCmd = app.Command("release", "Releases the current project").Alias("r").Default()
-	var releaseNotes = releaseCmd.Flag("release-notes", "Load custom release notes from a markdown file").PlaceHolder("notes.md").String()
-	var releaseHeader = releaseCmd.Flag("release-header", "Load custom release notes header from a markdown file").PlaceHolder("notes-header.md").String()
-	var releaseFooter = releaseCmd.Flag("release-footer", "Load custom release notes footer from a markdown file").PlaceHolder("notes-footer.md").String()
-	var snapshot = releaseCmd.Flag("snapshot", "Generate an unversioned snapshot release, skipping all validations and without publishing any artifacts").Bool()
-	var skipPublish = releaseCmd.Flag("skip-publish", "Skips publishing artifacts").Bool()
-	var skipSign = releaseCmd.Flag("skip-sign", "Skips signing the artifacts").Bool()
-	var skipValidate = releaseCmd.Flag("skip-validate", "Skips several sanity checks").Bool()
-	var rmDist = releaseCmd.Flag("rm-dist", "Remove the dist folder before building").Bool()
-	var parallelism = releaseCmd.Flag("parallelism", "Amount tasks to run concurrently").Short('p').Default("4").Int()
-	var timeout = releaseCmd.Flag("timeout", "Timeout to the entire release process").Default("30m").Duration()
-
-	app.Version(buildVersion(version, commit, date, builtBy))
-	app.VersionFlag.Short('v')
-	app.HelpFlag.Short('h')
-	app.UsageTemplate(static.UsageTemplate)
-
-	cmd := kingpin.MustParse(app.Parse(os.Args[1:]))
-	if *debug {
-		log.SetLevel(log.DebugLevel)
-	}
-	switch cmd {
-	case initCmd.FullCommand():
-		var filename = *config
-		if filename == "" {
-			filename = ".goreleaser.yml"
-		}
-		if err := initProject(filename); err != nil {
-			log.WithError(err).Error("failed to init project")
-			os.Exit(1)
-		}
-		log.WithField("file", filename).Info("config created; please edit accordingly to your needs")
-	case checkCmd.FullCommand():
-		ctx, err := checkConfig(*config)
-		if err != nil {
-			log.WithError(err).Errorf(color.New(color.Bold).Sprintf("config is invalid"))
-			os.Exit(1)
-		}
-		if ctx.Deprecated {
-			log.Warn(color.New(color.Bold).Sprintf("config is valid, but uses deprecated properties, check logs above for details"))
-			os.Exit(2)
-		}
-		log.Infof(color.New(color.Bold).Sprintf("config is valid"))
-	case releaseCmd.FullCommand():
-		start := time.Now()
-		log.Infof(color.New(color.Bold).Sprintf("releasing using goreleaser %s...", version))
-		var options = releaseOptions{
-			Config:        *config,
-			ReleaseNotes:  *releaseNotes,
-			ReleaseHeader: *releaseHeader,
-			ReleaseFooter: *releaseFooter,
-			Snapshot:      *snapshot,
-			SkipPublish:   *skipPublish,
-			SkipValidate:  *skipValidate,
-			SkipSign:      *skipSign,
-			RmDist:        *rmDist,
-			Parallelism:   *parallelism,
-			Timeout:       *timeout,
-		}
-		ctx, err := releaseProject(options)
-		if err != nil {
-			log.WithError(err).Errorf(color.New(color.Bold).Sprintf("release failed after %0.2fs", time.Since(start).Seconds()))
-			os.Exit(1)
-			return
-		}
-		if ctx.Deprecated {
-			log.Warn(color.New(color.Bold).Sprintf("your config is using deprecated properties, check logs above for details"))
-		}
-		log.Infof(color.New(color.Bold).Sprintf("release succeeded after %0.2fs", time.Since(start).Seconds()))
-	}
-}
-
-func checkConfig(filename string) (*context.Context, error) {
-	cfg, err := loadConfig(filename)
-	if err != nil {
-		return nil, err
-	}
-	var ctx = context.New(cfg)
-	return ctx, ctrlc.Default.Run(ctx, func() error {
-		log.Info(color.New(color.Bold).Sprint("checking config:"))
-		return defaults.Pipe{}.Run(ctx)
-	})
-}
-
-func releaseProject(options releaseOptions) (*context.Context, error) {
-	cfg, err := loadConfig(options.Config)
-	if err != nil {
-		return nil, err
-	}
-	ctx, cancel := context.NewWithTimeout(cfg, options.Timeout)
-	defer cancel()
-	ctx.Parallelism = options.Parallelism
-	log.Debugf("parallelism: %v", ctx.Parallelism)
-	ctx.ReleaseNotes = options.ReleaseNotes
-	ctx.ReleaseHeader = options.ReleaseHeader
-	ctx.ReleaseFooter = options.ReleaseFooter
-	ctx.Snapshot = options.Snapshot
-	ctx.SkipPublish = ctx.Snapshot || options.SkipPublish
-	ctx.SkipValidate = ctx.Snapshot || options.SkipValidate
-	ctx.SkipSign = options.SkipSign
-	ctx.RmDist = options.RmDist
-	return ctx, ctrlc.Default.Run(ctx, func() error {
-		for _, pipe := range pipeline.Pipeline {
-			if err := middleware.Logging(
-				pipe.String(),
-				middleware.ErrHandler(pipe.Run),
-				middleware.DefaultInitialPadding,
-			)(ctx); err != nil {
-				return err
-			}
-		}
-		return nil
-	})
-}
-
-// InitProject creates an example goreleaser.yml in the current directory
-func initProject(filename string) error {
-	if _, err := os.Stat(filename); !os.IsNotExist(err) {
-		if err != nil {
-			return err
-		}
-		return fmt.Errorf("%s already exists", filename)
-	}
-	log.Infof(color.New(color.Bold).Sprintf("Generating %s file", filename))
-	return ioutil.WriteFile(filename, []byte(static.ExampleConfig), 0644)
-}
-
-func loadConfig(path string) (config.Project, error) {
-	if path != "" {
-		return config.Load(path)
-	}
-	for _, f := range [4]string{
-		".goreleaser.yml",
-		".goreleaser.yaml",
-		"goreleaser.yml",
-		"goreleaser.yaml",
-	} {
-		proj, err := config.Load(f)
-		if err != nil && os.IsNotExist(err) {
-			continue
-		}
-		return proj, err
-	}
-	// the user didn't specify a config file and the known possible file names
-	// don't exist, so, return an empty config and a nil err.
-	log.Warn("could not find a config file, using defaults...")
-	return config.Project{}, nil
+	cmd.Execute(
+		buildVersion(version, commit, date, builtBy),
+		os.Exit,
+		os.Args[1:],
+	)
 }
 
 func buildVersion(version, commit, date, builtBy string) string {
-	var result = fmt.Sprintf("version: %s", version)
+	var result = version
 	if commit != "" {
 		result = fmt.Sprintf("%s\ncommit: %s", result, commit)
 	}


Test patch:
diff --git a/cmd/check_test.go b/cmd/check_test.go
new file mode 100644
index 00000000..ff369445
--- /dev/null
+++ b/cmd/check_test.go
@@ -0,0 +1,37 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestCheckConfig(t *testing.T) {
+	var cmd = newCheckCmd()
+	cmd.cmd.SetArgs([]string{"-f", "testdata/good.yml"})
+	require.NoError(t, cmd.cmd.Execute())
+}
+
+func TestCheckConfigThatDoesNotExist(t *testing.T) {
+	var cmd = newCheckCmd()
+	cmd.cmd.SetArgs([]string{"-f", "testdata/nope.yml"})
+	require.EqualError(t, cmd.cmd.Execute(), "open testdata/nope.yml: no such file or directory")
+}
+
+func TestCheckConfigUnmarshalError(t *testing.T) {
+	var cmd = newCheckCmd()
+	cmd.cmd.SetArgs([]string{"-f", "testdata/unmarshal_error.yml"})
+	require.EqualError(t, cmd.cmd.Execute(), "yaml: unmarshal errors:\n  line 1: field foo not found in type config.Project")
+}
+
+func TestCheckConfigInvalid(t *testing.T) {
+	var cmd = newCheckCmd()
+	cmd.cmd.SetArgs([]string{"-f", "testdata/invalid.yml"})
+	require.EqualError(t, cmd.cmd.Execute(), "invalid config: found 2 builds with the ID 'a', please fix your config")
+}
+
+func TestCheckConfigDeprecated(t *testing.T) {
+	var cmd = newCheckCmd()
+	cmd.cmd.SetArgs([]string{"-f", "testdata/good.yml", "--deprecated"})
+	require.EqualError(t, cmd.cmd.Execute(), "config is valid, but uses deprecated properties, check logs above for details")
+}
diff --git a/cmd/config_test.go b/cmd/config_test.go
new file mode 100644
index 00000000..51b752eb
--- /dev/null
+++ b/cmd/config_test.go
@@ -0,0 +1,41 @@
+package cmd
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/stretchr/testify/require"
+)
+
+func TestConfigFlagNotSetButExists(t *testing.T) {
+	for _, name := range []string{
+		".goreleaser.yml",
+		".goreleaser.yaml",
+		"goreleaser.yml",
+		"goreleaser.yaml",
+	} {
+		t.Run(name, func(t *testing.T) {
+			folder, back := setup(t)
+			defer back()
+			require.NoError(t, os.Rename(
+				filepath.Join(folder, "goreleaser.yml"),
+				filepath.Join(folder, name),
+			))
+			proj, err := loadConfig("")
+			require.NoError(t, err)
+			require.NotEqual(t, config.Project{}, proj)
+		})
+	}
+}
+
+func TestConfigFileDoesntExist(t *testing.T) {
+	folder, back := setup(t)
+	defer back()
+	err := os.Remove(filepath.Join(folder, "goreleaser.yml"))
+	require.NoError(t, err)
+	proj, err := loadConfig("")
+	require.NoError(t, err)
+	require.Equal(t, config.Project{}, proj)
+}
diff --git a/cmd/init_test.go b/cmd/init_test.go
new file mode 100644
index 00000000..91fc412e
--- /dev/null
+++ b/cmd/init_test.go
@@ -0,0 +1,44 @@
+package cmd
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestInit(t *testing.T) {
+	var folder = mktemp(t)
+	var cmd = newInitCmd().cmd
+	var path = filepath.Join(folder, "foo.yaml")
+	cmd.SetArgs([]string{"-f", path})
+	require.NoError(t, cmd.Execute())
+	require.FileExists(t, path)
+}
+
+func TestInitFileExists(t *testing.T) {
+	var folder = mktemp(t)
+	var cmd = newInitCmd().cmd
+	var path = filepath.Join(folder, "twice.yaml")
+	cmd.SetArgs([]string{"-f", path})
+	require.NoError(t, cmd.Execute())
+	require.EqualError(t, cmd.Execute(), "open "+path+": file exists")
+	require.FileExists(t, path)
+}
+
+func TestInitFileError(t *testing.T) {
+	var folder = mktemp(t)
+	var cmd = newInitCmd().cmd
+	var path = filepath.Join(folder, "nope.yaml")
+	require.NoError(t, os.Chmod(folder, 0000))
+	cmd.SetArgs([]string{"-f", path})
+	require.EqualError(t, cmd.Execute(), "open "+path+": permission denied")
+}
+
+func mktemp(t *testing.T) string {
+	folder, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
+	return folder
+}
diff --git a/cmd/release_test.go b/cmd/release_test.go
new file mode 100644
index 00000000..6019d7ed
--- /dev/null
+++ b/cmd/release_test.go
@@ -0,0 +1,87 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRelease(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	var cmd = newReleaseCmd()
+	cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2", "--deprecated"})
+	require.NoError(t, cmd.cmd.Execute())
+}
+
+func TestReleaseInvalidConfig(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	createFile(t, "goreleaser.yml", "foo: bar")
+	var cmd = newReleaseCmd()
+	cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2", "--deprecated"})
+	require.EqualError(t, cmd.cmd.Execute(), "yaml: unmarshal errors:\n  line 1: field foo not found in type config.Project")
+}
+
+func TestReleaseBrokenProject(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	createFile(t, "main.go", "not a valid go file")
+	var cmd = newReleaseCmd()
+	cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2"})
+	require.EqualError(t, cmd.cmd.Execute(), "failed to parse dir: .: main.go:1:1: expected 'package', found not")
+}
+
+func TestReleaseFlags(t *testing.T) {
+	var setup = func(opts releaseOpts) *context.Context {
+		return setupContext(context.New(config.Project{}), opts)
+	}
+
+	t.Run("snapshot", func(t *testing.T) {
+		var ctx = setup(releaseOpts{
+			snapshot: true,
+		})
+		require.True(t, ctx.Snapshot)
+		require.True(t, ctx.SkipPublish)
+		require.True(t, ctx.SkipPublish)
+	})
+
+	t.Run("skips", func(t *testing.T) {
+		var ctx = setup(releaseOpts{
+			skipPublish:  true,
+			skipSign:     true,
+			skipValidate: true,
+		})
+		require.True(t, ctx.SkipSign)
+		require.True(t, ctx.SkipPublish)
+		require.True(t, ctx.SkipPublish)
+	})
+
+	t.Run("parallelism", func(t *testing.T) {
+		require.Equal(t, 1, setup(releaseOpts{
+			parallelism: 1,
+		}).Parallelism)
+	})
+
+	t.Run("notes", func(t *testing.T) {
+		var notes = "foo.md"
+		var header = "header.md"
+		var footer = "footer.md"
+		var ctx = setup(releaseOpts{
+			releaseNotes:  notes,
+			releaseHeader: header,
+			releaseFooter: footer,
+		})
+		require.Equal(t, notes, ctx.ReleaseNotes)
+		require.Equal(t, header, ctx.ReleaseHeader)
+		require.Equal(t, footer, ctx.ReleaseFooter)
+	})
+
+	t.Run("rm dist", func(t *testing.T) {
+		require.True(t, setup(releaseOpts{
+			rmDist: true,
+		}).RmDist)
+	})
+}
diff --git a/cmd/root_test.go b/cmd/root_test.go
new file mode 100644
index 00000000..59bfd932
--- /dev/null
+++ b/cmd/root_test.go
@@ -0,0 +1,87 @@
+package cmd
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestRootCmd(t *testing.T) {
+	var mem = &exitMemento{}
+	Execute("1.2.3", mem.Exit, []string{"-h"})
+	require.Equal(t, 0, mem.code)
+}
+
+func TestRootCmdHelp(t *testing.T) {
+	var mem = &exitMemento{}
+	var cmd = newRootCmd("", mem.Exit).cmd
+	cmd.SetArgs([]string{"-h"})
+	require.NoError(t, cmd.Execute())
+	require.Equal(t, 0, mem.code)
+}
+
+func TestRootCmdVersion(t *testing.T) {
+	var b bytes.Buffer
+	var mem = &exitMemento{}
+	var cmd = newRootCmd("1.2.3", mem.Exit).cmd
+	cmd.SetOut(&b)
+	cmd.SetArgs([]string{"-v"})
+	require.NoError(t, cmd.Execute())
+	require.Equal(t, "goreleaser version 1.2.3\n", b.String())
+	require.Equal(t, 0, mem.code)
+}
+
+func TestRootCmdExitCode(t *testing.T) {
+	var mem = &exitMemento{}
+	var cmd = newRootCmd("", mem.Exit)
+	var args = []string{"check", "--deprecated", "-f", "testdata/good.yml"}
+	cmd.Execute(args)
+	require.Equal(t, 2, mem.code)
+}
+
+func TestRootRelease(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	var mem = &exitMemento{}
+	var cmd = newRootCmd("", mem.Exit)
+	cmd.Execute([]string{})
+	require.Equal(t, 1, mem.code)
+}
+
+func TestRootReleaseDebug(t *testing.T) {
+	_, back := setup(t)
+	defer back()
+	var mem = &exitMemento{}
+	var cmd = newRootCmd("", mem.Exit)
+	cmd.Execute([]string{"r", "--debug"})
+	require.Equal(t, 1, mem.code)
+}
+
+func TestShouldPrependRelease(t *testing.T) {
+	var result = func(args []string) bool {
+		return shouldPrependRelease(newRootCmd("1", func(_ int) {}).cmd, args)
+	}
+
+	t.Run("no args", func(t *testing.T) {
+		require.True(t, result([]string{}))
+	})
+
+	t.Run("release args", func(t *testing.T) {
+		require.True(t, result([]string{"--skip-validate"}))
+	})
+
+	t.Run("several release args", func(t *testing.T) {
+		require.True(t, result([]string{"--skip-validate", "--snapshot"}))
+	})
+
+	for _, s := range []string{"--help", "-h", "-v", "--version"} {
+		t.Run(s, func(t *testing.T) {
+			require.False(t, result([]string{s}))
+		})
+	}
+
+	t.Run("check", func(t *testing.T) {
+		require.False(t, result([]string{"check", "-f", "testdata/good.yml"}))
+	})
+}
diff --git a/cmd/testdata/good.yml b/cmd/testdata/good.yml
new file mode 100644
index 00000000..462e7201
--- /dev/null
+++ b/cmd/testdata/good.yml
@@ -0,0 +1,28 @@
+# This is an example goreleaser.yaml file with some sane defaults.
+# Make sure to check the documentation at http://goreleaser.com
+before:
+  hooks:
+    # You may remove this if you don't use go modules.
+    - go mod download
+    # you may remove this if you don't need go generate
+    - go generate ./...
+builds:
+- env:
+  - CGO_ENABLED=0
+archives:
+- replacements:
+    darwin: Darwin
+    linux: Linux
+    windows: Windows
+    386: i386
+    amd64: x86_64
+checksum:
+  name_template: 'checksums.txt'
+snapshot:
+  name_template: "{{ .Tag }}-next"
+changelog:
+  sort: asc
+  filters:
+    exclude:
+    - '^docs:'
+    - '^test:'
diff --git a/cmd/testdata/invalid.yml b/cmd/testdata/invalid.yml
new file mode 100644
index 00000000..9b3aa1ba
--- /dev/null
+++ b/cmd/testdata/invalid.yml
@@ -0,0 +1,5 @@
+builds:
+- id: a
+- id: b
+- id: c
+- id: a
diff --git a/cmd/testdata/unmarshal_error.yml b/cmd/testdata/unmarshal_error.yml
new file mode 100644
index 00000000..20e9ff3f
--- /dev/null
+++ b/cmd/testdata/unmarshal_error.yml
@@ -0,0 +1,1 @@
+foo: bar
diff --git a/cmd/util_test.go b/cmd/util_test.go
new file mode 100644
index 00000000..a4ade6a8
--- /dev/null
+++ b/cmd/util_test.go
@@ -0,0 +1,75 @@
+package cmd
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/stretchr/testify/require"
+	"github.com/tj/assert"
+)
+
+type exitMemento struct {
+	code int
+}
+
+func (e *exitMemento) Exit(i int) {
+	e.code = i
+}
+
+func setup(t *testing.T) (current string, back func()) {
+	_ = os.Unsetenv("GITHUB_TOKEN")
+	_ = os.Unsetenv("GITLAB_TOKEN")
+
+	folder, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
+	previous, err := os.Getwd()
+	assert.NoError(t, err)
+	assert.NoError(t, os.Chdir(folder))
+	createGoreleaserYaml(t)
+	createMainGo(t)
+	goModInit(t)
+	testlib.GitInit(t)
+	testlib.GitAdd(t)
+	testlib.GitCommit(t, "asdf")
+	testlib.GitTag(t, "v0.0.1")
+	testlib.GitCommit(t, "asas89d")
+	testlib.GitCommit(t, "assssf")
+	testlib.GitCommit(t, "assd")
+	testlib.GitTag(t, "v0.0.2")
+	testlib.GitRemoteAdd(t, "git@github.com:goreleaser/fake.git")
+	return folder, func() {
+		assert.NoError(t, os.Chdir(previous))
+	}
+}
+
+func createFile(t *testing.T, filename, contents string) {
+	assert.NoError(t, ioutil.WriteFile(filename, []byte(contents), 0644))
+}
+
+func createMainGo(t *testing.T) {
+	createFile(t, "main.go", "package main\nfunc main() {println(0)}")
+}
+
+func goModInit(t *testing.T) {
+	createFile(t, "go.mod", `module foo
+
+go 1.14
+`)
+}
+
+func createGoreleaserYaml(t *testing.T) {
+	var yaml = `build:
+  binary: fake
+  goos:
+    - linux
+  goarch:
+    - amd64
+release:
+  github:
+    owner: goreleaser
+    name: fake
+`
+	createFile(t, "goreleaser.yml", yaml)
+}
diff --git a/main_test.go b/main_test.go
index b4275313..c9e2db7e 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1,295 +1,40 @@
 package main
 
 import (
-	"io/ioutil"
-	"os"
-	"path/filepath"
 	"testing"
-	"time"
 
-	"github.com/goreleaser/goreleaser/internal/testlib"
-	"github.com/goreleaser/goreleaser/pkg/config"
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"gopkg.in/yaml.v2"
 )
 
-func init() {
-	_ = os.Unsetenv("GITHUB_TOKEN")
-	_ = os.Unsetenv("GITLAB_TOKEN")
-}
-
-func TestReleaseProject(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	_, err := releaseProject(testParams())
-	assert.NoError(t, err)
-}
-
-func TestCheckConfig(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	_, err := checkConfig(testParams().Config)
-	assert.NoError(t, err)
-}
-
-func TestCheckConfigFails(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	var filename = "fail.yaml"
-	assert.NoError(t, ioutil.WriteFile(filename, []byte("nope: 1"), 0644))
-	_, err := checkConfig(filename)
-	assert.Error(t, err)
-}
-
-func TestReleaseProjectSkipPublish(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	params := testParams()
-	params.Snapshot = true
-	params.SkipPublish = true
-	_, err := releaseProject(params)
-	assert.NoError(t, err)
-}
-
-func TestConfigFileIsSetAndDontExist(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	params := testParams()
-	params.Config = "/this/wont/exist"
-	_, err := releaseProject(params)
-	assert.Error(t, err)
-}
-
-func TestConfigFlagNotSetButExists(t *testing.T) {
-	for _, name := range []string{
-		".goreleaser.yml",
-		".goreleaser.yaml",
-		"goreleaser.yml",
-		"goreleaser.yaml",
-	} {
-		t.Run(name, func(t *testing.T) {
-			folder, back := setup(t)
-			defer back()
-			err := os.Rename(
-				filepath.Join(folder, "goreleaser.yml"),
-				filepath.Join(folder, name),
-			)
-			assert.NoError(t, err)
-			proj, err := loadConfig("")
-			assert.NoError(t, err)
-			assert.NotEqual(t, config.Project{}, proj)
-		})
-	}
-}
-
-func TestConfigFileDoesntExist(t *testing.T) {
-	folder, back := setup(t)
-	defer back()
-	err := os.Remove(filepath.Join(folder, "goreleaser.yml"))
-	assert.NoError(t, err)
-	proj, err := loadConfig("")
-	assert.NoError(t, err)
-	assert.Equal(t, config.Project{}, proj)
-}
-
-func TestReleaseNotesFileDontExist(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	params := testParams()
-	params.ReleaseNotes = "/this/also/wont/exist"
-	_, err := releaseProject(params)
-	assert.Error(t, err)
-}
-
-func TestCustomReleaseNotesFile(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	releaseNotes, err := ioutil.TempFile("", "")
-	assert.NoError(t, err)
-	createFile(t, releaseNotes.Name(), "nothing important at all")
-	var params = testParams()
-	params.ReleaseNotes = releaseNotes.Name()
-	_, err = releaseProject(params)
-	assert.NoError(t, err)
-}
-
-func TestCustomReleaseHeaderFileDontExist(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	params := testParams()
-	params.ReleaseHeader = "/header/that/dont/exist"
-	params.Snapshot = false
-	_, err := releaseProject(params)
-	assert.Error(t, err)
-}
-
-func TestCustomReleaseHeaderFile(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	releaseHeader, err := ioutil.TempFile("", "")
-	assert.NoError(t, err)
-	createFile(t, releaseHeader.Name(), "some release header")
-	params := testParams()
-	params.ReleaseHeader = releaseHeader.Name()
-	params.Snapshot = false
-	params.SkipPublish = true
-	_, err = releaseProject(params)
-	assert.NoError(t, err)
-}
-
-func TestCustomReleaseFooterFileDontExist(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	params := testParams()
-	params.ReleaseFooter = "/footer/that/dont/exist"
-	params.Snapshot = false
-	_, err := releaseProject(params)
-	assert.Error(t, err)
-}
-
-func TestCustomReleaseFooterFile(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	releaseFooter, err := ioutil.TempFile("", "")
-	assert.NoError(t, err)
-	createFile(t, releaseFooter.Name(), "some release footer")
-	params := testParams()
-	params.ReleaseFooter = releaseFooter.Name()
-	params.Snapshot = false
-	params.SkipPublish = true
-	_, err = releaseProject(params)
-	assert.NoError(t, err)
-}
-
-func TestBrokenPipe(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	createFile(t, "main.go", "not a valid go file")
-	_, err := releaseProject(testParams())
-	assert.Error(t, err)
-}
-
-func TestInitProject(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	var filename = "test_goreleaser.yml"
-	assert.NoError(t, initProject(filename))
-
-	file, err := os.Open(filename)
-	assert.NoError(t, err)
-	out, err := ioutil.ReadAll(file)
-	assert.NoError(t, err)
-
-	assert.NoError(t, yaml.Unmarshal(out, &config.Project{}))
-}
-
-func TestInitProjectFileExist(t *testing.T) {
-	_, back := setup(t)
-	defer back()
-	var filename = "test_goreleaser.yml"
-	createFile(t, filename, "")
-	assert.Error(t, initProject(filename))
-}
-
-func TestInitProjectDefaultPipeFails(t *testing.T) {
-	folder, back := setup(t)
-	defer back()
-	var filename = "test_goreleaser.yml"
-	assert.NoError(t, os.Chmod(folder, 0000))
-	assert.EqualError(t, initProject(filename), `stat test_goreleaser.yml: permission denied`)
-}
-
-func testParams() releaseOptions {
-	return releaseOptions{
-		Parallelism: 4,
-		Snapshot:    true,
-		Timeout:     time.Minute,
-	}
-}
-
-func setup(t *testing.T) (current string, back func()) {
-	folder, err := ioutil.TempDir("", "")
-	assert.NoError(t, err)
-	previous, err := os.Getwd()
-	assert.NoError(t, err)
-	assert.NoError(t, os.Chdir(folder))
-	createGoreleaserYaml(t)
-	createMainGo(t)
-	goModInit(t)
-	testlib.GitInit(t)
-	testlib.GitAdd(t)
-	testlib.GitCommit(t, "asdf")
-	testlib.GitTag(t, "v0.0.1")
-	testlib.GitCommit(t, "asas89d")
-	testlib.GitCommit(t, "assssf")
-	testlib.GitCommit(t, "assd")
-	testlib.GitTag(t, "v0.0.2")
-	testlib.GitRemoteAdd(t, "git@github.com:goreleaser/fake.git")
-	return folder, func() {
-		assert.NoError(t, os.Chdir(previous))
-	}
-}
-
-func createFile(t *testing.T, filename, contents string) {
-	assert.NoError(t, ioutil.WriteFile(filename, []byte(contents), 0644))
-}
-
-func createMainGo(t *testing.T) {
-	createFile(t, "main.go", "package main\nfunc main() {println(0)}")
-}
-
-func goModInit(t *testing.T) {
-	createFile(t, "go.mod", `module foo
-
-go 1.14
-`)
-}
-
-func createGoreleaserYaml(t *testing.T) {
-	var yaml = `build:
-  binary: fake
-  goos:
-    - linux
-  goarch:
-    - amd64
-release:
-  github:
-    owner: goreleaser
-    name: fake
-`
-	createFile(t, "goreleaser.yml", yaml)
-}
-
 func TestVersion(t *testing.T) {
 	for name, tt := range map[string]struct {
 		version, commit, date, builtBy string
 		out                            string
 	}{
 		"all empty": {
-			out: "version: ",
+			out: "",
 		},
 		"complete": {
 			version: "1.2.3",
 			date:    "12/12/12",
 			commit:  "aaaa",
 			builtBy: "me",
-			out:     "version: 1.2.3\ncommit: aaaa\nbuilt at: 12/12/12\nbuilt by: me",
+			out:     "1.2.3\ncommit: aaaa\nbuilt at: 12/12/12\nbuilt by: me",
 		},
 		"only version": {
 			version: "1.2.3",
-			out:     "version: 1.2.3",
+			out:     "1.2.3",
 		},
 		"version and date": {
 			version: "1.2.3",
 			date:    "12/12/12",
-			out:     "version: 1.2.3\nbuilt at: 12/12/12",
+			out:     "1.2.3\nbuilt at: 12/12/12",
 		},
 		"version, date, built by": {
 			version: "1.2.3",
 			date:    "12/12/12",
 			builtBy: "me",
-			out:     "version: 1.2.3\nbuilt at: 12/12/12\nbuilt by: me",
+			out:     "1.2.3\nbuilt at: 12/12/12\nbuilt by: me",
 		},
 	} {
 		tt := tt