Turborepo · beginner
How to verify Turborepo cache inputs include environment dependencies
Turborepo caches task outputs based on a content hash derived from declared inputs. When an environment dependency changes but is not listed in a task's `inputs`, the hash is unchanged, the cache returns a stale artifact, and the task fails to invalidate. This guide shows how to verify that environment variables, dotenv files, and related inputs are wired into a Turborepo task so that the cache invalidates correctly.
The symptoms
- •Task output is reused from cache even though an environment variable was modified between runs.
- •Changing a `.env` file (for example `apps/web/.env`) does not trigger a cache miss for the dependent task, so a stale build or test artifact is served.
- •CI and local runs produce different results for the same task hash, indicating an undeclared environment dependency.
- •`turbo run <task> --summarize` or `--log-order` shows the task marked `CACHE HIT` despite a known environment change.
- •`turbo run <task> --force` reproduces the expected behavior, confirming the cache, not the code, is the source of the stale result.
Likely causes
- •The task definition in `turbo.json` omits `"inputs": ["$TURBO_ROOT$/.env", ".env", ".env.*", "*.env"]`, so dotenv files do not contribute to the hash.
- •Environment variables consumed by the task are not enumerated under `"env": ["MODE", "NODE_ENV", "API_URL", ...]` in the task definition, so they are excluded from hashing.
- •Glob patterns in `inputs` are scoped too tightly and miss environment files in workspaces, package subdirectories, or shared config roots.
- •`globalDependencies` is set to file paths or globs that do not match the actual environment files used at runtime.
- •`globalEnv` is not declared at all, so process-level environment changes do not invalidate downstream tasks.
- •A task overrides inputs via `--ignore` flags in CI, silently dropping the environment dependency from the hash.
First ten minutes
- 01Confirm the symptom: run the affected task with `--summarize` and observe whether the task reports `CACHE HIT` after a known environment change.
- 02Inspect `turbo.json` (or the `pipeline` key in `package.json` for older layouts) and locate the task under verification.
- 03List every environment file the task reads at runtime: `.env`, `.env.local`, `.env.<mode>`, workspace-level dotenv, and CI-injected dotenv.
- 04Cross-check each file against the task's declared `inputs`, `globalDependencies`, `globalEnv`, and per-task `env`.
- 05Reproduce the misbehavior on demand by changing one declared input and confirming the hash changes; then change an undeclared environment file and confirm the hash is stable.
- 06Capture the current hash and the affected file list before any edit so the fix can be proved against the original state.
Evidence to collect
- •The full hash string printed by `turbo run <task> --summarize` for a known input change and for an environment file change.
- •The rendered `inputs` array for the task from `turbo.json`, including any inherited defaults from `globalDependencies` and `globalEnv`.
- •A diff between the environment files at the workspace root, each package root, and any `dotenv` directory consumed by the toolchain.
- •The environment variables exported into the task process, typically via a small `printenv` step or the runner's exposed env.
- •Logs from `--log-order=stream` showing per-task `CACHE HIT` or `CACHE MISS` annotations paired with the hash.
Where to look
- •The boundary between `turbo.json` declarations and the runtime environment: this is where hashing decisions are made.
- •The boundary between package-level dotenv files and the workspace-level files referenced by `globalDependencies` and `globalEnv`.
- •The boundary between CI-injected environment variables and local dotenv files, since CI often substitutes values without writing files.
- •The boundary between declared `env` keys and the actual variables read by the task, including transitive reads inside build plugins or test runners.
Diagnostic steps
- 01Run `turbo run <task> --summarize` twice, modifying a dotenv file between runs; if the hash is identical the file is not an input.
- 02Run `turbo run <task> --summarize` twice, modifying a declared input; if the hash is identical the input is not actually hashed (for example, it is excluded by an ignore pattern).
- 03Compare the task's `inputs` glob against the on-disk location of each environment file using a path-printing step; mismatches indicate the glob is too narrow.
- 04Inspect the `env` array in the task definition and cross-reference with the variables the process actually reads; missing keys mean those variables do not contribute to the hash.
- 05Check whether `globalEnv` is present and includes every variable that affects build output across tasks, not only the task under test.
- 06Reproduce in CI with the same hash to confirm the local diagnosis is not masked by a local-only file; if the CI hash differs, the CI environment is missing the same input declarations.
Common mistakes
- •Declaring `inputs: ["$TURBO_ROOT$/.env"]` but reading `.env.local` or `.env.production` at runtime, leaving those files outside the hash.
- •Listing only file globs and forgetting `env`, so a variable change (for example `API_URL`) does not invalidate the cache.
- •Adding `globalDependencies` but not `globalEnv`, so global dotenv files invalidate the hash while global variables do not.
- •Relying on `outputs` to imply inputs: outputs only declare what is cached, not what is hashed.
- •Forgetting that `passThroughEnv` excludes variables from hashing; variables in `passThroughEnv` will not invalidate the cache.
- •Assuming `turbo run --force` is a verification step rather than a bypass; it hides the underlying hashing defect.
Safe fixes
- •Add the missing dotenv paths to the task's `inputs`, including the workspace-level `.env` and any per-package `.env*` variants the task reads.
- •Add every environment variable that influences the task's output to the task's `env` array, keeping the list narrow to avoid spurious cache misses.
- •Promote variables that affect more than one task to `globalEnv` so a single change invalidates every dependent task.
- •Move shared environment files into the workspace root and reference them through `$TURBO_ROOT$/.env` style inputs so all packages share the same hash contribution.
- •Remove variables that should not influence caching from `env` and place them in `passThroughEnv` to keep the hash stable for irrelevant runtime values.
Prove the fix
- 01Changing a previously unlisted dotenv file now produces a different task hash on `turbo run <task> --summarize`.
- 02Changing a previously unlisted environment variable now produces a different task hash and the affected task reports `CACHE MISS`.
- 03A control run that does not touch any input produces an identical hash before and after the configuration change, confirming no spurious misses.
- 04CI and local runs now produce the same hash for identical inputs, eliminating environment-only drift.
- 05Removing the change and re-running returns to `CACHE HIT`, confirming the new inputs are scoped correctly and do not over-invalidate.
Prevention and next steps
- •Treat the task's `inputs`, `env`, `globalDependencies`, and `globalEnv` as a contract: every file and variable read by the task must appear in at least one of these arrays.
- •Add a CI check that runs `turbo run <task> --summarize` with a synthetic dotenv or env mutation, asserting the hash changes.
- •Document the environment contract for each task alongside its `turbo.json` entry so reviewers can spot missing entries.
- •Prefer workspace-root dotenv files referenced via `$TURBO_ROOT$` to reduce per-package drift.
Safe commands and checks
turbo run <task> --summarize turbo run <task> --log-order=stream turbo run <task> --dry-run=json cat turbo.json grep -RIn "env" turbo.json package.json apps/<pkg>/package.json packages/<pkg>/package.json