From 253d61fe1675b9cfd45b146e8a46b37f5902f3ce Mon Sep 17 00:00:00 2001 From: wan9chi Date: Sun, 8 Nov 2026 21:17:14 +0800 Subject: [PATCH 1/3] fix(cache): don't record env queries that match every name Since Vite 8.1.0, `loadEnv(mode, dir, prefixes)` sends each prefix to the runner as a tracked `getEnvs({ prefix })` query; 8.1.0-beta.0 sent the glob `*` instead. Astro, React Router, TanStack Start and SvelteKit call `loadEnv(mode, dir, '')`, so the runner recorded the whole parent env in the cache key. On GitHub Actions, per-run variables such as `GITHUB_RUN_ID`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` and `INVOCATION_ID` then made every run miss. The rule: a `getEnvs` query for `Prefix("")` or the glob `*` returns nothing and records nothing, whether or not it is tracked. Every other query is unchanged, so narrow ones like `VITE_` stay tracked. `loadEnv('')` still returns the task's own env: after merging the runner's reply, Vite copies every matching `process.env` variable, so the result is the `.env` files plus the env the task was spawned with. Serving the full parent env untracked would not have been safe: React Router does `Object.assign(process.env, loadEnv(mode, dir, ''))`, which would copy withheld variables into `process.env`, and later reads of them would be dependencies the cache key misses. Trade-offs: - Variables the task config withholds are no longer visible through `loadEnv('')` without configuration. A SvelteKit app importing `DATABASE_URL` from `$env/static/private` that is set only in CI fails to build until the variable is declared in the task's `env`. - Default-untracked variables that a framework inlines, such as `GITHUB_SHA` through SvelteKit's static private env, are no longer tracked through this path. That's the same risk as reading `process.env` directly under `vp run`. - A tool that calls `getEnvs({ prefix: '' })` directly gets `{}`; the client docs now say to read `process.env` instead. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../fixtures/vite_build_cache/snapshots.toml | 54 +++++++++++++++++++ ...nv_without_prefix_ignores_unrelated_env.md | 33 ++++++++++++ .../fixtures/vite_build_cache/vite-task.json | 4 ++ .../vite.load-all-env.config.js | 9 ++++ crates/vt_server/src/lib.rs | 7 +++ packages/vite-task-client/src/index.d.ts | 4 ++ packages/vite-task-client/src/index.js | 4 ++ 8 files changed, 116 insertions(+) create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_load_env_without_prefix_ignores_unrelated_env.md create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite.load-all-env.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f3c73a2..6ccdf32d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** Builds whose config calls Vite's `loadEnv(mode, dir, '')`, as Astro, React Router, TanStack Start and SvelteKit do, no longer miss the cache on every CI run. That call made `vp run` add every environment variable to the cache key, including ones that change on each run, such as `GITHUB_RUN_ID` on GitHub Actions. Now `loadEnv(mode, dir, '')` returns the `.env` files and the variables the task receives, and adds nothing to the cache key itself. A variable the task doesn't receive, such as a `DATABASE_URL` that SvelteKit reads through `$env/static/private`, must now be added to the task's `cache: { env: [...] }` ([#976](https://github.com/voidzero-dev/vite-task/pull/976)). - **Added** `vp run --json` prints every package and task in the workspace, with each task's `dependsOn` resolved to task IDs, as one JSON document for scripts, editors and coding agents. It runs nothing and, like `vp run` without a task, loads every package's config. It prints the same document from any directory in the workspace: indented in a terminal, on one line when captured. Its `$schema` field points to a JSON Schema that describes each field, which `vp run` writes to the system temp directory ([#948](https://github.com/voidzero-dev/vite-task/pull/948)). - **Fixed** On Linux, where every user shares the temp folder `/tmp`, the helper files `vp run` writes there, such as the library that tracks file accesses, now go in a folder only the current user can write to, like `/tmp/fspy-1000`. Before, they went in a folder such as `/tmp/fspy` that belonged to whoever ran `vp run` first: running a different version as another user failed, and the same version used files that user could replace ([#947](https://github.com/voidzero-dev/vite-task/pull/947)). - **Fixed** On macOS, task commands that use `sh` or `bash` now behave as they do on Linux in four cases. A `while read` loop with a redirect under `set -e` no longer exits the script. An `EXIT` trap now sees the exit status in `$?`, and a bare `exit` in the trap keeps that status, so a failed task is no longer reported as successful. `$BASH` is now set when the command runs as `bash`. The bundled shell is updated to an Oils build from master that has these fixes ([#939](https://github.com/voidzero-dev/vite-task/pull/939)). diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml index 4248aa961..a510d240d 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots.toml @@ -179,3 +179,57 @@ steps = [ ], ], comment = "cache miss: envs changed (NODE_ENV changed)" }, ] + +[[e2e]] +name = "vite_load_env_without_prefix_ignores_unrelated_env" +comment = """ +A config calling `loadEnv(mode, dir, '')` makes Vite send `getEnvs({ prefix: '' })`, a query that matches every env name. The runner answers it with nothing and records nothing, so `loadEnv` returns the task's own env from `process.env`: `APP_LABEL`, which the task declares, but not `UNRELATED`. A change to `UNRELATED` doesn't make the next run miss. +""" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "--cache", + "build-load-all-env", + ], envs = [ + [ + "APP_LABEL", + "alpha", + ], + [ + "UNRELATED", + "one", + ], + ], comment = "first run: loadEnv sees the declared APP_LABEL only" }, + { argv = [ + "vt", + "run", + "--cache", + "build-load-all-env", + ], envs = [ + [ + "APP_LABEL", + "alpha", + ], + [ + "UNRELATED", + "two", + ], + ], comment = "cache hit: UNRELATED changed, but it isn't in the cache key" }, + { argv = [ + "vt", + "run", + "--cache", + "build-load-all-env", + ], envs = [ + [ + "APP_LABEL", + "bravo", + ], + [ + "UNRELATED", + "two", + ], + ], comment = "cache miss: the declared APP_LABEL changed" }, +] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_load_env_without_prefix_ignores_unrelated_env.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_load_env_without_prefix_ignores_unrelated_env.md new file mode 100644 index 000000000..e4875cd1c --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/snapshots/vite_load_env_without_prefix_ignores_unrelated_env.md @@ -0,0 +1,33 @@ +# vite_load_env_without_prefix_ignores_unrelated_env + +A config calling `loadEnv(mode, dir, '')` makes Vite send `getEnvs({ prefix: '' })`, a query that matches every env name. The runner answers it with nothing and records nothing, so `loadEnv` returns the task's own env from `process.env`: `APP_LABEL`, which the task declares, but not `UNRELATED`. A change to `UNRELATED` doesn't make the next run miss. + +## `APP_LABEL=alpha UNRELATED=one vt run --cache build-load-all-env` + +first run: loadEnv sees the declared APP_LABEL only + +``` +$ vite build --config vite.load-all-env.config.js +APP_LABEL=alpha UNRELATED=(unset) +``` + +## `APP_LABEL=alpha UNRELATED=two vt run --cache build-load-all-env` + +cache hit: UNRELATED changed, but it isn't in the cache key + +``` +$ vite build --config vite.load-all-env.config.js ◉ cache hit, replaying +APP_LABEL=alpha UNRELATED=(unset) + +--- +vt run: cache hit. +``` + +## `APP_LABEL=bravo UNRELATED=two vt run --cache build-load-all-env` + +cache miss: the declared APP_LABEL changed + +``` +$ vite build --config vite.load-all-env.config.js ○ cache miss: env 'APP_LABEL' changed, executing +APP_LABEL=bravo UNRELATED=(unset) +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json index 44a96b59b..985080a4b 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite-task.json @@ -6,6 +6,10 @@ // every `VITE_*` env via `getEnvs`, so the glob + match-set are // fingerprinted automatically. "cache": true + }, + "build-load-all-env": { + "command": "vite build --config vite.load-all-env.config.js", + "cache": { "env": ["APP_LABEL"] } } } } diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite.load-all-env.config.js b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite.load-all-env.config.js new file mode 100644 index 000000000..d0fc00639 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/vite_build_cache/vite.load-all-env.config.js @@ -0,0 +1,9 @@ +import { defineConfig, loadEnv } from 'vite'; + +// Reads the whole env the way frameworks such as SvelteKit do, with +// `loadEnv(mode, dir, '')`, and prints what it got for two variables. +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + console.log(`APP_LABEL=${env.APP_LABEL ?? '(unset)'} UNRELATED=${env.UNRELATED ?? '(unset)'}`); + return { logLevel: 'silent' }; +}); diff --git a/crates/vt_server/src/lib.rs b/crates/vt_server/src/lib.rs index dd079d172..cf031600a 100644 --- a/crates/vt_server/src/lib.rs +++ b/crates/vt_server/src/lib.rs @@ -157,6 +157,13 @@ impl Handler for Recorder { query: &IpcEnvQuery<'_>, tracked: bool, ) -> Result>, Arc>, vt_glob::env::EnvGlobError> { + // A query that matches every name, like the one Vite's + // `loadEnv(mode, dir, '')` sends, gets nothing and records nothing. + // Recording it would put every variable of the parent env in the cache + // key, so per-run ones, such as a CI run ID, would make every run miss. + if matches!(query, IpcEnvQuery::Prefix("") | IpcEnvQuery::Glob("*")) { + return Ok(FxHashMap::default()); + } let key = match query { IpcEnvQuery::Glob(pattern) => EnvQuery::Glob(Arc::from(*pattern)), IpcEnvQuery::Prefix(prefix) => EnvQuery::Prefix(Arc::from(*prefix)), diff --git a/packages/vite-task-client/src/index.d.ts b/packages/vite-task-client/src/index.d.ts index f61f2882a..92e16fcbd 100644 --- a/packages/vite-task-client/src/index.d.ts +++ b/packages/vite-task-client/src/index.d.ts @@ -51,6 +51,10 @@ export function getEnv(name: string, options?: { * dependency, so adding, removing, or changing a matching env invalidates * this run's cache entry. * + * A query that matches every env name, `{ prefix: '' }` or `*`, returns an + * empty object and records nothing: read the task's own env from + * `process.env` instead. + * * Has no effect on `process.env`; the caller decides what to do with the * returned values. Returns an empty object when not running inside a runner. * diff --git a/packages/vite-task-client/src/index.js b/packages/vite-task-client/src/index.js index 7a135f65d..c1e69ccf4 100644 --- a/packages/vite-task-client/src/index.js +++ b/packages/vite-task-client/src/index.js @@ -116,6 +116,10 @@ export function getEnv(name, options) { * dependency, so adding, removing, or changing a matching env invalidates * this run's cache entry. * + * A query that matches every env name, `{ prefix: '' }` or `*`, returns an + * empty object and records nothing: read the task's own env from + * `process.env` instead. + * * Has no effect on `process.env`; the caller decides what to do with the * returned values. Returns an empty object when not running inside a runner. * From 286faf5caacd8814cabe05732b84da6317c1d6f3 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Sun, 8 Nov 2026 21:24:11 +0800 Subject: [PATCH 2/3] docs(changelog): simplify the loadEnv entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ccdf32d3..091c093c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -- **Fixed** Builds whose config calls Vite's `loadEnv(mode, dir, '')`, as Astro, React Router, TanStack Start and SvelteKit do, no longer miss the cache on every CI run. That call made `vp run` add every environment variable to the cache key, including ones that change on each run, such as `GITHUB_RUN_ID` on GitHub Actions. Now `loadEnv(mode, dir, '')` returns the `.env` files and the variables the task receives, and adds nothing to the cache key itself. A variable the task doesn't receive, such as a `DATABASE_URL` that SvelteKit reads through `$env/static/private`, must now be added to the task's `cache: { env: [...] }` ([#976](https://github.com/voidzero-dev/vite-task/pull/976)). +- **Changed** Vite's `loadEnv(mode, dir, '')` no longer puts every environment variable in the cache key, so builds that call it, as SvelteKit and Astro do, no longer miss the cache on every CI run. It now sees only the variables the task receives; add any others the build needs to the task's `cache: { env: [...] }` ([#976](https://github.com/voidzero-dev/vite-task/pull/976)). - **Added** `vp run --json` prints every package and task in the workspace, with each task's `dependsOn` resolved to task IDs, as one JSON document for scripts, editors and coding agents. It runs nothing and, like `vp run` without a task, loads every package's config. It prints the same document from any directory in the workspace: indented in a terminal, on one line when captured. Its `$schema` field points to a JSON Schema that describes each field, which `vp run` writes to the system temp directory ([#948](https://github.com/voidzero-dev/vite-task/pull/948)). - **Fixed** On Linux, where every user shares the temp folder `/tmp`, the helper files `vp run` writes there, such as the library that tracks file accesses, now go in a folder only the current user can write to, like `/tmp/fspy-1000`. Before, they went in a folder such as `/tmp/fspy` that belonged to whoever ran `vp run` first: running a different version as another user failed, and the same version used files that user could replace ([#947](https://github.com/voidzero-dev/vite-task/pull/947)). - **Fixed** On macOS, task commands that use `sh` or `bash` now behave as they do on Linux in four cases. A `while read` loop with a redirect under `set -e` no longer exits the script. An `EXIT` trap now sees the exit status in `$?`, and a bare `exit` in the trap keeps that status, so a failed task is no longer reported as successful. `$BASH` is now set when the command runs as `bash`. The bundled shell is updated to an Oils build from master that has these fixes ([#939](https://github.com/voidzero-dev/vite-task/pull/939)). From a720b7e6cdf775e9c566d8768e1c12016ee371a4 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Sun, 8 Nov 2026 21:25:16 +0800 Subject: [PATCH 3/3] docs(changelog): describe the change in vite-task-client terms Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091c093c4..601ba9eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -- **Changed** Vite's `loadEnv(mode, dir, '')` no longer puts every environment variable in the cache key, so builds that call it, as SvelteKit and Astro do, no longer miss the cache on every CI run. It now sees only the variables the task receives; add any others the build needs to the task's `cache: { env: [...] }` ([#976](https://github.com/voidzero-dev/vite-task/pull/976)). +- **Changed** In `@voidzero-dev/vite-task-client`, a `getEnvs` query that matches every env name, `{ prefix: '' }` or `*`, now returns nothing instead of every environment variable, and no longer adds them to the cache key ([#976](https://github.com/voidzero-dev/vite-task/pull/976)). - **Added** `vp run --json` prints every package and task in the workspace, with each task's `dependsOn` resolved to task IDs, as one JSON document for scripts, editors and coding agents. It runs nothing and, like `vp run` without a task, loads every package's config. It prints the same document from any directory in the workspace: indented in a terminal, on one line when captured. Its `$schema` field points to a JSON Schema that describes each field, which `vp run` writes to the system temp directory ([#948](https://github.com/voidzero-dev/vite-task/pull/948)). - **Fixed** On Linux, where every user shares the temp folder `/tmp`, the helper files `vp run` writes there, such as the library that tracks file accesses, now go in a folder only the current user can write to, like `/tmp/fspy-1000`. Before, they went in a folder such as `/tmp/fspy` that belonged to whoever ran `vp run` first: running a different version as another user failed, and the same version used files that user could replace ([#947](https://github.com/voidzero-dev/vite-task/pull/947)). - **Fixed** On macOS, task commands that use `sh` or `bash` now behave as they do on Linux in four cases. A `while read` loop with a redirect under `set -e` no longer exits the script. An `EXIT` trap now sees the exit status in `$?`, and a bare `exit` in the trap keeps that status, so a failed task is no longer reported as successful. `$BASH` is now set when the command runs as `bash`. The bundled shell is updated to an Oils build from master that has these fixes ([#939](https://github.com/voidzero-dev/vite-task/pull/939)).