Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions crates/vite_task/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,62 +72,73 @@ impl ExecutionContext<'_> {
/// passes `graph.node_count() == 1`; recursive calls AND with the nested graph's
/// node count.
///
/// Leaf-level errors are reported through the reporter and do not abort the graph.
/// Cycle detection is handled at plan time, so this function cannot encounter cycles.
/// Fast-fail: if any task fails (non-zero exit or infrastructure error), remaining
/// tasks and `&&`-chained items are skipped. Leaf-level errors are reported through
/// the reporter. Cycle detection is handled at plan time.
///
/// Returns `true` if all tasks succeeded, `false` if any task failed.
#[tracing::instrument(level = "debug", skip_all)]
#[expect(clippy::future_not_send, reason = "uses !Send types internally")]
async fn execute_expanded_graph(
&mut self,
graph: &ExecutionGraph,
all_ancestors_single_node: bool,
) {
) -> bool {
// `compute_topological_order()` returns nodes in topological order: for every
// edge A→B, A appears before B. Since our edges mean "A depends on B",
// dependencies (B) appear after their dependents (A). We iterate in reverse
// to get execution order where dependencies run first.

// Execute tasks in dependency-first order. Each task may have multiple items
// (from `&&`-split commands), which are executed sequentially.
// If any task fails, subsequent tasks and items are skipped (fast-fail).
let topo_order = graph.compute_topological_order();
for &node_ix in topo_order.iter().rev() {
let task_execution = &graph[node_ix];

for item in &task_execution.items {
match &item.kind {
let failed = match &item.kind {
ExecutionItemKind::Leaf(leaf_kind) => {
self.execute_leaf(
&item.execution_item_display,
leaf_kind,
all_ancestors_single_node,
)
.boxed_local()
.await;
.await
}
ExecutionItemKind::Expanded(nested_graph) => {
self.execute_expanded_graph(
nested_graph,
all_ancestors_single_node && nested_graph.node_count() == 1,
)
.boxed_local()
.await;
!self
.execute_expanded_graph(
nested_graph,
all_ancestors_single_node && nested_graph.node_count() == 1,
)
.boxed_local()
.await
}
};
if failed {
return false;
}
}
}
true
}

/// Execute a single leaf item (in-process command or spawned process).
///
/// Creates a [`LeafExecutionReporter`] from the graph reporter and delegates
/// to the appropriate execution method.
///
/// Returns `true` if the execution failed (non-zero exit or infrastructure error).
#[tracing::instrument(level = "debug", skip_all)]
#[expect(clippy::future_not_send, reason = "uses !Send types internally")]
async fn execute_leaf(
&mut self,
display: &ExecutionItemDisplay,
leaf_kind: &LeafExecutionKind,
all_ancestors_single_node: bool,
) {
) -> bool {
let mut leaf_reporter =
self.reporter.new_leaf_execution(display, leaf_kind, all_ancestors_single_node);

Expand All @@ -150,15 +161,21 @@ impl ExecutionContext<'_> {
None,
)
.await;
false
}
LeafExecutionKind::Spawn(spawn_execution) => {
#[expect(
clippy::large_futures,
reason = "spawn execution with cache management creates large futures"
)]
let _ =
let outcome =
execute_spawn(leaf_reporter, spawn_execution, self.cache, self.cache_base_path)
.await;
match outcome {
SpawnOutcome::CacheHit => false,
SpawnOutcome::Spawned(status) => !status.success(),
SpawnOutcome::Failed => true,
}
}
}
}
Expand Down Expand Up @@ -519,8 +536,8 @@ impl Session<'_> {
cache_base_path: &self.workspace_path,
};

// Execute the graph. Leaf-level errors are reported through the reporter
// and do not abort the graph. Cycle detection is handled at plan time.
// Execute the graph with fast-fail: if any task fails, remaining tasks
// are skipped. Leaf-level errors are reported through the reporter.
let all_single_node = execution_graph.node_count() == 1;
execution_context.execute_expanded_graph(&execution_graph, all_single_node).await;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"name": "pkg-a",
"scripts": {
"fail": "node -e \"process.exit(42)\""
"fail": "node -e \"process.exit(42)\"",
"check": "node -e \"process.exit(1)\"",
"chained": "node -e \"process.exit(3)\" && echo 'second'"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"name": "pkg-b",
"dependencies": {
"pkg-a": "workspace:*"
},
"scripts": {
"fail": "node -e \"process.exit(7)\""
"fail": "node -e \"process.exit(7)\"",
"check": "echo 'pkg-b check passed'"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@ steps = [
]

[[e2e]]
name = "multiple task failures returns exit code 1"
name = "task failure fast-fails remaining tasks"
steps = [
"vt run -r fail # multiple failures -> exit code 1",
"vt run -r fail # pkg-a fails, pkg-b is skipped",
]

[[e2e]]
name = "dependency failure fast-fails dependents"
cwd = "packages/pkg-b"
steps = [
"vt run -t check # pkg-a fails, pkg-b is skipped",
]

[[e2e]]
name = "chained command with && stops at first failure"
steps = [
"vt run pkg-a#chained # first fails with exit code 3, second should not run",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[3]> vt run pkg-a#chained # first fails with exit code 3, second should not run
~/packages/pkg-a$ node -e "process.exit(3)"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
info:
cwd: packages/pkg-b
---
[1]> vt run -t check # pkg-a fails, pkg-b is skipped
~/packages/pkg-a$ node -e "process.exit(1)"

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[42]> vt run -r fail # pkg-a fails, pkg-b is skipped
~/packages/pkg-a$ node -e "process.exit(42)"
Loading