1
0
mirror of https://github.com/go-task/task.git synced 2025-01-04 03:48:02 +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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 155 additions and 0 deletions

View File

@ -2,6 +2,8 @@
## 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`
([#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/)

View File

@ -130,6 +130,7 @@ includes:
| `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. |
| `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. |
| `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`. |

View File

@ -118,6 +118,45 @@ tasks:
- 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
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.",
"$ref": "#/definitions/3/env"
},
"dotenv": {
"description": "A list of `.env` file paths to be parsed.",
"type": "array",
"items": {
"type": "string"
}
},
"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.",
"type": "boolean",

View File

@ -1410,6 +1410,54 @@ func TestDotenvHasEnvVarInPath(t *testing.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) {
const dir = "testdata/exit_immediately"

View File

@ -19,6 +19,7 @@ type Task struct {
Dir string
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Interactive bool
Internal bool
@ -65,6 +66,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
Dir string
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Interactive bool
Internal bool
@ -89,6 +91,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
t.Dir = task.Dir
t.Vars = task.Vars
t.Env = task.Env
t.Dotenv = task.Dotenv
t.Silent = task.Silent
t.Interactive = task.Interactive
t.Internal = task.Internal
@ -117,6 +120,7 @@ func (t *Task) DeepCopy() *Task {
Dir: t.Dir,
Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(),
Dotenv: deepCopySlice(t.Dotenv),
Silent: t.Silent,
Interactive: t.Interactive,
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
import (
"os"
"strings"
"github.com/joho/godotenv"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"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),
Vars: nil,
Env: nil,
Dotenv: r.ReplaceSlice(origTask.Dotenv),
Silent: origTask.Silent,
Interactive: origTask.Interactive,
Internal: origTask.Internal,
@ -76,8 +80,28 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
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.Merge(r.ReplaceVars(e.Taskfile.Env))
new.Env.Merge(r.ReplaceVars(dotenvEnvs))
new.Env.Merge(r.ReplaceVars(origTask.Env))
if evaluateShVars {
err = new.Env.Range(func(k string, v taskfile.Var) error {