fix(core): make the cache stack survive CI checkouts and the action cache#1060
Conversation
…ache The whole-repo scan cache escaped the action's cached path entirely (own tmpdir resolution) and three key inputs (config/dotenv/toolchain stats) rotated on every fresh checkout; the dead-code caches were stat-keyed so re-cloned trees never hit. Route every cache through the shared cache-dir resolution, content-hash the checkout-sensitive key inputs, add ninja-style mtime repair to both dead-code caches, TTL-prune the supply-chain store, and stop the PR baseline sub-scan from minting orphan cache dirs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📦 GitHub Action release recommendedThis PR changes the React Doctor GitHub Action's release surface:
The composite action is versioned independently from the npm packages, so it After merging, cut the tag from the merge commit on git checkout main && git pull --ff-only
merge_commit=$(git rev-parse HEAD)
git tag -a v2.2.6 "$merge_commit" -m "react-doctor action v2.2.6"
git tag -fa v2 "$merge_commit" -m "react-doctor action v2 (floating major -> v2.2.6)"
git push origin v2.2.6
git push --force origin v2 # moves only the floating major pointerThis bump can also be performed automatically on merge — set the repo |
commit: |
| export const computeConfigFingerprint = (projectDirectory: string, version: string): string => { | ||
| const parts: string[] = [`v=${version}`]; | ||
| let directory = projectDirectory; | ||
| for (;;) { | ||
| for (const filename of CONFIG_FINGERPRINT_FILENAMES) { | ||
| try { | ||
| const stat = fs.statSync(path.join(directory, filename)); | ||
| parts.push(`${directory}/${filename}=${stat.mtimeMs}:${stat.size}`); | ||
| } catch { | ||
| continue; | ||
| } | ||
| const contentHash = hashFileContents(path.join(directory, filename)); | ||
| if (contentHash !== null) parts.push(`${directory}/${filename}=${contentHash}`); | ||
| } | ||
| const parent = path.dirname(directory); | ||
| if (parent === directory) break; |
There was a problem hiding this comment.
🚩 Config fingerprint now hashes lockfile contents instead of stat-fingerprinting them
The computeConfigFingerprint at packages/core/src/utils/compute-config-fingerprint.ts:12-24 switched from fs.statSync (mtime + size) to hashFileContents (SHA-1 of file bytes) for every file in CONFIG_FINGERPRINT_FILENAMES. This list includes lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lock, bun.lockb) which can be tens of megabytes in large monorepos. The function also walks UP the directory tree to the filesystem root, hashing these files at every ancestor. The comment at lines 6-11 acknowledges this tradeoff ('lockfiles are the largest, and hashing is far cheaper than the scan they key'). In practice the cost is likely single-digit milliseconds for most projects, but for a monorepo with a 50 MB lockfile and deep nesting, the repeated hashing at each ancestor level could add up. Worth confirming this doesn't regress scan startup time on large monorepos.
(Refers to lines 12-24)
Was this helpful? React with 👍 or 👎 to provide feedback.
Why
An audit of the cache stack under the GitHub Action's cache found the headline feature broken: the whole-repo scan cache never worked in CI — it resolved its own on-disk location (
node_modules/.cache→os.tmpdir(), neither of which is the${runner.temp}/react-doctor-cachepath the action persists) AND three of its key inputs (config / dotenv / toolchain fingerprints, all mtime-based) rotated on every fresh checkout. The two dead-code caches were stat-fingerprinted, so re-cloned trees missed permanently, and the supply-chain purl store grew monotonically across restore→save cycles.CI simulation (fresh clone at a stable runner-style path each run, cache dir persisted, output equivalence vs no-cache controls on every run):
What changed
resolveReactDoctorCacheDir(env override → node_modules/.cache → uid-scoped tmpdir), landing inside the action's cached path; the tmpdir fallback gains the same uid/per-project scoping as the other caches.process.versionfor node, stat retained only for foreign nvm binaries.action.yml: comment-only documentation update — the release guard recommends a patch bump (v2.2.5 → v2.2.6) on merge.Test plan
vp fmt+smoke:json-reportclean.REACT_DOCTOR_CACHE_DIRhonored by the scan cache, fresh-checkout key stability, supply-chain TTL pruning.🤖 Generated with Claude Code
Note
Medium Risk
Touches cache invalidation across scan, dead-code, and deslop paths; behavior is designed fail-open with schema bumps and tests, but wrong repair logic could theoretically serve stale diagnostics until a content change.
Overview
Makes the GitHub Action’s persisted
REACT_DOCTOR_CACHE_DIRactually warm the full cache stack after fresh CI checkouts, where stat-only fingerprints and wrong on-disk paths previously forced cold scans.The whole-repo scan-result cache now lives under the shared
resolveReactDoctorCacheDirroot (same as lint, sidecar, dead-code, supply-chain) instead ofnode_modules/.cacheor an unscoped temp path. Its cache key no longer rotates on re-clone: config and dotenv inputs use content hashes; the toolchain fingerprint uses package versions (andprocess.versionfor the current Node) instead of install mtimes.Dead-code caching (schema v2) stops folding every file’s mtime into the key. Entries store per-file
(mtime, size, content hash); on lookup, stat mismatches with unchanged size trigger a one-time content hash and mtime repair (ninja/restat), persisted so the next run is stat-fast. deslop-js incremental summary cache (schema v2) gets the same repair for summaries and package facts, and manifest/bundler-config layers in collect/resolution hashes move to content hashes.Supply-chain cache prunes files older than the 24h TTL so restored CI caches don’t grow forever with unused purls. PR baseline sub-scans disable per-file and sidecar lint caches to avoid orphan subdirs in the persisted cache dir.
action.ymlcomments document the expanded restored cache layers.Reviewed by Cursor Bugbot for commit fcc18b8. Bugbot is set up for automated code reviews on this repo. Configure here.