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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

- **Fixed** An invalid glob in `--filter` no longer shows its error message twice ([#763](https://github.com/voidzero-dev/vite-task/pull/763)).
- **Changed** The detailed summary from `vp run --verbose` and `vp run --last-details` now shows each underlying cause of an error on its own line ([#761](https://github.com/voidzero-dev/vite-task/pull/761)).
- **Added** Remote caching. Configure an endpoint with the workspace's `cache: { remote: { url } }` or `VP_REMOTE_CACHE_URL`, and choose access with `--remote-cache=off|read|read-write` or `VP_REMOTE_CACHE`. The default is `read` with an endpoint and `off` without one. After a local cache miss, `vp run` looks the task up in the remote cache and, on a hit, restores its outputs and caches it locally. A failed read is just a cache miss, with the failure as its reason. In `read-write` mode, `vp run` also uploads the results of successful, cacheable tasks after caching them locally. A failed upload doesn't fail the task; the run summary shows a warning instead. Tasks can opt out with `cache: { remote: false }` ([#727](https://github.com/voidzero-dev/vite-task/pull/727), [#755](https://github.com/voidzero-dev/vite-task/pull/755), [#756](https://github.com/voidzero-dev/vite-task/pull/756)).
- **Added** Remote caching. Configure an endpoint with the workspace's `cache: { remote: { url } }` or `VP_REMOTE_CACHE_URL`, and choose access with `--remote-cache=off|read|read-write` or `VP_REMOTE_CACHE`. The default is `read` with an endpoint and `off` without one. After a local cache miss, `vp run` looks the task up in the remote cache and, on a hit, restores its outputs and caches it locally. The task output and the run summary show which hits came from the remote cache. A failed read is just a cache miss, with the failure as its reason. In `read-write` mode, `vp run` also uploads the results of successful, cacheable tasks after caching them locally. A failed upload doesn't fail the task; the run summary shows a warning instead. Tasks can opt out with `cache: { remote: false }` ([#727](https://github.com/voidzero-dev/vite-task/pull/727), [#755](https://github.com/voidzero-dev/vite-task/pull/755), [#756](https://github.com/voidzero-dev/vite-task/pull/756), [#757](https://github.com/voidzero-dev/vite-task/pull/757)).
- **Fixed** On Windows, environment variable names used by `vp run` now match regardless of ASCII letter case. Assignments in task commands override earlier assignments and inherited variables spelled differently, and `FORCE_COLOR`, `VP_RUN_CONCURRENCY_LIMIT`, and variables requested through `@voidzero-dev/vite-task-client` are found under any spelling ([#747](https://github.com/voidzero-dev/vite-task/pull/747)).
- **Changed** A task's cache settings now go inside `cache`, e.g. `cache: { env: ["NODE_ENV"], input: ["src/**"] }`; `cache: true` is the same as `cache: {}`. `env`, `untrackedEnv`, `input`, and `output` are no longer supported at the top level of a task ([#749](https://github.com/voidzero-dev/vite-task/pull/749)).
- **Fixed** Cached tasks on macOS no longer intermittently fail with exit 2 and `oils I/O error (main): No such process` when a fast command finishes before the shell gets scheduled. The bundled shell that runs task commands is updated to Oils 0.38.0, which fixes this race ([#702](https://github.com/voidzero-dev/vite-task/issues/702), [#703](https://github.com/voidzero-dev/vite-task/pull/703)).
Expand Down
28 changes: 24 additions & 4 deletions crates/vt/src/session/cache/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use serde::{Deserialize, Serialize};
use vt_plan::cache_metadata::SpawnFingerprint;
use vt_str::Str;

use super::{CacheMiss, EnvMismatch, FingerprintMismatch, InputChangeKind, split_path};
use super::{
CacheHitSource, CacheMiss, EnvMismatch, FingerprintMismatch, InputChangeKind, split_path,
};
use crate::session::event::CacheStatus;

/// Describes a single atomic change between two spawn fingerprints.
Expand Down Expand Up @@ -141,17 +143,20 @@ fn format_env_changed_inline(names: &[&Str]) -> Str {
/// Format cache status for inline display (during Start event).
///
/// Returns `Some(formatted_string)` for Hit, Miss with reason, and Disabled, None for `NotFound`.
/// - Cache Hit: Shows "cache hit" indicator
/// - Cache Hit: Shows "cache hit" indicator, or "remote cache hit" for a remote hit
/// - Cache Miss (NotFound): No inline message (just command)
/// - Cache Miss (with mismatch): Shows "cache miss" with brief reason
/// - Cache Disabled: Shows "cache disabled" with reason
///
/// 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 { .. } => {
CacheStatus::Hit { source, .. } => {
// Show "cache hit" indicator when replaying from cache
Some(Str::from("◉ cache hit, replaying"))
Some(match source {
CacheHitSource::Local => Str::from("◉ cache hit, replaying"),
CacheHitSource::Remote => Str::from("◉ remote cache hit, replaying"),
})
}
CacheStatus::Miss(CacheMiss::NotFound) => {
// No inline message for "not found" case - just show command
Expand Down Expand Up @@ -249,6 +254,21 @@ mod tests {
);
}

#[test]
fn inline_cache_hit_names_remote_source() {
let hit =
|source| CacheStatus::Hit { replayed_duration: std::time::Duration::ZERO, source };

assert_eq!(
format_cache_status_inline(&hit(CacheHitSource::Local)).as_deref(),
Some("◉ cache hit, replaying")
);
assert_eq!(
format_cache_status_inline(&hit(CacheHitSource::Remote)).as_deref(),
Some("◉ remote cache hit, replaying")
);
}

#[test]
fn inline_tracked_env_mismatch_preserves_kind() {
let added = CacheStatus::Miss(CacheMiss::FingerprintMismatch(
Expand Down
25 changes: 21 additions & 4 deletions crates/vt/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,23 @@ pub struct ExecutionCache {
remote_clients: RemoteClients,
}

/// A cache hit: the entry to replay, and the cache it came from.
#[derive(Debug)]
pub struct CacheHit {
pub value: CacheEntryValue,
pub source: CacheHitSource,
}

/// The cache a hit came from.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum CacheHitSource {
/// The local cache.
#[default]
Local,
/// The remote cache. The entry has since been recorded locally.
Remote,
}

#[derive(Debug, Clone)]
#[expect(
clippy::large_enum_variant,
Expand Down Expand Up @@ -329,7 +346,7 @@ impl ExecutionCache {
}

/// Try to hit cache by looking up the cache entry key and validating inputs.
/// Returns `Ok(Ok(cache_value))` on cache hit, `Ok(Err(cache_miss))` on miss.
/// Returns `Ok(Ok(cache_hit))` on cache hit, `Ok(Err(cache_miss))` on miss.
///
/// After a local miss, the remote cache is queried if the task has one. A
/// remote hit is recorded locally, with its output archive downloaded into
Expand All @@ -343,14 +360,14 @@ impl ExecutionCache {
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
workspace_root: &AbsolutePath,
cache_dir: &AbsolutePath,
) -> anyhow::Result<Result<CacheEntryValue, CacheMiss>> {
) -> anyhow::Result<Result<CacheHit, CacheMiss>> {
let cache_key = CacheEntryKey::from_metadata(cache_metadata);

let local_miss = match self
.try_hit_local(cache_metadata, &cache_key, globbed_inputs, workspace_root)
.await?
{
Ok(cache_value) => return Ok(Ok(cache_value)),
Ok(value) => return Ok(Ok(CacheHit { value, source: CacheHitSource::Local })),
Err(miss) => miss,
};
#[expect(
Expand All @@ -375,7 +392,7 @@ impl ExecutionCache {
)
.await?
{
Ok(cache_value) => return Ok(Ok(cache_value)),
Ok(value) => return Ok(Ok(CacheHit { value, source: CacheHitSource::Remote })),
Err(miss) => miss,
};
Ok(Err(match local_miss {
Expand Down
4 changes: 2 additions & 2 deletions crates/vt/src/session/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{process::ExitStatus, time::Duration};
use vt_path::RelativePathBuf;
use vt_server::Error as IpcServerError;

use super::cache::{CacheMiss, remote::UploadError};
use super::cache::{CacheHitSource, CacheMiss, remote::UploadError};

/// The cache operation that failed.
#[derive(Debug)]
Expand Down Expand Up @@ -129,7 +129,7 @@ pub enum CacheUpdateStatus {
pub enum CacheStatus {
Disabled(CacheDisabledReason),
Miss(CacheMiss),
Hit { replayed_duration: Duration },
Hit { replayed_duration: Duration, source: CacheHitSource },
}

/// Convert `ExitStatus` to an i32 exit code.
Expand Down
10 changes: 5 additions & 5 deletions crates/vt/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use self::{
spawn::{ChildHandle, ChildOutcome, SpawnStdio, spawn},
};
use super::{
cache::{CacheEntryValue, CacheMiss, ExecutionCache, archive},
cache::{CacheEntryValue, CacheHit, CacheMiss, ExecutionCache, archive},
event::{
CacheDisabledReason, CacheErrorKind, CacheNotUpdatedReason, CacheStatus, CacheUpdateStatus,
ExecutionError,
Expand Down Expand Up @@ -384,9 +384,9 @@ async fn run(
// runs exactly once on every arm) and either replay the hit — no need
// to execute the command — or carry the globbed inputs into the run.
let (stdio_config, globbed_inputs) = match lookup {
CacheLookup::Hit(cached) => {
CacheLookup::Hit(CacheHit { value: cached, source }) => {
let mut stdio_config =
reporter.start(CacheStatus::Hit { replayed_duration: cached.duration });
reporter.start(CacheStatus::Hit { replayed_duration: cached.duration, source });
return Ok(replay_cache_hit(
&mut stdio_config,
&cached,
Expand Down Expand Up @@ -501,8 +501,8 @@ async fn run(
/// reason plus the globbed inputs (reused by the cache-update phase after the
/// run), and disabled has neither.
enum CacheLookup {
/// Cache hit — the cached entry to replay.
Hit(CacheEntryValue),
/// Cache hit — the cached entry to replay, and where it came from.
Hit(CacheHit),
/// Cache miss — the detailed reason (`NotFound` or `FingerprintMismatch`).
Miss { miss: CacheMiss, globbed_inputs: BTreeMap<RelativePathBuf, u64> },
/// Caching is disabled for this task (no cache metadata).
Expand Down
Loading
Loading