diff --git a/docs/usage.md b/docs/usage.md index a3a6a304..609f9c71 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -33,8 +33,10 @@ executable called must be available by the OS or in PATH. If you omit a task name, "default" will be assumed. -## Environment +## Environment variables + +### Task You can use `env` to set custom environment variables for a specific task: ```yaml @@ -66,6 +68,39 @@ tasks: > NOTE: `env` supports expansion and retrieving output from a shell command > just like variables, as you can see on the [Variables](#variables) section. + +### Operating System +Environment variables from the OS are accessible using `$VARNAME`: + +```yaml +version: '2' + +tasks: + greet: + cmds: + - echo "Hello $USER" +``` + +### .env + +*.env* files are supported in v3 using the `dotenv` declaration: + +.env +``` +KEYNAME=VALUE +``` +Taskfile.yml +```yaml +version: '3' + +dotenv: ['.env'] + +tasks: + greet: + cmds: + - echo "Using $KEYNAME" +``` + ## Operating System specific tasks If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your Taskfile diff --git a/go.mod b/go.mod index 82847d3d..180b78a8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/go-task/task/v2 require ( github.com/fatih/color v1.7.0 github.com/go-task/slim-sprig v0.0.0-20200516131648-f9bac4e523eb + github.com/joho/godotenv v1.3.0 github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-zglob v0.0.1 github.com/radovskyb/watcher v1.0.5 diff --git a/go.sum b/go.sum index 85e5048f..8cc5a938 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/go-task/slim-sprig v0.0.0-20200516131648-f9bac4e523eb h1:/qbv1F67s6ehqX9mG23cJOeca3FWpOVKgtPfPUMAi0k= github.com/go-task/slim-sprig v0.0.0-20200516131648-f9bac4e523eb/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= diff --git a/internal/taskfile/read/taskfile.go b/internal/taskfile/read/taskfile.go index 8bbe919f..70bee710 100644 --- a/internal/taskfile/read/taskfile.go +++ b/internal/taskfile/read/taskfile.go @@ -3,6 +3,7 @@ package read import ( "errors" "fmt" + "github.com/joho/godotenv" "os" "path/filepath" "runtime" @@ -16,6 +17,7 @@ import ( var ( // ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile") + ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile") ) // Taskfile reads a Taskfile for a given directory @@ -34,6 +36,22 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) { return nil, err } + if v >= 3.0 { + if len(t.Dotenv) > 0 { + for _, envFile := range t.Dotenv { + var envFilePath string + if filepath.IsAbs(envFile) { + envFilePath = envFile + } else { + envFilePath = filepath.Join(dir, envFile) + } + if err = godotenv.Load(envFilePath); err != nil { + return nil, err + } + } + } + } + for namespace, includedTask := range t.Includes { if v >= 3.0 { tr := templater.Templater{Vars: &taskfile.Vars{}, RemoveNoValue: true} @@ -68,6 +86,12 @@ func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) { return nil, ErrIncludedTaskfilesCantHaveIncludes } + if v >= 3.0 { + if len(includedTaskfile.Dotenv) > 0 { + return nil, ErrIncludedTaskfilesCantHaveDotenvs + } + } + if includedTask.AdvancedImport { for _, task := range includedTaskfile.Tasks { if !filepath.IsAbs(task.Dir) { diff --git a/internal/taskfile/taskfile.go b/internal/taskfile/taskfile.go index d9a17ac0..5c6308d2 100644 --- a/internal/taskfile/taskfile.go +++ b/internal/taskfile/taskfile.go @@ -16,6 +16,7 @@ type Taskfile struct { Env *Vars Tasks Tasks Silent bool + Dotenv []string } // UnmarshalYAML implements yaml.Unmarshaler interface @@ -30,6 +31,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { Env *Vars Tasks Tasks Silent bool + Dotenv []string } if err := unmarshal(&taskfile); err != nil { return err @@ -43,6 +45,7 @@ func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { tf.Env = taskfile.Env tf.Tasks = taskfile.Tasks tf.Silent = taskfile.Silent + tf.Dotenv = taskfile.Dotenv if tf.Expansions <= 0 { tf.Expansions = 2 } diff --git a/task_test.go b/task_test.go index e8bbe438..87a5552a 100644 --- a/task_test.go +++ b/task_test.go @@ -816,3 +816,50 @@ func TestShortTaskNotation(t *testing.T) { assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"})) assert.Equal(t, "string-slice-1\nstring-slice-2\nstring\n", buff.String()) } + +func TestDotenvShouldIncludeAllEnvFiles(t *testing.T) { + tt := fileContentTest{ + Dir: "testdata/dotenv", + Target: "default", + TrimSpace: false, + Files: map[string]string{ + "include.txt": "INCLUDE1='from_include1' INCLUDE2='from_include2'\n", + }, + } + tt.Run(t) +} + +func TestDotenvShouldErrorWithIncludeEnvPath(t *testing.T) { + const dir = "testdata/dotenv" + const entry = "Taskfile-errors1.yml" + + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Entrypoint: entry, + Summary: true, + Stdout: &buff, + Stderr: &buff, + } + err := e.Setup() + assert.Error(t, err) + assert.Contains(t, err.Error(), "no such file") +} + +func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) { + const dir = "testdata/dotenv" + const entry = "Taskfile-errors2.yml" + + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Entrypoint: entry, + Summary: true, + Stdout: &buff, + Stderr: &buff, + } + + err := e.Setup() + assert.Error(t, err) + assert.Contains(t, err.Error(), "move the dotenv") +} diff --git a/testdata/dotenv/.gitignore b/testdata/dotenv/.gitignore new file mode 100644 index 00000000..2211df63 --- /dev/null +++ b/testdata/dotenv/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/testdata/dotenv/Taskfile-errors1.yml b/testdata/dotenv/Taskfile-errors1.yml new file mode 100644 index 00000000..8551f165 --- /dev/null +++ b/testdata/dotenv/Taskfile-errors1.yml @@ -0,0 +1,8 @@ +version: '3' + +dotenv: ['include1/.env', 'include1/envs/.env', 'file-does-not-exist'] + +tasks: + default: + cmds: + - echo "INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'" > include-errors1.txt diff --git a/testdata/dotenv/Taskfile-errors2.yml b/testdata/dotenv/Taskfile-errors2.yml new file mode 100644 index 00000000..7da25c5e --- /dev/null +++ b/testdata/dotenv/Taskfile-errors2.yml @@ -0,0 +1,9 @@ +version: '3' + +includes: + include1: './include1' + +tasks: + default: + cmds: + - echo "INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'" > include-errors2.txt diff --git a/testdata/dotenv/Taskfile.yml b/testdata/dotenv/Taskfile.yml new file mode 100644 index 00000000..f4b775fc --- /dev/null +++ b/testdata/dotenv/Taskfile.yml @@ -0,0 +1,8 @@ +version: '3' + +dotenv: ['include1/.env', 'include1/envs/.env'] + +tasks: + default: + cmds: + - echo "INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'" > include.txt diff --git a/testdata/dotenv/include1/.env b/testdata/dotenv/include1/.env new file mode 100644 index 00000000..15bd90a8 --- /dev/null +++ b/testdata/dotenv/include1/.env @@ -0,0 +1 @@ +INCLUDE1=from_include1 diff --git a/testdata/dotenv/include1/Taskfile.yml b/testdata/dotenv/include1/Taskfile.yml new file mode 100644 index 00000000..43a4b6b7 --- /dev/null +++ b/testdata/dotenv/include1/Taskfile.yml @@ -0,0 +1,3 @@ +version: '3' + +dotenv: ['.env'] diff --git a/testdata/dotenv/include1/envs/.env b/testdata/dotenv/include1/envs/.env new file mode 100644 index 00000000..17a38fd3 --- /dev/null +++ b/testdata/dotenv/include1/envs/.env @@ -0,0 +1 @@ +INCLUDE2=from_include2