Repository: goreleaser/goreleaser
Pull request #1435 (2020-04-12 15:07:22)

feat: improve check command
- instruct to run `goreleaser check` on CI
- actually fail if deprecated properties are being used
- warn in the end of a release as well if deprecated properties are being used

closes https://github.com/goreleaser/goreleaser/issues/1289


Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index ba20e220..1b9f4bb5 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -97,7 +97,7 @@ scoop:
   description: Deliver Go binaries as fast and easily as possible
   license: MIT
 nfpms:
-  - name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
+  - file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
     homepage:  https://goreleaser.com
     description: Deliver Go binaries as fast and easily as possible
     maintainer: Carlos Alexandro Becker <root@carlosbecker.com>
diff --git a/.travis.yml b/.travis.yml
index a5cab594..1d9aae47 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,6 +11,7 @@ install:
   - make setup
 script:
   - make ci
+  - ./goreleaser check
   - test -n "$TRAVIS_TAG" || ./goreleaser --snapshot --parallelism 2
 after_success:
   - bash <(curl -s https://codecov.io/bash)
diff --git a/go.mod b/go.mod
index 4ba5be51..67d88aa1 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/goreleaser/nfpm v1.2.1
 	github.com/imdario/mergo v0.3.9
 	github.com/jarcoal/httpmock v1.0.5
+	github.com/mattn/go-shellwords v1.0.10
 	github.com/mattn/go-zglob v0.0.1
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index a185a512..64404270 100644
--- a/go.sum
+++ b/go.sum
@@ -172,6 +172,8 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
 github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
+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/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
diff --git a/internal/deprecate/deprecate.go b/internal/deprecate/deprecate.go
index 8430a04b..1bf196ad 100644
--- a/internal/deprecate/deprecate.go
+++ b/internal/deprecate/deprecate.go
@@ -8,12 +8,14 @@ import (
 	"github.com/apex/log"
 	"github.com/apex/log/handlers/cli"
 	"github.com/fatih/color"
+	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
 const baseURL = "https://goreleaser.com/deprecations#"
 
 // Notice warns the user about the deprecation of the given property
-func Notice(property string) {
+func Notice(ctx *context.Context, property string) {
+	ctx.Deprecated = true
 	cli.Default.Padding += 3
 	defer func() {
 		cli.Default.Padding -= 3
diff --git a/internal/pipe/before/before.go b/internal/pipe/before/before.go
index 5b000dbd..a6826733 100644
--- a/internal/pipe/before/before.go
+++ b/internal/pipe/before/before.go
@@ -3,12 +3,12 @@ package before
 import (
 	"fmt"
 	"os/exec"
-	"strings"
 
 	"github.com/apex/log"
 	"github.com/fatih/color"
 	"github.com/goreleaser/goreleaser/internal/tmpl"
 	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/mattn/go-shellwords"
 )
 
 // Pipe is a global hook pipe
@@ -28,7 +28,10 @@ func (Pipe) Run(ctx *context.Context) error {
 		if err != nil {
 			return err
 		}
-		args := strings.Fields(s)
+		args, err := shellwords.Parse(s)
+		if err != nil {
+			return err
+		}
 		log.Infof("running %s", color.CyanString(step))
 		cmd := exec.Command(args[0], args[1:]...)
 		cmd.Env = ctx.Env.Strings()
diff --git a/internal/pipe/build/build.go b/internal/pipe/build/build.go
index d1193fd6..b7841c71 100644
--- a/internal/pipe/build/build.go
+++ b/internal/pipe/build/build.go
@@ -16,6 +16,7 @@ import (
 	builders "github.com/goreleaser/goreleaser/pkg/build"
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/mattn/go-shellwords"
 	"github.com/pkg/errors"
 
 	// langs to init
@@ -76,43 +77,87 @@ func buildWithDefaults(ctx *context.Context, build config.Build) config.Build {
 }
 
 func runPipeOnBuild(ctx *context.Context, build config.Build) error {
-	if err := runHook(ctx, build.Env, build.Hooks.Pre); err != nil {
-		return errors.Wrap(err, "pre hook failed")
-	}
 	var g = semerrgroup.New(ctx.Parallelism)
 	for _, target := range build.Targets {
 		target := target
 		build := build
 		g.Go(func() error {
-			return doBuild(ctx, build, target)
+			opts, err := buildOptionsForTarget(ctx, build, target)
+			if err != nil {
+				return err
+			}
+
+			if err := runHook(ctx, *opts, build.Env, build.Hooks.Pre); err != nil {
+				return errors.Wrap(err, "pre hook failed")
+			}
+			if err := doBuild(ctx, build, *opts); err != nil {
+				return err
+			}
+			if err := runHook(ctx, *opts, build.Env, build.Hooks.Post); err != nil {
+				return errors.Wrap(err, "post hook failed")
+			}
+			return nil
 		})
 	}
-	if err := g.Wait(); err != nil {
-		return err
-	}
-	return errors.Wrap(runHook(ctx, build.Env, build.Hooks.Post), "post hook failed")
+
+	return g.Wait()
 }
 
-func runHook(ctx *context.Context, env []string, hook string) error {
-	if hook == "" {
+func runHook(ctx *context.Context, opts builders.Options, buildEnv []string, hooks config.BuildHooks) error {
+	if len(hooks) == 0 {
 		return nil
 	}
-	sh, err := tmpl.New(ctx).WithEnvS(env).Apply(hook)
-	if err != nil {
-		return err
+
+	for _, hook := range hooks {
+		var env []string
+
+		env = append(env, ctx.Env.Strings()...)
+		env = append(env, buildEnv...)
+
+		for _, rawEnv := range hook.Env {
+			e, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(rawEnv)
+			if err != nil {
+				return err
+			}
+			env = append(env, e)
+		}
+
+		dir, err := tmpl.New(ctx).WithBuildOptions(opts).Apply(hook.Dir)
+		if err != nil {
+			return err
+		}
+
+		sh, err := tmpl.New(ctx).WithBuildOptions(opts).
+			WithEnvS(env).
+			Apply(hook.Cmd)
+		if err != nil {
+			return err
+		}
+
+		log.WithField("hook", sh).Info("running hook")
+		cmd, err := shellwords.Parse(sh)
+		if err != nil {
+			return err
+		}
+
+		if err := run(ctx, dir, cmd, env); err != nil {
+			return err
+		}
 	}
-	log.WithField("hook", sh).Info("running hook")
-	cmd := strings.Fields(sh)
-	env = append(env, ctx.Env.Strings()...)
-	return run(ctx, cmd, env)
+
+	return nil
 }
 
-func doBuild(ctx *context.Context, build config.Build, target string) error {
+func doBuild(ctx *context.Context, build config.Build, opts builders.Options) error {
+	return builders.For(build.Lang).Build(ctx, build, opts)
+}
+
+func buildOptionsForTarget(ctx *context.Context, build config.Build, target string) (*builders.Options, error) {
 	var ext = extFor(target, build.Flags)
 
 	binary, err := tmpl.New(ctx).Apply(build.Binary)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	build.Binary = binary
@@ -125,15 +170,15 @@ func doBuild(ctx *context.Context, build config.Build, target string) error {
 		),
 	)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	log.WithField("binary", path).Info("building")
-	return builders.For(build.Lang).Build(ctx, build, builders.Options{
+	return &builders.Options{
 		Target: target,
 		Name:   name,
 		Path:   path,
 		Ext:    ext,
-	})
+	}, nil
 }
 
 func extFor(target string, flags config.FlagArray) string {
@@ -154,11 +199,14 @@ func extFor(target string, flags config.FlagArray) string {
 	return ""
 }
 
-func run(ctx *context.Context, command, env []string) error {
+func run(ctx *context.Context, dir string, command, env []string) error {
 	/* #nosec */
 	var cmd = exec.CommandContext(ctx, command[0], command[1:]...)
 	var log = log.WithField("env", env).WithField("cmd", command)
 	cmd.Env = env
+	if dir != "" {
+		cmd.Dir = dir
+	}
 	log.Debug("running")
 	if out, err := cmd.CombinedOutput(); err != nil {
 		log.WithError(err).Debug("failed")
diff --git a/internal/pipe/nfpm/nfpm.go b/internal/pipe/nfpm/nfpm.go
index 1597a85e..25e92809 100644
--- a/internal/pipe/nfpm/nfpm.go
+++ b/internal/pipe/nfpm/nfpm.go
@@ -48,7 +48,7 @@ func (Pipe) Default(ctx *context.Context) error {
 			fpm.PackageName = ctx.Config.ProjectName
 		}
 		if fpm.NameTemplate != "" && fpm.FileNameTemplate == "" {
-			deprecate.Notice("nfpms.name_template")
+			deprecate.Notice(ctx, "nfpms.name_template")
 			fpm.FileNameTemplate = fpm.NameTemplate
 		}
 		if fpm.FileNameTemplate == "" {
diff --git a/internal/pipe/upload/upload.go b/internal/pipe/upload/upload.go
index 811f0d5e..c1d42c8f 100644
--- a/internal/pipe/upload/upload.go
+++ b/internal/pipe/upload/upload.go
@@ -23,7 +23,7 @@ func (Pipe) String() string {
 // Default sets the pipe defaults
 func (Pipe) Default(ctx *context.Context) error {
 	if len(ctx.Config.Puts) > 0 {
-		deprecate.Notice("puts")
+		deprecate.Notice(ctx, "puts")
 		ctx.Config.Uploads = append(ctx.Config.Uploads, ctx.Config.Puts...)
 	}
 	return http.Defaults(ctx.Config.Uploads)
diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go
index e3f6ee19..e8da6260 100644
--- a/internal/tmpl/tmpl.go
+++ b/internal/tmpl/tmpl.go
@@ -3,11 +3,13 @@ package tmpl
 
 import (
 	"bytes"
+	"path/filepath"
 	"strings"
 	"text/template"
 	"time"
 
 	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/pkg/build"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
@@ -42,8 +44,15 @@ const (
 	mips         = "Mips"
 	binary       = "Binary"
 	artifactName = "ArtifactName"
+
 	// gitlab only
 	artifactUploadHash = "ArtifactUploadHash"
+
+	// build keys
+	name   = "Name"
+	ext    = "Ext"
+	path   = "Path"
+	target = "Target"
 )
 
 // New Template
@@ -114,6 +123,19 @@ func (t *Template) WithArtifact(a *artifact.Artifact, replacements map[string]st
 	return t
 }
 
+func (t *Template) WithBuildOptions(opts build.Options) *Template {
+	return t.WithExtraFields(buildOptsToFields(opts))
+}
+
+func buildOptsToFields(opts build.Options) Fields {
+	return Fields{
+		target: opts.Target,
+		ext:    opts.Ext,
+		name:   opts.Name,
+		path:   opts.Path,
+	}
+}
+
 // Apply applies the given string against the Fields stored in the template.
 func (t *Template) Apply(s string) (string, error) {
 	var out bytes.Buffer
@@ -127,6 +149,7 @@ func (t *Template) Apply(s string) (string, error) {
 			"tolower": strings.ToLower,
 			"toupper": strings.ToUpper,
 			"trim":    strings.TrimSpace,
+			"dir":     filepath.Dir,
 		}).
 		Parse(s)
 	if err != nil {
diff --git a/main.go b/main.go
index 03530e82..dc7f0214 100644
--- a/main.go
+++ b/main.go
@@ -86,14 +86,17 @@ func main() {
 		if err := initProject(filename); err != nil {
 			log.WithError(err).Error("failed to init project")
 			os.Exit(1)
-			return
 		}
 		log.WithField("file", filename).Info("config created; please edit accordingly to your needs")
 	case checkCmd.FullCommand():
-		if err := checkConfig(*config); err != nil {
+		ctx, err := checkConfig(*config)
+		if err != nil {
 			log.WithError(err).Errorf(color.New(color.Bold).Sprintf("config is invalid"))
 			os.Exit(1)
-			return
+		}
+		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():
@@ -112,31 +115,35 @@ func main() {
 			Parallelism:   *parallelism,
 			Timeout:       *timeout,
 		}
-		if err := releaseProject(options); err != nil {
+		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) error {
+func checkConfig(filename string) (*context.Context, error) {
 	cfg, err := loadConfig(filename)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	var ctx = context.New(cfg)
-	return ctrlc.Default.Run(ctx, func() error {
+	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) error {
+func releaseProject(options releaseOptions) (*context.Context, error) {
 	cfg, err := loadConfig(options.Config)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	ctx, cancel := context.NewWithTimeout(cfg, options.Timeout)
 	defer cancel()
@@ -150,7 +157,7 @@ func releaseProject(options releaseOptions) error {
 	ctx.SkipValidate = ctx.Snapshot || options.SkipValidate
 	ctx.SkipSign = options.SkipSign
 	ctx.RmDist = options.RmDist
-	return ctrlc.Default.Run(ctx, func() error {
+	return ctx, ctrlc.Default.Run(ctx, func() error {
 		for _, pipe := range pipeline.Pipeline {
 			if err := middleware.Logging(
 				pipe.String(),
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 55fcada0..146e6150 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -151,7 +151,7 @@ type Build struct {
 	Ldflags  StringArray    `yaml:",omitempty"`
 	Flags    FlagArray      `yaml:",omitempty"`
 	Binary   string         `yaml:",omitempty"`
-	Hooks    Hooks          `yaml:",omitempty"`
+	Hooks    HookConfig     `yaml:",omitempty"`
 	Env      []string       `yaml:",omitempty"`
 	Lang     string         `yaml:",omitempty"`
 	Asmflags StringArray    `yaml:",omitempty"`
@@ -159,6 +159,54 @@ type Build struct {
 	Skip     bool           `yaml:",omitempty"`
 }
 
+type HookConfig struct {
+	Pre  BuildHooks `yaml:",omitempty"`
+	Post BuildHooks `yaml:",omitempty"`
+}
+
+type BuildHooks []BuildHook
+
+// UnmarshalYAML is a custom unmarshaler that allows simplified declaration of single command
+func (bhc *BuildHooks) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var singleCmd string
+	err := unmarshal(&singleCmd)
+	if err == nil {
+		*bhc = []BuildHook{{Cmd: singleCmd}}
+		return nil
+	}
+
+	type t BuildHooks
+	var hooks t
+	if err := unmarshal(&hooks); err != nil {
+		return err
+	}
+	*bhc = (BuildHooks)(hooks)
+	return nil
+}
+
+type BuildHook struct {
+	Dir string   `yaml:",omitempty"`
+	Cmd string   `yaml:",omitempty"`
+	Env []string `yaml:",omitempty"`
+}
+
+// UnmarshalYAML is a custom unmarshaler that allows simplified declarations of commands as strings
+func (bh *BuildHook) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var cmd string
+	if err := unmarshal(&cmd); err != nil {
+		type t BuildHook
+		var hook t
+		if err := unmarshal(&hook); err != nil {
+			return err
+		}
+		*bh = (BuildHook)(hook)
+		return nil
+	}
+
+	bh.Cmd = cmd
+	return nil
+}
+
 // FormatOverride is used to specify a custom format for a specific GOOS.
 type FormatOverride struct {
 	Goos   string `yaml:",omitempty"`
diff --git a/pkg/context/context.go b/pkg/context/context.go
index 870b5d75..ff901220 100644
--- a/pkg/context/context.go
+++ b/pkg/context/context.go
@@ -78,6 +78,7 @@ type Context struct {
 	SkipValidate  bool
 	RmDist        bool
 	PreRelease    bool
+	Deprecated    bool
 	Parallelism   int
 	Semver        Semver
 }
diff --git a/www/content/build.md b/www/content/build.md
index 660280b9..79e497b5 100644
--- a/www/content/build.md
+++ b/www/content/build.md
@@ -110,7 +110,7 @@ builds:
     # Default is both hooks empty.
     hooks:
       pre: rice embed-go
-      post: ./script.sh
+      post: ./script.sh {{ .Path }}
 
     # If true, skip the build.
     # Useful for library projects.
@@ -137,6 +137,66 @@ Then you can run:
 GOVERSION=$(go version) goreleaser
 ```
 
+## Build Hooks
+
+Both pre and post hooks run **for each build target**, regardless of whether
+these targets are generated via a matrix of OSes and architectures
+or defined explicitly.
+
+In addition to simple declarations as shown above _multiple_ hooks can be declared
+to help retaining reusability of config between different build environments.
+
+```yml
+builds:
+  -
+    id: "with-hooks"
+    targets:
+     - "darwin_amd64"
+     - "windows_amd64"
+    hooks:
+      pre:
+       - first-script.sh
+       - second-script.sh
+      post:
+       - upx "{{ .Path }}"
+       - codesign -project="{{ .ProjectName }}" "{{ .Path }}"
+```
+
+Each hook can also have its own work directory and environment variables:
+
+```yml
+builds:
+  -
+    id: "with-hooks"
+    targets:
+     - "darwin_amd64"
+     - "windows_amd64"
+    hooks:
+      pre:
+       - cmd: first-script.sh
+         dir: "{{ dir .Dist}}"
+         env:
+          - HOOK_SPECIFIC_VAR={{ .Env.GLOBAL_VAR }}
+       - second-script.sh
+```
+
+All properties of a hook (`cmd`, `dir` and `env`) support [templating](/customization/#Name%20Templates)
+with `post` hooks having binary artifact available (as these run _after_ the build).
+Additionally the following build details are exposed to both `pre` and `post` hooks:
+
+|       Key       |              Description               |
+| :-------------: | :------------------------------------: |
+|      .Name      | Filename of the binary, e.g. `bin.exe` |
+|      .Ext       | Extension, e.g. `.exe`                 |
+|      .Path      | Absolute path to the binary            |
+|     .Target     | Build target, e.g. `darwin_amd64`      |
+
+Environment variables are inherited and overridden in the following order:
+
+ - global (`env`)
+ - build (`builds[].env`)
+ - hook (`builds[].hooks.pre[].env` and `builds[].hooks.post[].env`)
+
 ## Go Modules
 
  If you use Go 1.11+ with go modules or vgo, when GoReleaser runs it may
diff --git a/www/content/ci.md b/www/content/ci.md
index 1e63408e..d190a879 100644
--- a/www/content/ci.md
+++ b/www/content/ci.md
@@ -18,16 +18,20 @@ You may want to setup your project to auto-deploy your new tags on
 # .travis.yml
 language: go
 
+# needed only if you use the snap pipe:
 addons:
   apt:
     packages:
-    # needed only if you use the snap pipe:
     - snapcraft
 
-# needed for the docker pipe
+# needed only if you use the docker pipe
 services:
 - docker
 
+script:
+  - go test ./... # replace this with your test script
+  - curl -sfL https://git.io/goreleaser | sh -s -- check # check goreleaser config for deprecations
+
 after_success:
 # docker login is required if you want to push docker images.
 # DOCKER_PASSWORD should be a secret in your .travis.yml configuration.
diff --git a/www/content/templates.md b/www/content/templates.md
index 1f3e86bf..25eada2e 100644
--- a/www/content/templates.md
+++ b/www/content/templates.md
@@ -57,6 +57,7 @@ On all fields, you have these available functions:
 | `tolower "V1.2"`        | makes input string lowercase. See [ToLower](https://golang.org/pkg/strings/#ToLower)                     |
 | `toupper "v1.2"`        | makes input string uppercase. See [ToUpper](https://golang.org/pkg/strings/#ToUpper)                     |
 | `trim " v1.2  "`        | removes all leading and trailing white space. See [TrimSpace](https://golang.org/pkg/strings/#TrimSpace) |
+| `dir .Path`             | returns all but the last element of path, typically the path's directory. See [Dir](https://golang.org/pkg/path/filepath/#Dir)
 
 With all those fields, you may be able to compose the name of your artifacts
 pretty much the way you want:


Test patch:
diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go
index 21376c50..aa76a30b 100644
--- a/internal/builders/golang/build_test.go
+++ b/internal/builders/golang/build_test.go
@@ -400,7 +400,7 @@ func TestRunPipeWithoutMainFunc(t *testing.T) {
 		Builds: []config.Build{
 			{
 				Binary: "no-main",
-				Hooks:  config.Hooks{},
+				Hooks:  config.HookConfig{},
 				Targets: []string{
 					runtimeTarget,
 				},
@@ -448,7 +448,7 @@ func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) {
 			{
 				Env:    []string{"GO111MODULE=off"},
 				Binary: "foo",
-				Hooks:  config.Hooks{},
+				Hooks:  config.HookConfig{},
 				Targets: []string{
 					runtimeTarget,
 				},
diff --git a/internal/deprecate/deprecate_test.go b/internal/deprecate/deprecate_test.go
index 4394533a..31a41ffe 100644
--- a/internal/deprecate/deprecate_test.go
+++ b/internal/deprecate/deprecate_test.go
@@ -8,6 +8,8 @@ import (
 	"github.com/apex/log"
 	"github.com/apex/log/handlers/cli"
 	"github.com/fatih/color"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/require"
 )
 
@@ -21,8 +23,10 @@ func TestNotice(t *testing.T) {
 	log.SetHandler(cli.New(f))
 
 	log.Info("first")
-	Notice("foo.bar.whatever")
+	var ctx = context.New(config.Project{})
+	Notice(ctx, "foo.bar.whatever")
 	log.Info("last")
+	require.True(t, ctx.Deprecated)
 
 	require.NoError(t, f.Close())
 
diff --git a/internal/pipe/before/before_test.go b/internal/pipe/before/before_test.go
index c717f78e..e47f9468 100644
--- a/internal/pipe/before/before_test.go
+++ b/internal/pipe/before/before_test.go
@@ -20,6 +20,7 @@ func TestRunPipe(t *testing.T) {
 		{},
 		{"go version"},
 		{"go version", "go list"},
+		{`bash -c "go version; echo \"lala spaces and such\""`},
 	} {
 		ctx := context.New(
 			config.Project{
@@ -32,6 +33,21 @@ func TestRunPipe(t *testing.T) {
 	}
 }
 
+func TestRunPipeInvalidCommand(t *testing.T) {
+	for _, tc := range [][]string{
+		{`bash -c "echo \"unterminated command\"`},
+	} {
+		ctx := context.New(
+			config.Project{
+				Before: config.Before{
+					Hooks: tc,
+				},
+			},
+		)
+		require.Error(t, Pipe{}.Run(ctx))
+	}
+}
+
 func TestRunPipeFail(t *testing.T) {
 	for _, tc := range [][]string{
 		{"go tool foobar"},
diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go
index d8579859..f007cf43 100644
--- a/internal/pipe/build/build_test.go
+++ b/internal/pipe/build/build_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var fakeArtifact = &artifact.Artifact{
@@ -79,7 +80,9 @@ func TestBuild(t *testing.T) {
 		Version: "1.2.3",
 		Config:  config,
 	}
-	error := doBuild(ctx, ctx.Config.Builds[0], "darwin_amd64")
+	opts, err := buildOptionsForTarget(ctx, ctx.Config.Builds[0], "darwin_amd64")
+	assert.NoError(t, err)
+	error := doBuild(ctx, ctx.Config.Builds[0], *opts)
 	assert.NoError(t, error)
 }
 
@@ -117,9 +120,13 @@ func TestRunFullPipe(t *testing.T) {
 				Binary:  "testing",
 				Flags:   []string{"-v"},
 				Ldflags: []string{"-X main.test=testing"},
-				Hooks: config.Hooks{
-					Pre:  "touch " + pre,
-					Post: "touch " + post,
+				Hooks: config.HookConfig{
+					Pre: []config.BuildHook{
+						{Cmd: "touch " + pre},
+					},
+					Post: []config.BuildHook{
+						{Cmd: "touch " + post},
+					},
 				},
 				Targets: []string{"whatever"},
 			},
@@ -148,9 +155,13 @@ func TestRunFullPipeFail(t *testing.T) {
 				Binary:  "testing",
 				Flags:   []string{"-v"},
 				Ldflags: []string{"-X main.test=testing"},
-				Hooks: config.Hooks{
-					Pre:  "touch " + pre,
-					Post: "touch " + post,
+				Hooks: config.HookConfig{
+					Pre: []config.BuildHook{
+						{Cmd: "touch " + pre},
+					},
+					Post: []config.BuildHook{
+						{Cmd: "touch " + post},
+					},
 				},
 				Targets: []string{"whatever"},
 			},
@@ -166,29 +177,29 @@ func TestRunFullPipeFail(t *testing.T) {
 func TestRunPipeFailingHooks(t *testing.T) {
 	folder, back := testlib.Mktmp(t)
 	defer back()
-	var config = config.Project{
+	var cfg = config.Project{
 		Dist: folder,
 		Builds: []config.Build{
 			{
 				Lang:    "fake",
 				Binary:  "hooks",
-				Hooks:   config.Hooks{},
+				Hooks:   config.HookConfig{},
 				Targets: []string{"whatever"},
 			},
 		},
 	}
 	t.Run("pre-hook", func(t *testing.T) {
-		var ctx = context.New(config)
+		var ctx = context.New(cfg)
 		ctx.Git.CurrentTag = "2.3.4"
-		ctx.Config.Builds[0].Hooks.Pre = "exit 1"
-		ctx.Config.Builds[0].Hooks.Post = "echo post"
+		ctx.Config.Builds[0].Hooks.Pre = []config.BuildHook{{Cmd: "exit 1"}}
+		ctx.Config.Builds[0].Hooks.Post = []config.BuildHook{{Cmd: "echo post"}}
 		assert.EqualError(t, Pipe{}.Run(ctx), `pre hook failed: "": exec: "exit": executable file not found in $PATH`)
 	})
 	t.Run("post-hook", func(t *testing.T) {
-		var ctx = context.New(config)
+		var ctx = context.New(cfg)
 		ctx.Git.CurrentTag = "2.3.4"
-		ctx.Config.Builds[0].Hooks.Pre = "echo pre"
-		ctx.Config.Builds[0].Hooks.Post = "exit 1"
+		ctx.Config.Builds[0].Hooks.Pre = []config.BuildHook{{Cmd: "echo pre"}}
+		ctx.Config.Builds[0].Hooks.Post = []config.BuildHook{{Cmd: "exit 1"}}
 		assert.EqualError(t, Pipe{}.Run(ctx), `post hook failed: "": exec: "exit": executable file not found in $PATH`)
 	})
 }
@@ -395,7 +406,7 @@ func TestTemplate(t *testing.T) {
 	assert.Contains(t, binary, `-X "main.foo=123"`)
 }
 
-func TestHookEnvs(t *testing.T) {
+func TestRunHookEnvs(t *testing.T) {
 	tmp, back := testlib.Mktmp(t)
 	defer back()
 
@@ -406,26 +417,166 @@ func TestHookEnvs(t *testing.T) {
 		},
 	}
 
-	t.Run("valid template", func(t *testing.T) {
+	var opts = api.Options{
+		Name:   "binary-name",
+		Path:   "./binary-name",
+		Target: "darwin_amd64",
+	}
+
+	simpleHook := func(cmd string) config.BuildHooks {
+		return []config.BuildHook{{Cmd: cmd}}
+	}
+
+	t.Run("valid cmd template with ctx env", func(t *testing.T) {
 		var err = runHook(context.New(config.Project{
 			Builds: []config.Build{
 				build,
 			},
-		}), build.Env, "touch {{ .Env.FOO }}")
+			Env: []string{
+				fmt.Sprintf("CTXFOO=%s/foo", tmp),
+			},
+		}), opts, []string{}, simpleHook("touch {{ .Env.CTXFOO }}"))
 		assert.NoError(t, err)
 		assert.FileExists(t, filepath.Join(tmp, "foo"))
 	})
 
-	t.Run("invalid template", func(t *testing.T) {
+	t.Run("valid cmd template with build env", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+		}), opts, build.Env, simpleHook("touch {{ .Env.FOO }}"))
+		assert.NoError(t, err)
+		assert.FileExists(t, filepath.Join(tmp, "foo"))
+	})
+
+	t.Run("valid cmd template with hook env", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+		}), opts, []string{}, []config.BuildHook{{
+			Cmd: "touch {{ .Env.HOOK_ONLY_FOO }}",
+			Env: []string{
+				fmt.Sprintf("HOOK_ONLY_FOO=%s/hook_only", tmp),
+			},
+		}})
+		assert.NoError(t, err)
+		assert.FileExists(t, filepath.Join(tmp, "hook_only"))
+	})
+
+	t.Run("valid cmd template with ctx and build env", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+			Env: []string{
+				fmt.Sprintf("OVER_FOO=%s/ctx_over_build", tmp),
+			},
+		}), opts, []string{
+			fmt.Sprintf("OVER_FOO=%s/build_over_ctx", tmp),
+		}, simpleHook("touch {{ .Env.OVER_FOO }}"))
+		assert.NoError(t, err)
+
+		assert.FileExists(t, filepath.Join(tmp, "build_over_ctx"))
+		assert.NoFileExists(t, filepath.Join(tmp, "ctx_over_build"))
+	})
+
+	t.Run("valid cmd template with ctx and hook env", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+			Env: []string{
+				fmt.Sprintf("CTX_OR_HOOK_FOO=%s/ctx_over_hook", tmp),
+			},
+		}), opts, []string{}, []config.BuildHook{{
+			Cmd: "touch {{ .Env.CTX_OR_HOOK_FOO }}",
+			Env: []string{
+				fmt.Sprintf("CTX_OR_HOOK_FOO=%s/hook_over_ctx", tmp),
+			},
+		}})
+		assert.NoError(t, err)
+		assert.FileExists(t, filepath.Join(tmp, "hook_over_ctx"))
+		assert.NoFileExists(t, filepath.Join(tmp, "ctx_over_hook"))
+	})
+
+	t.Run("valid cmd template with build and hook env", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+		}), opts, []string{
+			fmt.Sprintf("BUILD_OR_HOOK_FOO=%s/build_over_hook", tmp),
+		}, []config.BuildHook{{
+			Cmd: "touch {{ .Env.BUILD_OR_HOOK_FOO }}",
+			Env: []string{
+				fmt.Sprintf("BUILD_OR_HOOK_FOO=%s/hook_over_build", tmp),
+			},
+		}})
+		assert.NoError(t, err)
+		assert.FileExists(t, filepath.Join(tmp, "hook_over_build"))
+		assert.NoFileExists(t, filepath.Join(tmp, "build_over_hook"))
+	})
+
+	t.Run("valid cmd template with ctx, build and hook env", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+			Env: []string{
+				fmt.Sprintf("CTX_OR_BUILD_OR_HOOK_FOO=%s/ctx_wins", tmp),
+			},
+		}), opts, []string{
+			fmt.Sprintf("CTX_OR_BUILD_OR_HOOK_FOO=%s/build_wins", tmp),
+		}, []config.BuildHook{{
+			Cmd: "touch {{ .Env.CTX_OR_BUILD_OR_HOOK_FOO }}",
+			Env: []string{
+				fmt.Sprintf("CTX_OR_BUILD_OR_HOOK_FOO=%s/hook_wins", tmp),
+			},
+		}})
+		assert.NoError(t, err)
+		assert.FileExists(t, filepath.Join(tmp, "hook_wins"))
+		assert.NoFileExists(t, filepath.Join(tmp, "ctx_wins"))
+		assert.NoFileExists(t, filepath.Join(tmp, "build_wins"))
+	})
+
+	t.Run("invalid cmd template", func(t *testing.T) {
 		var err = runHook(context.New(config.Project{
 			Builds: []config.Build{
 				build,
 			},
-		}), build.Env, "touch {{ .Env.FOOss }}")
+		}), opts, build.Env, simpleHook("touch {{ .Env.FOOss }}"))
 		assert.EqualError(t, err, `template: tmpl:1:13: executing "tmpl" at <.Env.FOOss>: map has no entry for key "FOOss"`)
 	})
 
-	t.Run("env inside shell", func(t *testing.T) {
+	t.Run("invalid dir template", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+		}), opts, build.Env, []config.BuildHook{{
+			Cmd: "echo something",
+			Dir: "{{ .Env.INVALID_ENV }}",
+		}})
+		assert.EqualError(t, err, `template: tmpl:1:7: executing "tmpl" at <.Env.INVALID_ENV>: map has no entry for key "INVALID_ENV"`)
+	})
+
+	t.Run("invalid hook env template", func(t *testing.T) {
+		var err = runHook(context.New(config.Project{
+			Builds: []config.Build{
+				build,
+			},
+		}), opts, build.Env, []config.BuildHook{{
+			Cmd: "echo something",
+			Env: []string{
+				"TEST={{ .Env.MISSING_ENV }}",
+			},
+		}})
+		assert.EqualError(t, err, `template: tmpl:1:12: executing "tmpl" at <.Env.MISSING_ENV>: map has no entry for key "MISSING_ENV"`)
+	})
+
+	t.Run("build env inside shell", func(t *testing.T) {
 		var shell = `#!/bin/sh -e
 touch "$BAR"`
 		err := ioutil.WriteFile(filepath.Join(tmp, "test.sh"), []byte(shell), 0750)
@@ -434,8 +585,112 @@ touch "$BAR"`
 			Builds: []config.Build{
 				build,
 			},
-		}), build.Env, "sh test.sh")
+		}), opts, build.Env, simpleHook("sh test.sh"))
 		assert.NoError(t, err)
 		assert.FileExists(t, filepath.Join(tmp, "bar"))
 	})
 }
+
+func TestPipeOnBuild_hooksRunPerTarget(t *testing.T) {
+	tmpDir, back := testlib.Mktmp(t)
+	defer back()
+
+	build := config.Build{
+		Lang:   "fake",
+		Binary: "testing.v{{.Version}}",
+		Targets: []string{
+			"linux_amd64",
+			"darwin_amd64",
+			"windows_amd64",
+		},
+		Hooks: config.HookConfig{
+			Pre: []config.BuildHook{
+				{Cmd: "touch pre-hook-{{.Target}}", Dir: tmpDir},
+			},
+			Post: config.BuildHooks{
+				{Cmd: "touch post-hook-{{.Target}}", Dir: tmpDir},
+			},
+		},
+	}
+	ctx := context.New(config.Project{
+		Builds: []config.Build{
+			build,
+		},
+	})
+	err := runPipeOnBuild(ctx, build)
+	assert.NoError(t, err)
+	assert.FileExists(t, filepath.Join(tmpDir, "pre-hook-linux_amd64"))
+	assert.FileExists(t, filepath.Join(tmpDir, "pre-hook-darwin_amd64"))
+	assert.FileExists(t, filepath.Join(tmpDir, "pre-hook-windows_amd64"))
+	assert.FileExists(t, filepath.Join(tmpDir, "post-hook-linux_amd64"))
+	assert.FileExists(t, filepath.Join(tmpDir, "post-hook-darwin_amd64"))
+	assert.FileExists(t, filepath.Join(tmpDir, "post-hook-windows_amd64"))
+}
+
+func TestPipeOnBuild_invalidBinaryTpl(t *testing.T) {
+	build := config.Build{
+		Lang:   "fake",
+		Binary: "testing.v{{.XYZ}}",
+		Targets: []string{
+			"linux_amd64",
+		},
+	}
+	ctx := context.New(config.Project{
+		Builds: []config.Build{
+			build,
+		},
+	})
+	err := runPipeOnBuild(ctx, build)
+	assert.EqualError(t, err, `template: tmpl:1:11: executing "tmpl" at <.XYZ>: map has no entry for key "XYZ"`)
+}
+
+func TestBuildOptionsForTarget(t *testing.T) {
+	tmpDir, back := testlib.Mktmp(t)
+	defer back()
+
+	build := config.Build{
+		ID:     "testid",
+		Binary: "testbinary",
+		Targets: []string{
+			"linux_amd64",
+			"darwin_amd64",
+			"windows_amd64",
+		},
+	}
+	ctx := context.New(config.Project{
+		Dist:   tmpDir,
+		Builds: []config.Build{build},
+	})
+	opts, err := buildOptionsForTarget(ctx, build, "linux_amd64")
+	assert.NoError(t, err)
+	assert.Equal(t, &api.Options{
+		Name:   "testbinary",
+		Path:   filepath.Join(tmpDir, "testid_linux_amd64", "testbinary"),
+		Target: "linux_amd64",
+	}, opts)
+}
+
+func TestHookComplex(t *testing.T) {
+	tmp, back := testlib.Mktmp(t)
+	defer back()
+
+	require.NoError(t, runHook(context.New(config.Project{}), api.Options{}, []string{}, config.BuildHooks{
+		{
+			Cmd: `bash -c "touch foo"`,
+		},
+		{
+			Cmd: `bash -c "touch \"bar\""`,
+		},
+	}))
+
+	require.FileExists(t, filepath.Join(tmp, "foo"))
+	require.FileExists(t, filepath.Join(tmp, "bar"))
+}
+
+func TestHookInvalidShelCommand(t *testing.T) {
+	require.Error(t, runHook(context.New(config.Project{}), api.Options{}, []string{}, config.BuildHooks{
+		{
+			Cmd: `bash -c "echo \"unterminated command\"`,
+		},
+	}))
+}
diff --git a/main_test.go b/main_test.go
index ff7dc9cf..b4275313 100644
--- a/main_test.go
+++ b/main_test.go
@@ -22,13 +22,15 @@ func init() {
 func TestReleaseProject(t *testing.T) {
 	_, back := setup(t)
 	defer back()
-	assert.NoError(t, releaseProject(testParams()))
+	_, err := releaseProject(testParams())
+	assert.NoError(t, err)
 }
 
 func TestCheckConfig(t *testing.T) {
 	_, back := setup(t)
 	defer back()
-	assert.NoError(t, checkConfig(testParams().Config))
+	_, err := checkConfig(testParams().Config)
+	assert.NoError(t, err)
 }
 
 func TestCheckConfigFails(t *testing.T) {
@@ -36,7 +38,8 @@ func TestCheckConfigFails(t *testing.T) {
 	defer back()
 	var filename = "fail.yaml"
 	assert.NoError(t, ioutil.WriteFile(filename, []byte("nope: 1"), 0644))
-	assert.Error(t, checkConfig(filename))
+	_, err := checkConfig(filename)
+	assert.Error(t, err)
 }
 
 func TestReleaseProjectSkipPublish(t *testing.T) {
@@ -45,7 +48,8 @@ func TestReleaseProjectSkipPublish(t *testing.T) {
 	params := testParams()
 	params.Snapshot = true
 	params.SkipPublish = true
-	assert.NoError(t, releaseProject(params))
+	_, err := releaseProject(params)
+	assert.NoError(t, err)
 }
 
 func TestConfigFileIsSetAndDontExist(t *testing.T) {
@@ -53,7 +57,8 @@ func TestConfigFileIsSetAndDontExist(t *testing.T) {
 	defer back()
 	params := testParams()
 	params.Config = "/this/wont/exist"
-	assert.Error(t, releaseProject(params))
+	_, err := releaseProject(params)
+	assert.Error(t, err)
 }
 
 func TestConfigFlagNotSetButExists(t *testing.T) {
@@ -93,7 +98,8 @@ func TestReleaseNotesFileDontExist(t *testing.T) {
 	defer back()
 	params := testParams()
 	params.ReleaseNotes = "/this/also/wont/exist"
-	assert.Error(t, releaseProject(params))
+	_, err := releaseProject(params)
+	assert.Error(t, err)
 }
 
 func TestCustomReleaseNotesFile(t *testing.T) {
@@ -104,7 +110,8 @@ func TestCustomReleaseNotesFile(t *testing.T) {
 	createFile(t, releaseNotes.Name(), "nothing important at all")
 	var params = testParams()
 	params.ReleaseNotes = releaseNotes.Name()
-	assert.NoError(t, releaseProject(params))
+	_, err = releaseProject(params)
+	assert.NoError(t, err)
 }
 
 func TestCustomReleaseHeaderFileDontExist(t *testing.T) {
@@ -113,7 +120,8 @@ func TestCustomReleaseHeaderFileDontExist(t *testing.T) {
 	params := testParams()
 	params.ReleaseHeader = "/header/that/dont/exist"
 	params.Snapshot = false
-	assert.Error(t, releaseProject(params))
+	_, err := releaseProject(params)
+	assert.Error(t, err)
 }
 
 func TestCustomReleaseHeaderFile(t *testing.T) {
@@ -126,7 +134,8 @@ func TestCustomReleaseHeaderFile(t *testing.T) {
 	params.ReleaseHeader = releaseHeader.Name()
 	params.Snapshot = false
 	params.SkipPublish = true
-	assert.NoError(t, releaseProject(params))
+	_, err = releaseProject(params)
+	assert.NoError(t, err)
 }
 
 func TestCustomReleaseFooterFileDontExist(t *testing.T) {
@@ -135,7 +144,8 @@ func TestCustomReleaseFooterFileDontExist(t *testing.T) {
 	params := testParams()
 	params.ReleaseFooter = "/footer/that/dont/exist"
 	params.Snapshot = false
-	assert.Error(t, releaseProject(params))
+	_, err := releaseProject(params)
+	assert.Error(t, err)
 }
 
 func TestCustomReleaseFooterFile(t *testing.T) {
@@ -148,14 +158,16 @@ func TestCustomReleaseFooterFile(t *testing.T) {
 	params.ReleaseFooter = releaseFooter.Name()
 	params.Snapshot = false
 	params.SkipPublish = true
-	assert.NoError(t, releaseProject(params))
+	_, 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")
-	assert.Error(t, releaseProject(testParams()))
+	_, err := releaseProject(testParams())
+	assert.Error(t, err)
 }
 
 func TestInitProject(t *testing.T) {
@@ -169,8 +181,7 @@ func TestInitProject(t *testing.T) {
 	out, err := ioutil.ReadAll(file)
 	assert.NoError(t, err)
 
-	var config = config.Project{}
-	assert.NoError(t, yaml.Unmarshal(out, &config))
+	assert.NoError(t, yaml.Unmarshal(out, &config.Project{}))
 }
 
 func TestInitProjectFileExist(t *testing.T) {
diff --git a/pkg/config/config_build_hook_test.go b/pkg/config/config_build_hook_test.go
new file mode 100644
index 00000000..355e2328
--- /dev/null
+++ b/pkg/config/config_build_hook_test.go
@@ -0,0 +1,55 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	yaml "gopkg.in/yaml.v2"
+)
+
+func TestBuildHook_justString(t *testing.T) {
+	var actual HookConfig
+
+	err := yaml.UnmarshalStrict([]byte(`pre: ./script.sh`), &actual)
+	assert.NoError(t, err)
+	assert.Equal(t, BuildHook{
+		Cmd: "./script.sh",
+		Env: nil,
+	}, actual.Pre[0])
+}
+
+func TestBuildHook_stringCmds(t *testing.T) {
+	var actual HookConfig
+
+	err := yaml.UnmarshalStrict([]byte(`pre:
+ - ./script.sh
+ - second-script.sh
+`), &actual)
+	assert.NoError(t, err)
+
+	assert.Equal(t, BuildHooks{
+		{
+			Cmd: "./script.sh",
+			Env: nil,
+		},
+		{
+			Cmd: "second-script.sh",
+			Env: nil,
+		},
+	}, actual.Pre)
+}
+
+func TestBuildHook_complex(t *testing.T) {
+	var actual HookConfig
+
+	err := yaml.UnmarshalStrict([]byte(`pre:
+ - cmd: ./script.sh
+   env:
+    - TEST=value
+`), &actual)
+	assert.NoError(t, err)
+	assert.Equal(t, BuildHook{
+		Cmd: "./script.sh",
+		Env: []string{"TEST=value"},
+	}, actual.Pre[0])
+}