Skip to content

Check 3 — Task caching

Question: will a cache hit ever replay a wrong result, and are tasks re-running when they should hit? Output: findings/caching.json. If no orchestrator is detected, only caching/uncached-root-script applies; set status ok with reason no task orchestrator.

  • Turborepo: a task with no outputs caches only logs — on a hit, dist/ is NOT restored, so downstream tasks see stale or missing files. A package’s hash covers its own files, its package.json, turbo.json and the lockfile; changes in workspace dependencies reach a task’s hash only through dependsOn entries with ^ (e.g. ^build). Env vars affect the hash only when listed in env, globalEnv (or passThroughEnv for runtime-only); framework prefixes such as NEXT_PUBLIC_, VITE_ are auto-included. Turbo 2 strict env mode can also restrict which env vars are visible to tasks, so undeclared env is both a cache-key and runtime-visibility risk.
  • Nx: when outputs is omitted, Nx caches {workspaceRoot}/dist/{projectRoot}, {projectRoot}/dist, {projectRoot}/build, {projectRoot}/public. Env vars affect the hash only through an { "env": "NAME" } entry in inputs. Without a production named input that excludes tests, editing a spec file invalidates the build cache.
id severity detect
caching/missing-outputs high Turbo: a cacheable task named build/compile/bundle/*:build (or whose package script runs tsc, vite build, tsup, rollup, webpack, next build, esbuild) has no outputs. Package-level turbo.json overrides count. Nx: cacheable build-like target without outputs whose real out dir (outDir in the build tsconfig, outDir/--out-dir flags, build.outDir in vite config) is not one of Nx’s defaults — one finding per affected project, with package. Turbo findings are per task: location.file: "turbo.json", no package.
caching/build-inputs-include-tests low Turbo: build-like task has no inputs, or inputs that don’t exclude test globs (!**/*.test.*, !**/*.spec.*, !**/__tests__/**). Nx: no production named input, or build inputs don’t reference it, or production doesn’t negate test globs. Low severity because $TURBO_DEFAULT$ already covers all git-tracked files; the finding is about avoiding rebuilds when tests change, not correctness. Follow dependsOn chains: tests hashed by a dependency task (e.g. a transit task, or ^build of a package whose build inputs include tests) also invalidate the build. Measure impact (step 3) and put it in evidence.
caching/undeclared-env medium for each scan.env entry that can affect a cacheable task output. Flag build tasks only when the build inlines env (Next/Vite framework bundlers, webpack/Rspack/Rsbuild define, esbuild/tsup define, Rollup/plugin-replace, or similar bundlers), not runtime-only reads in a plain tsc service. Always flag cacheable test tasks when tests execute code that reads the env var (scan.env.isTest, or a test file in the package imports the module with the env read), because a cached pass can be replayed under a different value. Turbo: name not matched by the task’s env or globalEnv (* wildcards; ! negation) and not framework-prefixed. passThroughEnv / globalPassThroughEnv do not count: they make the var available without adding it to the hash, so a cached result can still be replayed under a different value. Nx: no { "env": NAME } in the target’s inputs (resolve named inputs). Ignore CI, PATH, HOME, TZ, PWD. Ignore NODE_ENV only when the scan confirms it cannot affect the output (e.g. the task’s script or bundler sets it to a fixed value); otherwise treat it like any other name. One finding per task config (Turbo task, or Nx target/targetDefault) that is missing declarations: list every missing name with its packages and up to 5 locations across all names, and set location.package only when a single package is affected.
caching/turbo-cache-disabled low Turbo task with cache: false that is deterministic by name: build, compile, bundle, lint, typecheck, check-types, format:check, or a unit test (test, test:unit, *:build, *:lint). Treat cache: false as deliberate — and do not flag — when the task is persistent: true; its name contains dev, start, serve, watch, deploy, release, publish, clean, db, migrat, seed, generate, extract, sync, upload, e2e, integration, or ends in :ci; or its env/passThroughEnv lists external-state vars (*DATABASE*, *DB_*, *REDIS*, *_URL, *TOKEN*, *SECRET*). Mention deliberately uncached tasks in reason, not as findings.
caching/unstable-hash high turbo installed: run TURBO_TELEMETRY_DISABLED=1 npx turbo run build --dry=json twice; any task whose hash differs between runs. Report the task and the inputs/environmentVariables that differ.
caching/volatile-input high non-negated task inputs or globalDependencies include build artifacts or machine-local files: dist, build, .turbo, .nx, *.tsbuildinfo, **/*.log, node_modules. Ignore negated globs (!…). Do not flag .env* files: including env files usually makes caches more correct.
caching/uncached-root-script low a root package.json script that runs tasks across packages bypassing the orchestrator: -r/--recursive, --workspaces/-ws, yarn workspaces foreach, lerna run. In repos without an orchestrator, say “consider adding Nx or Turborepo” in fix.
  1. Read turbo.json (Turbo v2 tasks and Turbo v1 pipeline, plus //#task root tasks and <pkg>/turbo.json overrides with "extends": ["//"]) and/or nx.json + every project.json / package.json#nx. For Nx, also read targetDefaults (including defaults keyed by executor), namedInputs, and legacy tasksRunnerOptions.*.options.cacheableOperations, and remember that Nx plugins (plugins in nx.json) can infer targets — if a target exists only via plugin inference, report rules that depend on its config as partial and say so.
  2. Apply the rules above. Quote the exact JSON in evidence (task name + its config object). Config-level findings (Turbo tasks, Nx targetDefaults/namedInputs) omit location.package; only set package when the problem is specific to one package (e.g. its own turbo.json/project.json, or an env read in its source).
  3. For build-inputs-include-tests, measure how often it matters with read-only git: git log --since="6 months ago" --name-only --format=%x00%H and count commits whose changed files are all test files (TEST pattern from scan.md). Report N of M commits (P%) changed only tests. Skip if the repo has no git history.
  4. unstable-hash runs only when turbo is installed; otherwise mark the check partial with reason turbo not installed; dry-run hash comparison skipped. Never run real tasks — only --dry=json.
  5. Write findings/caching.json.
  • missing-outputs (Turbo): "outputs": ["dist/**"] (add ".next/**", "!.next/cache/**" for Next.js).
  • missing-outputs (Nx): "outputs": ["{projectRoot}/lib"] matching the real out dir.
  • build-inputs-include-tests (Nx): "namedInputs": { "production": ["default", "!{projectRoot}/**/*.spec.ts", "!{projectRoot}/**/*.test.ts"] } and "build": { "inputs": ["production", "^production"] }.
  • build-inputs-include-tests (Turbo): "inputs": ["$TURBO_DEFAULT$", "!**/*.{test,spec}.*", "!**/__tests__/**"] ($TURBO_DEFAULT$ means all git-tracked files in the package). Apply it to every task in the build’s dependsOn chain that hashes sources (e.g. transit), or the build still rebuilds on test edits. Safe only if no package imports another package’s test files. Optional when the measured share of test-only commits is small.
  • undeclared-env: Turbo "env": ["API_URL"]; Nx "inputs": ["production", { "env": "API_URL" }].