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

  1. 01Confirm the symptom: run the affected task with `--summarize` and observe whether the task reports `CACHE HIT` after a known environment change.
  2. 02Inspect `turbo.json` (or the `pipeline` key in `package.json` for older layouts) and locate the task under verification.
  3. 03List every environment file the task reads at runtime: `.env`, `.env.local`, `.env.<mode>`, workspace-level dotenv, and CI-injected dotenv.
  4. 04Cross-check each file against the task's declared `inputs`, `globalDependencies`, `globalEnv`, and per-task `env`.
  5. 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.
  6. 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

  1. 01Run `turbo run <task> --summarize` twice, modifying a dotenv file between runs; if the hash is identical the file is not an input.
  2. 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).
  3. 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.
  4. 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.
  5. 05Check whether `globalEnv` is present and includes every variable that affects build output across tasks, not only the task under test.
  6. 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

  1. 01Changing a previously unlisted dotenv file now produces a different task hash on `turbo run <task> --summarize`.
  2. 02Changing a previously unlisted environment variable now produces a different task hash and the affected task reports `CACHE MISS`.
  3. 03A control run that does not touch any input produces an identical hash before and after the configuration change, confirming no spurious misses.
  4. 04CI and local runs now produce the same hash for identical inputs, eliminating environment-only drift.
  5. 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