Skip to content

Commit 4596350

Browse files
branchseerclaude
andcommitted
feat(reporter): return proper exit codes based on task failures
- Return 0 if all tasks succeed - Return the task's exit code if exactly one task failed - Return 1 if more than one task failed Update vite_task_bin to propagate the exit code to the process. Add e2e tests to verify exit code behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 274bf07 commit 4596350

12 files changed

Lines changed: 189 additions & 17 deletions

crates/vite_task/src/session/execute/mod.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pub mod fingerprint;
22
pub mod spawn;
33

4-
use std::sync::Arc;
4+
use std::{process::ExitCode, sync::Arc};
55

66
use futures_util::FutureExt;
77
use petgraph::{algo::toposort, graph::DiGraph};
@@ -328,11 +328,10 @@ impl ExecutionContext<'_> {
328328
}
329329

330330
impl<'a, CustomSubcommand> Session<'a, CustomSubcommand> {
331-
pub async fn execute(
332-
&self,
333-
plan: ExecutionPlan,
334-
mut reporter: Box<dyn Reporter>,
335-
) -> anyhow::Result<()> {
331+
/// Execute an execution plan, reporting events to the provided reporter.
332+
///
333+
/// Returns the ExitCode from the reporter's post_execution method.
334+
pub async fn execute(&self, plan: ExecutionPlan, mut reporter: Box<dyn Reporter>) -> ExitCode {
336335
let mut execution_context = ExecutionContext {
337336
event_handler: &mut *reporter,
338337
current_execution_id: ExecutionId::zero(),

crates/vite_task/src/session/reporter.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::{
44
collections::HashSet,
55
io::Write,
6+
process::ExitCode,
67
sync::{Arc, LazyLock},
78
time::Duration,
89
};
@@ -34,8 +35,8 @@ pub trait Reporter {
3435
fn handle_event(&mut self, event: ExecutionEvent);
3536

3637
/// Called after execution completes (whether successful or not)
37-
/// Returns Err if execution failed due to errors
38-
fn post_execution(self: Box<Self>) -> anyhow::Result<()>;
38+
/// Returns an ExitCode that is a suggestion for process exit
39+
fn post_execution(self: Box<Self>) -> ExitCode;
3940
}
4041

4142
const COMMAND_STYLE: Style = Style::new().cyan();
@@ -486,7 +487,7 @@ impl<W: Write> Reporter for LabeledReporter<W> {
486487
}
487488
}
488489

489-
fn post_execution(mut self: Box<Self>) -> anyhow::Result<()> {
490+
fn post_execution(mut self: Box<Self>) -> ExitCode {
490491
// Check if execution was aborted due to error
491492
if let Some(error_msg) = &self.first_error {
492493
// Print separator
@@ -513,7 +514,7 @@ impl<W: Write> Reporter for LabeledReporter<W> {
513514
.style(Style::new().bright_black())
514515
);
515516

516-
return Err(anyhow::anyhow!("Execution aborted: {}", error_msg));
517+
return ExitCode::FAILURE;
517518
}
518519

519520
// No errors - print summary if not hidden
@@ -525,6 +526,26 @@ impl<W: Write> Reporter for LabeledReporter<W> {
525526
self.print_summary();
526527
}
527528
}
528-
Ok(())
529+
530+
// Determine exit code based on failed tasks:
531+
// 1. All tasks succeed → return 0
532+
// 2. Exactly one task failed → return that task's exit code
533+
// 3. More than one task failed → return 1
534+
let failed_exit_codes: Vec<i32> = self
535+
.executions
536+
.iter()
537+
.filter_map(|exec| exec.exit_status)
538+
.filter(|&status| status != 0)
539+
.collect();
540+
541+
match failed_exit_codes.len() {
542+
0 => ExitCode::SUCCESS,
543+
1 => {
544+
// Return the single failed task's exit code (clamped to u8 range)
545+
let code = failed_exit_codes[0];
546+
ExitCode::from(code.clamp(1, 255) as u8)
547+
}
548+
_ => ExitCode::FAILURE,
549+
}
529550
}
530551
}

crates/vite_task_bin/src/main.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
use std::{env, sync::Arc};
1+
use std::{env, process::ExitCode, sync::Arc};
22

33
use vite_path::{AbsolutePath, current_dir};
44
use vite_task::{CLIArgs, Session, session::reporter::LabeledReporter};
55
use vite_task_bin::{CustomTaskSubcommand, NonTaskSubcommand, OwnedSessionCallbacks};
66

77
#[tokio::main]
8-
async fn main() -> anyhow::Result<()> {
8+
async fn main() -> ExitCode {
9+
match run().await {
10+
Ok(exit_code) => exit_code,
11+
Err(err) => {
12+
eprintln!("Error: {err}");
13+
ExitCode::FAILURE
14+
}
15+
}
16+
}
17+
18+
async fn run() -> anyhow::Result<ExitCode> {
919
let cwd: Arc<AbsolutePath> = current_dir()?.into();
1020
// Parse the CLI arguments and see if they are for vite-task or not
1121
let args = match CLIArgs::<CustomTaskSubcommand, NonTaskSubcommand>::try_parse_from(env::args())
@@ -20,7 +30,7 @@ async fn main() -> anyhow::Result<()> {
2030
CLIArgs::NonTask(NonTaskSubcommand::Version) => {
2131
// Non-task subcommands are not handled by vite-task's session.
2232
println!("{}", env!("CARGO_PKG_VERSION"));
23-
return Ok(());
33+
return Ok(ExitCode::SUCCESS);
2434
}
2535
};
2636

@@ -30,7 +40,7 @@ async fn main() -> anyhow::Result<()> {
3040

3141
// Create reporter and execute
3242
let reporter = LabeledReporter::new(std::io::stdout(), session.workspace_path());
33-
session.execute(plan, Box::new(reporter)).await?;
43+
let exit_code = session.execute(plan, Box::new(reporter)).await;
3444

35-
Ok(())
45+
Ok(exit_code)
3646
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "exit-codes-test",
3+
"private": true
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "pkg-a",
3+
"scripts": {
4+
"fail": "node -e \"process.exit(42)\""
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "pkg-b",
3+
"scripts": {
4+
"fail": "node -e \"process.exit(7)\""
5+
}
6+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- packages/*
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[[e2e]]
2+
name = "single task failure returns task exit code"
3+
steps = [
4+
"vite run pkg-a#fail",
5+
]
6+
7+
[[e2e]]
8+
name = "multiple task failures returns exit code 1"
9+
steps = [
10+
"vite run -r fail",
11+
]

crates/vite_task_bin/tests/test_snapshots/snapshots/test_snapshots__cycle dependency error@error_cycle_dependency.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
source: crates/vite_task_bin/tests/test_snapshots/main.rs
3+
assertion_line: 203
34
expression: e2e_outputs
45
input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/error_cycle_dependency
56
---
@@ -10,4 +11,3 @@ input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/error_cycle_depen
1011
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1112
Execution aborted due to error: Cycle dependencies detected: Cycle(NodeIndex(ExecutionIx(1)))
1213
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13-
Error: Execution aborted: Cycle dependencies detected: Cycle(NodeIndex(ExecutionIx(1)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
source: crates/vite_task_bin/tests/test_snapshots/main.rs
3+
assertion_line: 203
4+
expression: e2e_outputs
5+
input_file: crates/vite_task_bin/tests/test_snapshots/fixtures/exit-codes
6+
---
7+
[1]> vite run -r fail
8+
~/packages/pkg-b$ node -e "process.exit(7)"
9+
10+
~/packages/pkg-a$ node -e "process.exit(42)"
11+
12+
13+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14+
Vite+ Task RunnerExecution Summary
15+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16+
17+
Statistics: 2 tasks0 cache hits2 cache misses2 failed
18+
Performance: 0% cache hit rate
19+
20+
Task Details:
21+
────────────────────────────────────────────────
22+
[1] pkg-b#fail: ~/packages/pkg-b$ node -e "process.exit(7)" ✗ (exit code: 7)
23+
Cache miss: no previous cache entry found
24+
·······················································
25+
[2] pkg-a#fail: ~/packages/pkg-a$ node -e "process.exit(42)" ✗ (exit code: 42)
26+
Cache miss: no previous cache entry found
27+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

0 commit comments

Comments
 (0)