diff --git a/cli/internal/core/scheduler.go b/cli/internal/core/scheduler.go index 64e5736c4ad1a..aa1d31a49c429 100644 --- a/cli/internal/core/scheduler.go +++ b/cli/internal/core/scheduler.go @@ -56,6 +56,8 @@ type SchedulerExecutionOptions struct { Concurrency int // Parallel is whether to run tasks in parallel Parallel bool + // Restrict execution to only the listed task names + TasksOnly bool } // Execute executes the pipeline, constructing an internal task graph and walking it accordlingly. @@ -78,7 +80,7 @@ func (p *scheduler) Prepare(options *SchedulerExecutionOptions) error { p.Parallel = options.Parallel - if err := p.generateTaskGraph(pkgs, tasks, true); err != nil { + if err := p.generateTaskGraph(pkgs, tasks, options.TasksOnly); err != nil { return err } @@ -110,7 +112,7 @@ func (p *scheduler) Execute() []error { }) } -func (p *scheduler) generateTaskGraph(scope []string, targets []string, targetsOnly bool) error { +func (p *scheduler) generateTaskGraph(scope []string, taskNames []string, tasksOnly bool) error { if p.PackageTaskDeps == nil { p.PackageTaskDeps = [][]string{} } @@ -122,7 +124,7 @@ func (p *scheduler) generateTaskGraph(scope []string, targets []string, targetsO traversalQueue := []string{} for _, pkg := range scope { - for _, target := range targets { + for _, target := range taskNames { traversalQueue = append(traversalQueue, GetTaskId(pkg, target)) } } @@ -141,12 +143,16 @@ func (p *scheduler) generateTaskGraph(scope []string, targets []string, targetsO visited.Add(taskId) deps := task.Deps - if targetsOnly { + if tasksOnly { deps = deps.Filter(func(d interface{}) bool { - for _, target := range targets { - if dag.VertexName(d) == target { - return true - } + for _, target := range taskNames { + return fmt.Sprintf("%v", d) == target + } + return false + }) + task.TopoDeps = task.TopoDeps.Filter(func(d interface{}) bool { + for _, target := range taskNames { + return fmt.Sprintf("%v", d) == target } return false }) diff --git a/cli/internal/core/scheduler_test.go b/cli/internal/core/scheduler_test.go index 1d082fbf96c12..00c8f03834f41 100644 --- a/cli/internal/core/scheduler_test.go +++ b/cli/internal/core/scheduler_test.go @@ -9,7 +9,7 @@ import ( "github.com/pyr-sh/dag" ) -func TestSchedulerAddTask(t *testing.T) { +func TestSchedulerDefault(t *testing.T) { var g dag.AcyclicGraph g.Add("a") g.Add("b") @@ -61,6 +61,7 @@ func TestSchedulerAddTask(t *testing.T) { TaskNames: []string{"test"}, Concurrency: 10, Parallel: false, + TasksOnly: false, }) if err != nil { @@ -74,23 +75,112 @@ func TestSchedulerAddTask(t *testing.T) { } actual := strings.TrimSpace(p.TaskGraph.String()) - expected := strings.TrimSpace(leafString) + expected := strings.TrimSpace(leafStringAll) if actual != expected { t.Fatalf("bad: \n\nactual---\n%s\n\n expected---\n%s", actual, expected) } } -const leafString = ` +func TestSchedulerTasksOnly(t *testing.T) { + var g dag.AcyclicGraph + g.Add("a") + g.Add("b") + g.Add("c") + g.Connect(dag.BasicEdge("c", "b")) + g.Connect(dag.BasicEdge("c", "a")) + + p := NewScheduler(&g) + topoDeps := make(util.Set) + topoDeps.Add("build") + deps := make(util.Set) + deps.Add("prepare") + p.AddTask(&Task{ + Name: "build", + TopoDeps: topoDeps, + Deps: deps, + Run: func(cwd string) error { + fmt.Println(cwd) + return nil + }, + }) + p.AddTask(&Task{ + Name: "test", + TopoDeps: topoDeps, + Deps: deps, + Run: func(cwd string) error { + fmt.Println(cwd) + return nil + }, + }) + p.AddTask(&Task{ + Name: "prepare", + Run: func(cwd string) error { + fmt.Println(cwd) + return nil + }, + }) + + if _, ok := p.Tasks["build"]; !ok { + t.Fatal("AddTask is not adding tasks (build)") + } + + if _, ok := p.Tasks["test"]; !ok { + t.Fatal("AddTask is not adding tasks (test)") + } + + err := p.Prepare(&SchedulerExecutionOptions{ + Packages: nil, + TaskNames: []string{"test"}, + Concurrency: 10, + Parallel: false, + TasksOnly: true, + }) + + if err != nil { + t.Fatalf("%v", err) + } + + errs := p.Execute() + + for _, err := range errs { + t.Fatalf("%v", err) + } + + actual := strings.TrimSpace(p.TaskGraph.String()) + expected := strings.TrimSpace(leafStringOnly) + if actual != expected { + t.Fatalf("bad: \n\nactual---\n%s\n\n expected---\n%s", actual, expected) + } +} + +const leafStringAll = ` ___ROOT___ a#build + a#prepare +a#prepare ___ROOT___ a#test - ___ROOT___ + a#prepare b#build + b#prepare +b#prepare ___ROOT___ b#test + b#prepare +c#prepare ___ROOT___ c#test a#build b#build + c#prepare +` + +const leafStringOnly = ` +___ROOT___ +a#test + ___ROOT___ +b#test + ___ROOT___ +c#test + ___ROOT___ ` diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index 650ed42aaf4e0..0f93796ade49f 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -608,6 +608,7 @@ func (c *RunCommand) Run(args []string) int { TaskNames: ctx.Targets.UnsafeListOfStrings(), Concurrency: runOptions.concurrency, Parallel: runOptions.parallel, + TasksOnly: runOptions.only, }); err != nil { c.Ui.Error(fmt.Sprintf("Error preparing engine: %s", err)) return 1 @@ -692,6 +693,8 @@ type RunOptions struct { // Immediately exit on task failure bail bool passThroughArgs []string + // Restrict execution to only the listed task names. Default false + only bool } func getDefaultRunOptions() *RunOptions { @@ -706,6 +709,7 @@ func getDefaultRunOptions() *RunOptions { profile: "", // empty string does no tracing forceExecution: false, stream: true, + only: false, } } @@ -789,6 +793,8 @@ func parseRunArgs(args []string, cwd string) (*RunOptions, error) { } case strings.HasPrefix(arg, "--includeDependencies"): runOptions.ancestors = true + case strings.HasPrefix(arg, "--only"): + runOptions.only = true case strings.HasPrefix(arg, "--team"): case strings.HasPrefix(arg, "--token"): default: diff --git a/docs/pages/docs/reference/command-line-reference.mdx b/docs/pages/docs/reference/command-line-reference.mdx index 7fa93c75edec4..be5c0dd49d43c 100644 --- a/docs/pages/docs/reference/command-line-reference.mdx +++ b/docs/pages/docs/reference/command-line-reference.mdx @@ -145,6 +145,37 @@ turbo run build --no-cache turbo run dev --parallel --no-cache ``` +#### `--only` + +Default false. Restricts execution to only include specified tasks. This is very similar to how how `lerna` or `pnpm` run tasks by default. + +Given this pipeline: + +```json +{ + "turbo": { + "pipeline": { + "build": { + "dependsOn": [ + "^build" + ] + }, + "test": { + "dependsOn": [ + "^build" + ] + } + } + } +} +``` + +```shell +turbo run test --only +``` + +Will execute _only_ the `test` tasks in each package. It will not `build`. + #### `--parallel` Default `false`. Run commands in parallel across packages and apps and ignore the dependency graph. This is useful for developing with live reloading.