From 5fdaa9aa36a21baf712e18ae7fada5cd0037301c Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Sun, 18 Jun 2023 02:32:18 +0100 Subject: [PATCH] feat: implement gentle force experiment draft (#1216) * feat: implement gentle force experiment draft * docs: changelog --- CHANGELOG.md | 6 ++-- args/args.go | 8 ++--- args/args_test.go | 51 +++++++++++++++------------- cmd/task/task.go | 14 ++++++-- docs/docs/experiments/experiments.md | 21 ++++++++++++ internal/experiments/experiments.go | 4 +-- task.go | 4 ++- task_test.go | 46 +++++++++++++++++++++++++ taskfile/call.go | 1 + testdata/force/Taskfile.yml | 19 +++++++++++ 10 files changed, 139 insertions(+), 35 deletions(-) create mode 100644 testdata/force/Taskfile.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 056671af..04df0e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ - e.g. `taskfile.yml`, `taskfile.yaml`, `taskfile.dist.yml` & `taskfile.dist.yaml` - Bug fixed were made to the - [npm installation method](https://taskfile.dev/installation/#npm). - (#1190, by @sounisi5011). + [npm installation method](https://taskfile.dev/installation/#npm). (#1190, by + @sounisi5011). +- Added the [gentle force experiment](https://taskfile.dev/experiments) as a + draft (#1200, #1216 by @pd93). ## v3.26.0 - 2023-06-10 diff --git a/args/args.go b/args/args.go index e93e1ade..801ff88b 100644 --- a/args/args.go +++ b/args/args.go @@ -13,7 +13,7 @@ func ParseV3(args ...string) ([]taskfile.Call, *taskfile.Vars) { for _, arg := range args { if !strings.Contains(arg, "=") { - calls = append(calls, taskfile.Call{Task: arg}) + calls = append(calls, taskfile.Call{Task: arg, Direct: true}) continue } @@ -22,7 +22,7 @@ func ParseV3(args ...string) ([]taskfile.Call, *taskfile.Vars) { } if len(calls) == 0 { - calls = append(calls, taskfile.Call{Task: "default"}) + calls = append(calls, taskfile.Call{Task: "default", Direct: true}) } return calls, globals @@ -35,7 +35,7 @@ func ParseV2(args ...string) ([]taskfile.Call, *taskfile.Vars) { for _, arg := range args { if !strings.Contains(arg, "=") { - calls = append(calls, taskfile.Call{Task: arg}) + calls = append(calls, taskfile.Call{Task: arg, Direct: true}) continue } @@ -52,7 +52,7 @@ func ParseV2(args ...string) ([]taskfile.Call, *taskfile.Vars) { } if len(calls) == 0 { - calls = append(calls, taskfile.Call{Task: "default"}) + calls = append(calls, taskfile.Call{Task: "default", Direct: true}) } return calls, globals diff --git a/args/args_test.go b/args/args_test.go index 4404a4e9..3b74c7dd 100644 --- a/args/args_test.go +++ b/args/args_test.go @@ -20,17 +20,17 @@ func TestArgsV3(t *testing.T) { { Args: []string{"task-a", "task-b", "task-c"}, ExpectedCalls: []taskfile.Call{ - {Task: "task-a"}, - {Task: "task-b"}, - {Task: "task-c"}, + {Task: "task-a", Direct: true}, + {Task: "task-b", Direct: true}, + {Task: "task-c", Direct: true}, }, }, { Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"}, ExpectedCalls: []taskfile.Call{ - {Task: "task-a"}, - {Task: "task-b"}, - {Task: "task-c"}, + {Task: "task-a", Direct: true}, + {Task: "task-b", Direct: true}, + {Task: "task-c", Direct: true}, }, ExpectedGlobals: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( @@ -46,7 +46,7 @@ func TestArgsV3(t *testing.T) { { Args: []string{"task-a", "CONTENT=with some spaces"}, ExpectedCalls: []taskfile.Call{ - {Task: "task-a"}, + {Task: "task-a", Direct: true}, }, ExpectedGlobals: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( @@ -60,8 +60,8 @@ func TestArgsV3(t *testing.T) { { Args: []string{"FOO=bar", "task-a", "task-b"}, ExpectedCalls: []taskfile.Call{ - {Task: "task-a"}, - {Task: "task-b"}, + {Task: "task-a", Direct: true}, + {Task: "task-b", Direct: true}, }, ExpectedGlobals: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( @@ -75,19 +75,19 @@ func TestArgsV3(t *testing.T) { { Args: nil, ExpectedCalls: []taskfile.Call{ - {Task: "default"}, + {Task: "default", Direct: true}, }, }, { Args: []string{}, ExpectedCalls: []taskfile.Call{ - {Task: "default"}, + {Task: "default", Direct: true}, }, }, { Args: []string{"FOO=bar", "BAR=baz"}, ExpectedCalls: []taskfile.Call{ - {Task: "default"}, + {Task: "default", Direct: true}, }, ExpectedGlobals: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( @@ -122,16 +122,17 @@ func TestArgsV2(t *testing.T) { { Args: []string{"task-a", "task-b", "task-c"}, ExpectedCalls: []taskfile.Call{ - {Task: "task-a"}, - {Task: "task-b"}, - {Task: "task-c"}, + {Task: "task-a", Direct: true}, + {Task: "task-b", Direct: true}, + {Task: "task-c", Direct: true}, }, }, { Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"}, ExpectedCalls: []taskfile.Call{ { - Task: "task-a", + Task: "task-a", + Direct: true, Vars: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( map[string]taskfile.Var{ @@ -141,9 +142,10 @@ func TestArgsV2(t *testing.T) { ), }, }, - {Task: "task-b"}, + {Task: "task-b", Direct: true}, { - Task: "task-c", + Task: "task-c", + Direct: true, Vars: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( map[string]taskfile.Var{ @@ -160,7 +162,8 @@ func TestArgsV2(t *testing.T) { Args: []string{"task-a", "CONTENT=with some spaces"}, ExpectedCalls: []taskfile.Call{ { - Task: "task-a", + Task: "task-a", + Direct: true, Vars: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( map[string]taskfile.Var{ @@ -175,8 +178,8 @@ func TestArgsV2(t *testing.T) { { Args: []string{"FOO=bar", "task-a", "task-b"}, ExpectedCalls: []taskfile.Call{ - {Task: "task-a"}, - {Task: "task-b"}, + {Task: "task-a", Direct: true}, + {Task: "task-b", Direct: true}, }, ExpectedGlobals: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( @@ -190,19 +193,19 @@ func TestArgsV2(t *testing.T) { { Args: nil, ExpectedCalls: []taskfile.Call{ - {Task: "default"}, + {Task: "default", Direct: true}, }, }, { Args: []string{}, ExpectedCalls: []taskfile.Call{ - {Task: "default"}, + {Task: "default", Direct: true}, }, }, { Args: []string{"FOO=bar", "BAR=baz"}, ExpectedCalls: []taskfile.Call{ - {Task: "default"}, + {Task: "default", Direct: true}, }, ExpectedGlobals: &taskfile.Vars{ OrderedMap: orderedmap.FromMapWithOrder( diff --git a/cmd/task/task.go b/cmd/task/task.go index dde4ef2c..b8a9bdf2 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -15,6 +15,7 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/args" "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/sort" ver "github.com/go-task/task/v3/internal/version" @@ -53,6 +54,7 @@ var flags struct { taskSort string status bool force bool + forceAll bool watch bool verbose bool silent bool @@ -78,7 +80,6 @@ func main() { Verbose: flags.verbose, Color: flags.color, } - if err, ok := err.(*errors.TaskRunError); ok && flags.exitCode { l.Errf(logger.Red, "%v\n", err) os.Exit(err.TaskExitCode()) @@ -110,7 +111,6 @@ func run() error { pflag.BoolVarP(&flags.listJson, "json", "j", false, "Formats task list as JSON.") pflag.StringVar(&flags.taskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].") pflag.BoolVar(&flags.status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.") - pflag.BoolVarP(&flags.force, "force", "f", false, "Forces execution even when the task is up-to-date.") pflag.BoolVarP(&flags.watch, "watch", "w", false, "Enables watch of the given task.") pflag.BoolVarP(&flags.verbose, "verbose", "v", false, "Enables verbose mode.") pflag.BoolVarP(&flags.silent, "silent", "s", false, "Disables echoing.") @@ -129,6 +129,15 @@ func run() error { pflag.IntVarP(&flags.concurrency, "concurrency", "C", 0, "Limit number tasks to run concurrently.") pflag.DurationVarP(&flags.interval, "interval", "I", 0, "Interval to watch for changes.") pflag.BoolVarP(&flags.global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.") + + // Gentle force experiment will override the force flag and add a new force-all flag + if experiments.GentleForce { + pflag.BoolVarP(&flags.force, "force", "f", false, "Forces execution of the directly called task.") + pflag.BoolVar(&flags.forceAll, "force-all", false, "Forces execution of the called task and all its dependant tasks.") + } else { + pflag.BoolVarP(&flags.forceAll, "force", "f", false, "Forces execution even when the task is up-to-date.") + } + pflag.Parse() if flags.version { @@ -194,6 +203,7 @@ func run() error { e := task.Executor{ Force: flags.force, + ForceAll: flags.forceAll, Watch: flags.watch, Verbose: flags.verbose, Silent: flags.silent, diff --git a/docs/docs/experiments/experiments.md b/docs/docs/experiments/experiments.md index 8d337b96..64d3c6da 100644 --- a/docs/docs/experiments/experiments.md +++ b/docs/docs/experiments/experiments.md @@ -70,6 +70,27 @@ version 3 as soon as possible. A list of changes between version 2 and version 3 are available in the [Task v3 Release Notes][version-3-release-notes]. +### ![experiment] Gentle Force ([#1200](https://github.com/go-task/task/issues/1200)) + +- Environment variable: `TASK_X_FORCE=1` +- Breaks: `--force` flag + +The `--force` flag currently forces _all_ tasks to run regardless of the status +checks. This can be useful, but we have found that most of the time users only +expect the direct task they are calling to be forced and _not_ all of its +dependant tasks. + +This experiment changes the `--force` flag to only force the directly called +task. All dependant tasks will have their statuses checked as normal and will +only run if Task considers them to be out of date. A new `--force-all` flag will +also be added to maintain the current behavior for users that need this +functionality. + +If you want to migrate, but continue to force all dependant tasks to run, you +should replace all uses of the `--force` flag with `--force-all`. Alternatively, +if you want to adopt the new behavior, you can continue to use the `--force` +flag as you do now! + [breaking-change-proposal]: https://github.com/go-task/task/discussions/1191 [deprecate-version-2-schema]: https://github.com/go-task/task/issues/1197 diff --git a/internal/experiments/experiments.go b/internal/experiments/experiments.go index 1eb7cca7..df0e50e2 100644 --- a/internal/experiments/experiments.go +++ b/internal/experiments/experiments.go @@ -11,13 +11,13 @@ import ( const envPrefix = "TASK_X_" -var TestExperiment bool +var GentleForce bool func init() { if err := readDotEnv(); err != nil { panic(err) } - TestExperiment = parseEnv("TestExperiment") + GentleForce = parseEnv("GENTLE_FORCE") } func parseEnv(xName string) bool { diff --git a/task.go b/task.go index 5c564cc9..9bd8c8c0 100644 --- a/task.go +++ b/task.go @@ -50,6 +50,7 @@ type Executor struct { TempDir string Entrypoint string Force bool + ForceAll bool Watch bool Verbose bool Silent bool @@ -179,7 +180,8 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { return err } - if !e.Force { + skipFingerprinting := e.ForceAll || (call.Direct && e.Force) + if !skipFingerprinting { if err := ctx.Err(); err != nil { return err } diff --git a/task_test.go b/task_test.go index 8ea91d2a..b60dea7b 100644 --- a/task_test.go +++ b/task_test.go @@ -2120,3 +2120,49 @@ func TestSilence(t *testing.T) { buff.Reset() } + +func TestForce(t *testing.T) { + tests := []struct { + name string + env map[string]string + force bool + forceAll bool + }{ + { + name: "force", + force: true, + }, + { + name: "force-all", + forceAll: true, + }, + { + name: "force with gentle force experiment", + force: true, + env: map[string]string{ + "TASK_X_GENTLE_FORCE": "1", + }, + }, + { + name: "force-all with gentle force experiment", + forceAll: true, + env: map[string]string{ + "TASK_X_GENTLE_FORCE": "1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buff bytes.Buffer + e := task.Executor{ + Dir: "testdata/force", + Stdout: &buff, + Stderr: &buff, + Force: tt.force, + ForceAll: tt.forceAll, + } + require.NoError(t, e.Setup()) + require.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-with-dep", Direct: true})) + }) + } +} diff --git a/taskfile/call.go b/taskfile/call.go index bb00e782..aaec725b 100644 --- a/taskfile/call.go +++ b/taskfile/call.go @@ -5,4 +5,5 @@ type Call struct { Task string Vars *Vars Silent bool + Direct bool // Was the task called directly or via another task? } diff --git a/testdata/force/Taskfile.yml b/testdata/force/Taskfile.yml new file mode 100644 index 00000000..31e7d3a4 --- /dev/null +++ b/testdata/force/Taskfile.yml @@ -0,0 +1,19 @@ +version: "3" + +tasks: + task-with-dep: + status: [ test true ] + deps: [ indirect ] + cmds: + - echo "direct" + + task-with-subtask: + status: [ test true ] + cmds: + - task: indirect + - echo "direct" + + indirect: + status: [ test true ] + cmds: + - echo "indirect"