From 8229a0e5656a23b3bf3c5f2d15552adcfd8a4d4b Mon Sep 17 00:00:00 2001 From: Timothy Rule <34501912+trulede@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:40:20 +0100 Subject: [PATCH] Exchange values between tasks using dotenv files. --- task.go | 17 +++- task_test.go | 37 +++++++++ testdata/dotenv_task/generated/.gitignore | 1 + testdata/dotenv_task/generated/Taskfile.yml | 47 +++++++++++ variables.go | 87 ++++++++++++--------- 5 files changed, 151 insertions(+), 38 deletions(-) create mode 100644 testdata/dotenv_task/generated/.gitignore create mode 100644 testdata/dotenv_task/generated/Taskfile.yml diff --git a/task.go b/task.go index 54cda92762..c32496827d 100644 --- a/task.go +++ b/task.go @@ -206,8 +206,21 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { if err = e.startExecution(ctx, t, func(ctx context.Context) error { e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task) - if err := e.runDeps(ctx, t); err != nil { - return err + + if len(t.Deps) > 0 { + if err := e.runDeps(ctx, t); err != nil { + return err + } + if len(t.Dotenv) > 0 { + origTask, err := e.GetTask(call) + if err != nil { + return err + } + t.Env, err = e.taskEnv(t, origTask.Env, nil, true) + if err != nil { + return err + } + } } skipFingerprinting := e.ForceAll || (!call.Indirect && e.Force) diff --git a/task_test.go b/task_test.go index 9d54af9740..61996bbe3a 100644 --- a/task_test.go +++ b/task_test.go @@ -1802,6 +1802,43 @@ func TestTaskDotenvWithVarName(t *testing.T) { }) } +func TestTaskDotenvGenerated(t *testing.T) { + t.Parallel() + + tt := []fileContentTest{ + { + Dir: "testdata/dotenv_task/generated", + Target: "dotenv-dep-gen-default", + TrimSpace: true, + Files: map[string]string{ + "dotenv-dep-gen-default.txt": "gen-bar", + }, + }, + { + Dir: "testdata/dotenv_task/generated", + Target: "dotenv-dep-gen-var", + TrimSpace: true, + Files: map[string]string{ + "dotenv-dep-gen-var.txt": "var-bar", + }, + }, + { + Dir: "testdata/dotenv_task/generated", + Target: "dotenv-gen-seq", + TrimSpace: true, + Files: map[string]string{ + "dotenv-gen-seq.txt": "seq-bar", + }, + }, + } + for _, test := range tt { + t.Run("", func(t *testing.T) { + t.Parallel() + test.Run(t) + }) + } +} + func TestExitImmediately(t *testing.T) { t.Parallel() diff --git a/testdata/dotenv_task/generated/.gitignore b/testdata/dotenv_task/generated/.gitignore new file mode 100644 index 0000000000..2211df63dd --- /dev/null +++ b/testdata/dotenv_task/generated/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/testdata/dotenv_task/generated/Taskfile.yml b/testdata/dotenv_task/generated/Taskfile.yml new file mode 100644 index 0000000000..e0dc0f5924 --- /dev/null +++ b/testdata/dotenv_task/generated/Taskfile.yml @@ -0,0 +1,47 @@ +version: '3' + +env: + FOO: global + +tasks: + dotenv-gen: + vars: + DOTENV: '{{.DOTENV | default "gen-env.txt" }}' + DOTVAL: '{{.DOTVAL | default "gen-bar" }}' + cmds: + - echo "BAR={{.DOTVAL}}" > {{.DOTENV}} + + dotenv-dep-gen-default: + dotenv: + - gen-env.txt + deps: + - task: dotenv-gen + cmds: + - echo "$BAR" > dotenv-dep-gen-default.txt + + dotenv-dep-gen-var: + dotenv: + - gen-var-env.txt + deps: + - task: dotenv-gen + vars: + DOTENV: gen-var-env.txt + DOTVAL: var-bar + cmds: + - echo "$BAR" > dotenv-dep-gen-var.txt + + dotenv-gen-echo: + dotenv: + - gen-env.txt + cmds: + - echo $BAR + - echo "$BAR" > {{.ECHOFILE}} + + dotenv-gen-seq: + cmds: + - task: dotenv-gen + vars: + DOTVAL: seq-bar + - task: dotenv-gen-echo + vars: + ECHOFILE: dotenv-gen-seq.txt \ No newline at end of file diff --git a/variables.go b/variables.go index c10bfcd576..095bd94e8b 100644 --- a/variables.go +++ b/variables.go @@ -76,6 +76,54 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) { }, nil } +func (e *Executor) taskEnv(t *ast.Task, origTaskEnv *ast.Vars, cache *templater.Cache, evaluateShVars bool) (*ast.Vars, error) { + taskEnv := ast.NewVars() + if cache == nil { + cache = &templater.Cache{Vars: t.Vars} + } + + // Load dotenv files based on the templated list of t.Dotenv files. + dotenvEnvs := ast.NewVars() + if len(t.Dotenv) > 0 { + for _, dotEnvPath := range t.Dotenv { + dotEnvPath = filepathext.SmartJoin(t.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.Get(key); !ok { + dotenvEnvs.Set(key, ast.Var{Value: value}) + } + } + } + } + + // Merge the Task envars => destination (by-caller) is typically t.Env + taskEnv.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil) + taskEnv.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil) + taskEnv.Merge(templater.ReplaceVars(origTaskEnv, cache), nil) + if evaluateShVars { + for k, v := range taskEnv.All() { + // If the variable is not dynamic, we can set it and return + if v.Value != nil || v.Sh == nil { + taskEnv.Set(k, ast.Var{Value: v.Value}) + continue + } + static, err := e.Compiler.HandleDynamicVar(v, t.Dir, env.GetFromVars(taskEnv)) + if err != nil { + return nil, err + } + taskEnv.Set(k, ast.Var{Value: static}) + } + } + + return taskEnv, nil +} + func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, error) { origTask, err := e.GetTask(call) if err != nil { @@ -143,42 +191,9 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err new.Prefix = new.Task } - dotenvEnvs := ast.NewVars() - 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.Get(key); !ok { - dotenvEnvs.Set(key, ast.Var{Value: value}) - } - } - } - } - - new.Env = ast.NewVars() - new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil) - new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil) - new.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil) - if evaluateShVars { - for k, v := range new.Env.All() { - // If the variable is not dynamic, we can set it and return - if v.Value != nil || v.Sh == nil { - new.Env.Set(k, ast.Var{Value: v.Value}) - continue - } - static, err := e.Compiler.HandleDynamicVar(v, new.Dir, env.GetFromVars(new.Env)) - if err != nil { - return nil, err - } - new.Env.Set(k, ast.Var{Value: static}) - } + new.Env, err = e.taskEnv(&new, origTask.Env, cache, evaluateShVars) + if err != nil { + return nil, err } if len(origTask.Sources) > 0 && origTask.Method != "none" {