Skip to content
Open
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
13 changes: 1 addition & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
# Changelog

- **Fixed** Vite+ diagnostics now display individual paths and working directories without Rust debug formatting such as quoted paths or escaped Windows backslashes ([#534](https://github.com/voidzero-dev/vite-task/pull/534)).
- **Fixed** Automatic file-access tracking now works inside the default Codex CLI and Claude Code sandboxes ([#562](https://github.com/voidzero-dev/vite-task/issues/562), [#563](https://github.com/voidzero-dev/vite-task/issues/563), [#576](https://github.com/voidzero-dev/vite-task/pull/576), [#569](https://github.com/voidzero-dev/vite-task/pull/569)).
- **Fixed** Broad workspace globs no longer discover and run package scripts inside `node_modules` ([#539](https://github.com/voidzero-dev/vite-task/pull/539)).
- **Added** Tasks now run with `VP_RUN=1` set, so tools can tell they are running under `vp run` instead of being invoked directly ([#570](https://github.com/voidzero-dev/vite-task/pull/570)).
- **Fixed** The task cache now supports much larger automatically tracked input sets without hitting wincode's default 4 MiB sequence preallocation limit ([#554](https://github.com/voidzero-dev/vite-task/pull/554)).
- **Fixed** npm workspace patterns beginning with `./` now discover matching packages correctly ([vite-plus#2201](https://github.com/voidzero-dev/vite-plus/issues/2201), [#547](https://github.com/voidzero-dev/vite-task/pull/547)).
- **Fixed** Failures while forwarding output from a started task process no longer incorrectly say the process failed to spawn ([#506](https://github.com/voidzero-dev/vite-task/issues/506)).
- **Fixed** An issue where Bun tasks on macOS did not rerun when files they read, wrote, or listed changed ([#532](https://github.com/voidzero-dev/vite-task/issues/532), [#542](https://github.com/voidzero-dev/vite-task/pull/542)).
- **Improved** Windows file-access tracking now uses sparse temporary backing files where supported, avoiding upfront allocation of the full backing file on disk ([#524](https://github.com/voidzero-dev/vite-task/pull/524)).
- **Fixed** Automatic file-access tracking on Linux now works in containers and Kubernetes runners with limited `/dev/shm` space ([#353](https://github.com/voidzero-dev/vite-task/issues/353), [#523](https://github.com/voidzero-dev/vite-task/pull/523)).
- **Fixed** Windows automatic input tracking now records executable image reads when process creation performs the lookup inside `NtCreateUserProcess` ([#518](https://github.com/voidzero-dev/vite-task/pull/518)).
- **Fixed** Failures while waiting for a started task process to exit no longer incorrectly say the process failed to spawn ([#515](https://github.com/voidzero-dev/vite-task/pull/515)).
- **Added** `replayLogs: false` for cached tasks that should restore outputs and report cache hits without replaying cached stdout/stderr.
- **Fixed** Missing env vars requested through `@voidzero-dev/vite-task-client` now return `undefined` instead of `null`, preserving Vite production `NODE_ENV` semantics when builds run through `vp run` ([#508](https://github.com/voidzero-dev/vite-task/pull/508)).
- **Fixed** Windows builds no longer hang on CI when a `node_modules/.bin` `.cmd` shim is routed through PowerShell: the npm/pnpm/yarn `.ps1` wrappers read stdin and block forever on a non-TTY pipe, so the PowerShell rewrite is now skipped when stdin is not an interactive terminal, falling back to the `.cmd` (which never reads stdin) ([#491](https://github.com/voidzero-dev/vite-task/pull/491)).
- **Added** First-party support for caching `vite build` with zero cache config, giving Vite projects correct cache hits out of the box ([vitejs/vite#22453](https://github.com/vitejs/vite/pull/22453)).
Expand Down
10 changes: 7 additions & 3 deletions crates/vt/src/session/cache/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,13 @@ fn format_env_changed_inline(names: &[&Str]) -> Str {
/// Note: Returns plain text without styling. The reporter applies colors.
pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
match cache_status {
CacheStatus::Hit { .. } => {
// Show "cache hit" indicator when replaying from cache
Some(Str::from("◉ cache hit, replaying"))
CacheStatus::Hit { logs_replayed, .. } => {
if *logs_replayed {
// Show "cache hit" indicator when replaying from cache
Some(Str::from("◉ cache hit, replaying"))
} else {
Some(Str::from("◉ cache hit"))
}
}
CacheStatus::Miss(CacheMiss::NotFound) => {
// No inline message for "not found" case - just show command
Expand Down
2 changes: 1 addition & 1 deletion crates/vt/src/session/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ pub enum CacheUpdateStatus {
pub enum CacheStatus {
Disabled(CacheDisabledReason),
Miss(CacheMiss),
Hit { replayed_duration: Duration },
Hit { replayed_duration: Duration, logs_replayed: bool },
}

/// Convert `ExitStatus` to an i32 exit code.
Expand Down
25 changes: 16 additions & 9 deletions crates/vt/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,14 +385,18 @@ async fn run(
// to execute the command — or carry the globbed inputs into the run.
let (stdio_config, globbed_inputs) = match lookup {
CacheLookup::Hit(cached) => {
let mut stdio_config =
reporter.start(CacheStatus::Hit { replayed_duration: cached.duration });
let replay_logs = cache_metadata.is_none_or(|metadata| metadata.replay_logs);
let mut stdio_config = reporter.start(CacheStatus::Hit {
replayed_duration: cached.duration,
logs_replayed: replay_logs,
});
return Ok(replay_cache_hit(
&mut stdio_config,
&cached,
workspace_root,
cache_dir,
program_name,
replay_logs,
));
}
CacheLookup::Miss { miss, globbed_inputs } => {
Expand Down Expand Up @@ -547,14 +551,17 @@ fn replay_cache_hit(
workspace_root: &Arc<AbsolutePath>,
cache_dir: &AbsolutePath,
program_name: &str,
replay_logs: bool,
) -> Report {
for output in cached.std_outputs.iter() {
let writer: &mut dyn std::io::Write = match output.kind {
pipe::OutputKind::StdOut => &mut stdio_config.writers.stdout_writer,
pipe::OutputKind::StdErr => &mut stdio_config.writers.stderr_writer,
};
let _ = writer.write_all(&output.content);
let _ = writer.flush();
if replay_logs {
for output in cached.std_outputs.iter() {
let writer: &mut dyn std::io::Write = match output.kind {
pipe::OutputKind::StdOut => &mut stdio_config.writers.stdout_writer,
pipe::OutputKind::StdErr => &mut stdio_config.writers.stderr_writer,
};
let _ = writer.write_all(&output.content);
let _ = writer.flush();
}
}

// Restore output files from the cached archive. Failure here means the
Expand Down
31 changes: 22 additions & 9 deletions crates/vt/src/session/reporter/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ pub struct TaskSummary {
/// making invalid combinations unrepresentable.
#[derive(Serialize, Deserialize)]
pub enum TaskResult {
/// Cache hit — output was replayed from cache. Always successful.
CacheHit { saved_duration_ms: u64 },
/// Cache hit. Always successful.
CacheHit {
saved_duration_ms: u64,
#[serde(default = "default_logs_replayed")]
logs_replayed: bool,
},

/// In-process execution (built-in command like echo). Always successful.
InProcess,
Expand Down Expand Up @@ -92,6 +96,10 @@ pub enum SpawnedCacheStatus {
Disabled,
}

const fn default_logs_replayed() -> bool {
true
}

/// Outcome of a spawned process.
#[derive(Serialize, Deserialize)]
pub enum SpawnOutcome {
Expand Down Expand Up @@ -121,7 +129,7 @@ pub enum SpawnOutcome {
/// No `infra_error` field: cache operations are skipped on non-zero exit.
Failed { exit_code: NonZeroI32 },

/// Execution failed without a usable process exit status.
/// Could not start the process (e.g., command not found).
SpawnError(SavedExecutionError),
}

Expand Down Expand Up @@ -193,7 +201,7 @@ impl SummaryStats {

for task in tasks {
match &task.result {
TaskResult::CacheHit { saved_duration_ms } => {
TaskResult::CacheHit { saved_duration_ms, .. } => {
stats.cache_hits += 1;
stats.total_saved += Duration::from_millis(*saved_duration_ms);
}
Expand Down Expand Up @@ -345,9 +353,10 @@ impl TaskResult {
);

match cache_status {
CacheStatus::Hit { replayed_duration } => {
Self::CacheHit { saved_duration_ms: duration_to_ms(*replayed_duration) }
}
CacheStatus::Hit { replayed_duration, logs_replayed } => Self::CacheHit {
saved_duration_ms: duration_to_ms(*replayed_duration),
logs_replayed: *logs_replayed,
},
CacheStatus::Disabled(CacheDisabledReason::InProcessExecution) => Self::InProcess,
CacheStatus::Disabled(CacheDisabledReason::NoCacheMetadata) => Self::Spawned {
cache_status: SpawnedCacheStatus::Disabled,
Expand Down Expand Up @@ -565,10 +574,14 @@ impl TaskResult {
}

match self {
Self::CacheHit { saved_duration_ms } => {
Self::CacheHit { saved_duration_ms, logs_replayed } => {
let d = Duration::from_millis(*saved_duration_ms);
let formatted_duration = format_summary_duration(d);
vt_str::format!("→ Cache hit - output replayed - {formatted_duration} saved")
if *logs_replayed {
vt_str::format!("→ Cache hit - output replayed - {formatted_duration} saved")
} else {
vt_str::format!("→ Cache hit - logs skipped - {formatted_duration} saved")
}
}
Self::InProcess => Str::from("→ Cache disabled for built-in command"),
Self::Spawned { cache_status, .. } => match cache_status {
Expand Down
1 change: 1 addition & 0 deletions crates/vt_bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl vt::CommandHandler for CommandHandler {
program,
args: args.into_iter().filter(|a| a.as_str() != "--").collect(),
cache_config: UserCacheConfig::with_config(EnabledCacheConfig {
replay_logs: None,
env: None,
untracked_env: None,
input: None,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "replay-logs-test"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[[e2e]]
name = "cache_hit_skips_log_replay_but_restores_outputs"
comment = """
`replayLogs: false` should keep cache hits and output restoration, while suppressing cached stdout/stderr replay.
"""
steps = [
{ argv = [
"vt",
"run",
"build",
], comment = "first run — cache miss, prints logs and writes dist/output.txt" },
{ argv = [
"vtt",
"rm",
"-rf",
"dist",
], comment = "delete dist/ to prove cache-hit output restoration still happens" },
{ argv = [
"vt",
"run",
"build",
], comment = "second run — cache hit, skips cached log replay" },
{ argv = [
"vtt",
"print-file",
"dist/output.txt",
], comment = "output file restored from cache" },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# cache_hit_skips_log_replay_but_restores_outputs

`replayLogs: false` should keep cache hits and output restoration, while suppressing cached stdout/stderr replay.

## `vt run build`

first run — cache miss, prints logs and writes dist/output.txt

```
$ vtt print fresh logs
fresh logs

$ vtt write-file dist/output.txt artifact

---
vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details)
```

## `vtt rm -rf dist`

delete dist/ to prove cache-hit output restoration still happens

```
```

## `vt run build`

second run — cache hit, skips cached log replay

```
$ vtt print fresh logs ◉ cache hit

$ vtt write-file dist/output.txt artifact ◉ cache hit

---
vt run: 2/2 cache hit (100%). (Run `vt run --last-details` for full details)
```

## `vtt print-file dist/output.txt`

output file restored from cache

```
artifact
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tasks": {
"build": {
"command": "vtt print fresh logs && vtt write-file dist/output.txt artifact",
"cache": true,
"replayLogs": false,
"input": [],
"output": ["dist/**"]
}
}
}
4 changes: 4 additions & 0 deletions crates/vt_graph/run-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ dependsOn?: Array<DependsOnEntry>, } & ({
* Whether to cache the task
*/
cache?: true,
/**
* Whether to replay cached stdout/stderr on cache hits.
*/
replayLogs?: boolean,
/**
* Environment variable names to be fingerprinted and passed to the task.
*/
Expand Down
16 changes: 12 additions & 4 deletions crates/vt_graph/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ impl ResolvedTaskOptions {
let cache_config = match user_options.cache_config {
UserCacheConfig::Disabled { cache: MustBe!(false) } => None,
UserCacheConfig::Enabled { cache: _, enabled_cache_config } => {
let replay_logs = enabled_cache_config.replay_logs.unwrap_or(true);
let mut untracked_env: FxHashSet<Str> =
enabled_cache_config.untracked_env.unwrap_or_default().into_iter().collect();
untracked_env.extend(DEFAULT_UNTRACKED_ENV.iter().copied().map(Str::from));
Expand All @@ -83,6 +84,7 @@ impl ResolvedTaskOptions {
)?;

Some(CacheConfig {
replay_logs,
env_config: EnvConfig {
fingerprinted_envs: enabled_cache_config
.env
Expand All @@ -100,16 +102,22 @@ impl ResolvedTaskOptions {
}

#[derive(Debug, Clone, Serialize)]
#[expect(
clippy::struct_field_names,
reason = "env_config, input_config, output_config are distinct config categories, not a naming smell"
)]
pub struct CacheConfig {
#[serde(skip_serializing_if = "is_true")]
pub replay_logs: bool,
pub env_config: EnvConfig,
pub input_config: ResolvedGlobConfig,
pub output_config: ResolvedGlobConfig,
}

#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde's skip_serializing_if callback receives the field by reference"
)]
const fn is_true(value: &bool) -> bool {
*value
}

/// Resolved input configuration for cache fingerprinting.
///
/// This is the normalized form after parsing user config.
Expand Down
26 changes: 26 additions & 0 deletions crates/vt_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ impl UserCacheConfig {
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(optional_fields))]
#[serde(rename_all = "camelCase")]
pub struct EnabledCacheConfig {
/// Whether to replay cached stdout/stderr on cache hits.
pub replay_logs: Option<bool>,

/// Environment variable names to be fingerprinted and passed to the task.
pub env: Option<Box<[Str]>>,

Expand Down Expand Up @@ -260,6 +263,7 @@ impl Default for UserTaskOptions {
cache_config: UserCacheConfig::Enabled {
cache: None,
enabled_cache_config: EnabledCacheConfig {
replay_logs: None,
env: None,
untracked_env: None,
input: None,
Expand Down Expand Up @@ -702,6 +706,7 @@ mod tests {
UserCacheConfig::Enabled {
cache: Some(MustBe!(true)),
enabled_cache_config: EnabledCacheConfig {
replay_logs: None,
env: Some(std::iter::once("NODE_ENV".into()).collect()),
untracked_env: Some(std::iter::once("FOO".into()).collect()),
input: None,
Expand All @@ -711,6 +716,27 @@ mod tests {
);
}

#[test]
fn test_replay_logs_can_be_disabled() {
let user_config_json = json!({
"cache": true,
"replayLogs": false,
});
assert_eq!(
serde_json::from_value::<UserCacheConfig>(user_config_json).unwrap(),
UserCacheConfig::Enabled {
cache: Some(MustBe!(true)),
enabled_cache_config: EnabledCacheConfig {
replay_logs: Some(false),
env: None,
untracked_env: None,
input: None,
output: None,
}
},
);
}

#[test]
fn test_input_empty_array() {
let user_config_json = json!({
Expand Down
12 changes: 12 additions & 0 deletions crates/vt_plan/src/cache_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pub struct CacheMetadata {
/// Used at execution time to determine what output files to archive.
pub output_config: ResolvedGlobConfig,

/// Whether cached stdout/stderr should be replayed on cache hits.
#[serde(skip_serializing_if = "is_true")]
pub replay_logs: bool,

/// The unfiltered env context for runner-aware APIs. This is the planning
/// context's envs before spawn-env filtering, including command prefix envs
/// from this command and enclosing nested `vp run` expansions.
Expand All @@ -66,6 +70,14 @@ pub struct CacheMetadata {
pub unfiltered_envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
}

#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde's skip_serializing_if callback receives the field by reference"
)]
const fn is_true(value: &bool) -> bool {
*value
}

/// Fingerprint for spawn execution that affects caching.
///
/// # Environment Variable Impact on Cache
Expand Down
Loading