1
0
mirror of https://github.com/go-task/task.git synced 2025-11-27 22:38:20 +02:00

feat: add task-level dotenv support (#904)

This commit is contained in:
Pete Davison
2022-12-06 00:25:16 +00:00
committed by GitHub
parent 9cf930454d
commit 99d7338c29
10 changed files with 155 additions and 0 deletions

View File

@@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- Add task-level `dotenv` support
([#389](https://github.com/go-task/task/issues/389), [#904](https://github.com/go-task/task/pull/904)).
- It's now possible to use global level variables on `includes` - It's now possible to use global level variables on `includes`
([#942](https://github.com/go-task/task/issues/942), [#943](https://github.com/go-task/task/pull/943)). ([#942](https://github.com/go-task/task/issues/942), [#943](https://github.com/go-task/task/pull/943)).
- The website got a brand new [translation to Chinese](https://task-zh.readthedocs.io/zh_CN/latest/) - The website got a brand new [translation to Chinese](https://task-zh.readthedocs.io/zh_CN/latest/)

View File

@@ -130,6 +130,7 @@ includes:
| `dir` | `string` | | The directory in which this task should run. Defaults to the current working directory. | | `dir` | `string` | | The directory in which this task should run. Defaults to the current working directory. |
| `vars` | [`map[string]Variable`](#variable) | | A set of variables that can be used in the task. | | `vars` | [`map[string]Variable`](#variable) | | A set of variables that can be used in the task. |
| `env` | [`map[string]Variable`](#variable) | | A set of environment variables that will be made available to shell commands. | | `env` | [`map[string]Variable`](#variable) | | A set of environment variables that will be made available to shell commands. |
| `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. |
| `silent` | `bool` | `false` | Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. When combined with the `--list` flag, task descriptions will be hidden. | | `silent` | `bool` | `false` | Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. When combined with the `--list` flag, task descriptions will be hidden. |
| `interactive` | `bool` | `false` | Tells task that the command is interactive. | | `interactive` | `bool` | `false` | Tells task that the command is interactive. |
| `internal` | `bool` | `false` | Stops a task from being callable on the command line. It will also be omitted from the output when used with `--list`. | | `internal` | `bool` | `false` | Stops a task from being callable on the command line. It will also be omitted from the output when used with `--list`. |

View File

@@ -118,6 +118,45 @@ tasks:
- echo "Using $KEYNAME and endpoint $ENDPOINT" - echo "Using $KEYNAME and endpoint $ENDPOINT"
``` ```
Dotenv files can also be specified at the task level:
```yaml
version: '3'
env:
ENV: testing
tasks:
greet:
dotenv: ['.env', '{{.ENV}}/.env.', '{{.HOME}}/.env']
cmds:
- echo "Using $KEYNAME and endpoint $ENDPOINT"
```
Environment variables specified explicitly at the task-level will override
variables defined in dotfiles:
```yaml
version: '3'
env:
ENV: testing
tasks:
greet:
dotenv: ['.env', '{{.ENV}}/.env.', '{{.HOME}}/.env']
env:
KEYNAME: DIFFERENT_VALUE
cmds:
- echo "Using $KEYNAME and endpoint $ENDPOINT"
```
:::info
Please note that you are not currently able to use the `dotenv` key inside included Taskfiles.
:::
## Including other Taskfiles ## Including other Taskfiles
If you want to share tasks between different projects (Taskfiles), you can use If you want to share tasks between different projects (Taskfiles), you can use

View File

@@ -116,6 +116,13 @@
"description": "A set of environment variables that will be made available to shell commands.", "description": "A set of environment variables that will be made available to shell commands.",
"$ref": "#/definitions/3/env" "$ref": "#/definitions/3/env"
}, },
"dotenv": {
"description": "A list of `.env` file paths to be parsed.",
"type": "array",
"items": {
"type": "string"
}
},
"silent": { "silent": {
"description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. When combined with the `--list` flag, task descriptions will be hidden.", "description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. When combined with the `--list` flag, task descriptions will be hidden.",
"type": "boolean", "type": "boolean",

View File

@@ -1410,6 +1410,54 @@ func TestDotenvHasEnvVarInPath(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestTaskDotenv(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/dotenv_task/default",
Target: "dotenv",
TrimSpace: true,
Files: map[string]string{
"dotenv.txt": "foo",
},
}
tt.Run(t)
}
func TestTaskDotenvFail(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/dotenv_task/default",
Target: "no-dotenv",
TrimSpace: true,
Files: map[string]string{
"no-dotenv.txt": "global",
},
}
tt.Run(t)
}
func TestTaskDotenvOverriddenByEnv(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/dotenv_task/default",
Target: "dotenv-overridden-by-env",
TrimSpace: true,
Files: map[string]string{
"dotenv-overridden-by-env.txt": "overridden",
},
}
tt.Run(t)
}
func TestTaskDotenvWithVarName(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/dotenv_task/default",
Target: "dotenv-with-var-name",
TrimSpace: true,
Files: map[string]string{
"dotenv-with-var-name.txt": "foo",
},
}
tt.Run(t)
}
func TestExitImmediately(t *testing.T) { func TestExitImmediately(t *testing.T) {
const dir = "testdata/exit_immediately" const dir = "testdata/exit_immediately"

View File

@@ -19,6 +19,7 @@ type Task struct {
Dir string Dir string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string
Silent bool Silent bool
Interactive bool Interactive bool
Internal bool Internal bool
@@ -65,6 +66,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
Dir string Dir string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string
Silent bool Silent bool
Interactive bool Interactive bool
Internal bool Internal bool
@@ -89,6 +91,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
t.Dir = task.Dir t.Dir = task.Dir
t.Vars = task.Vars t.Vars = task.Vars
t.Env = task.Env t.Env = task.Env
t.Dotenv = task.Dotenv
t.Silent = task.Silent t.Silent = task.Silent
t.Interactive = task.Interactive t.Interactive = task.Interactive
t.Internal = task.Internal t.Internal = task.Internal
@@ -117,6 +120,7 @@ func (t *Task) DeepCopy() *Task {
Dir: t.Dir, Dir: t.Dir,
Vars: t.Vars.DeepCopy(), Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(), Env: t.Env.DeepCopy(),
Dotenv: deepCopySlice(t.Dotenv),
Silent: t.Silent, Silent: t.Silent,
Interactive: t.Interactive, Interactive: t.Interactive,
Internal: t.Internal, Internal: t.Internal,

1
testdata/dotenv_task/default/.env vendored Normal file
View File

@@ -0,0 +1 @@
FOO=foo

View File

@@ -0,0 +1 @@
*.txt

View File

@@ -0,0 +1,28 @@
version: '3'
env:
FOO: global
tasks:
dotenv:
dotenv: ['.env']
cmds:
- echo "$FOO" > dotenv.txt
dotenv-overridden-by-env:
dotenv: ['.env']
env:
FOO: overridden
cmds:
- echo "$FOO" > dotenv-overridden-by-env.txt
dotenv-with-var-name:
vars:
DOTENV: .env
dotenv: ['{{.DOTENV}}']
cmds:
- echo "$FOO" > dotenv-with-var-name.txt
no-dotenv:
cmds:
- echo "$FOO" > no-dotenv.txt

View File

@@ -1,8 +1,11 @@
package task package task
import ( import (
"os"
"strings" "strings"
"github.com/joho/godotenv"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/status" "github.com/go-task/task/v3/internal/status"
@@ -55,6 +58,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Dir: r.Replace(origTask.Dir), Dir: r.Replace(origTask.Dir),
Vars: nil, Vars: nil,
Env: nil, Env: nil,
Dotenv: r.ReplaceSlice(origTask.Dotenv),
Silent: origTask.Silent, Silent: origTask.Silent,
Interactive: origTask.Interactive, Interactive: origTask.Interactive,
Internal: origTask.Internal, Internal: origTask.Internal,
@@ -76,8 +80,28 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
new.Prefix = new.Task new.Prefix = new.Task
} }
dotenvEnvs := &taskfile.Vars{}
if len(new.Dotenv) > 0 {
for _, dotEnvPath := range new.Dotenv {
dotEnvPath = filepathext.SmartJoin(new.Dir, dotEnvPath)
if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) {
continue
}
envs, err := godotenv.Read(dotEnvPath)
if err != nil {
return nil, err
}
for key, value := range envs {
if _, ok := dotenvEnvs.Mapping[key]; !ok {
dotenvEnvs.Set(key, taskfile.Var{Static: value})
}
}
}
}
new.Env = &taskfile.Vars{} new.Env = &taskfile.Vars{}
new.Env.Merge(r.ReplaceVars(e.Taskfile.Env)) new.Env.Merge(r.ReplaceVars(e.Taskfile.Env))
new.Env.Merge(r.ReplaceVars(dotenvEnvs))
new.Env.Merge(r.ReplaceVars(origTask.Env)) new.Env.Merge(r.ReplaceVars(origTask.Env))
if evaluateShVars { if evaluateShVars {
err = new.Env.Range(func(k string, v taskfile.Var) error { err = new.Env.Range(func(k string, v taskfile.Var) error {