Skip to content

fix(ci): cache every workspace's node_modules, not just the root and one app - #9596

Merged
JSONbored merged 1 commit into
mainfrom
fix/ci-workspace-node-modules-cache
Jul 28, 2026
Merged

fix(ci): cache every workspace's node_modules, not just the root and one app#9596
JSONbored merged 1 commit into
mainfrom
fix/ci-workspace-node-modules-cache

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

The failure

Every PR's UI lint step is failing on main right now:

> @loopover/ui-miner@0.0.0 lint
> eslint .
Error: Cannot find module '@eslint/js'
Require stack:
- /home/runner/work/loopover/loopover/apps/loopover-miner-ui/eslint.config.ts

Why

setup-workspace's node_modules cache listed only node_modules and apps/loopover-ui/node_modules. npm decides per dependency whether it hoists to the root or nests under a workspace, and that decision moves whenever a version range changes. #8608's eslint-10 bump made npm nest @eslint/js under both UI apps — so on a cache hit, apps/loopover-miner-ui/node_modules was never restored (it isn't in the path list) and the config's very first import failed.

It reproduces on no developer machine: a real npm ci / npm install always creates those directories, so only the cache-hit path is broken. That's also why it slipped through #9589's own green run — that run was a cache miss (the lockfile had just changed), which installed everything for real.

The fix

Glob apps/*/node_modules + packages/*/node_modules — the same workspace globs package.json declares — in both the restore and the save step, so the cache follows npm's layout automatically instead of re-breaking on the next hoist change. review-enrichment and control-plane are deliberately untouched: they are not workspaces, and they already have their own installs and their own cache steps.

Verified: actionlint and lint:composite-actions both clean. The real proof is this PR's own CI — a first run installs fresh and saves the widened cache; every subsequent PR then restores a complete tree.

…one app

The node_modules cache saved only `node_modules` and
`apps/loopover-ui/node_modules`. npm decides per dependency whether it
hoists to the root or nests under a workspace, and that decision moves
whenever a version range changes -- #8608's eslint-10 bump nested
`@eslint/js` under both UI apps, so on a cache HIT
`apps/loopover-miner-ui/node_modules` did not exist at all and
`eslint .` died with "Cannot find module '@eslint/js'", failing UI lint
on every PR. It reproduces on no developer machine, because a real
npm ci/npm install always creates those directories.

Glob `apps/*` + `packages/*` (the workspace globs from package.json) so
the cache follows npm's layout automatically instead of re-breaking on
the next hoist change. review-enrichment and control-plane are not
workspaces -- they keep their own installs and their own cache steps.
@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored self-assigned this Jul 28, 2026
@JSONbored
JSONbored merged commit a2c5a89 into main Jul 28, 2026
3 of 4 checks passed
@JSONbored
JSONbored deleted the fix/ci-workspace-node-modules-cache branch July 28, 2026 21:27
@JSONbored

Copy link
Copy Markdown
Owner Author

Follow-up correction pushed: widening path alone was not enough. The cache key is computed from the manifests only, so a branch with an unchanged lockfile keeps hitting the entry archived under the narrow path list — which restores no apps/loopover-miner-ui/node_modules, and being a hit, skips npm ci entirely. The tree would have stayed exactly as broken.

Added an explicit npm-v2- generation salt to the key so one clean miss re-archives a complete tree under the new list, with a comment stating the salt must be bumped whenever path changes.

JSONbored added a commit that referenced this pull request Jul 28, 2026
…y takes effect (#9597)

#9596 widened the node_modules cache to every workspace but left the key
untouched, so nothing changed for a branch with an unchanged lockfile: it
keeps HITTING the entry archived under the narrow list, which restores no
apps/loopover-miner-ui/node_modules, and a hit skips npm ci -- leaving the
tree exactly as broken and UI lint still failing on 'Cannot find module
@eslint/js'.

Add an explicit npm-v2- generation salt so one clean miss re-archives a
complete tree under the current path list, and document that the salt must
be bumped whenever that list changes.
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
23614 1 23613 21
View the top 1 failed test(s) by shortest run time
test/unit/ci-composite-setup-workspace.test.ts > setup-workspace composite action > skips npm ci only on an exact node_modules cache hit, and saves the cache only after a successful install
Stack Traces | 0.0263s run time
AssertionError: expected 'node_modules\napps/*/node_modules\npa…' to contain 'apps/loopover-ui/node_modules'

- Expected
+ Received

- apps/loopover-ui/node_modules
+ node_modules
+ apps/*/node_modules
+ packages/*/node_modules
+

 ❯ test/unit/ci-composite-setup-workspace.test.ts:56:38

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

JSONbored added a commit that referenced this pull request Jul 28, 2026
…ath list (#9603)

#9596/#9597 changed the node_modules cache to glob every workspace and
added a generation salt to the key, but the composite-action test still
asserted the old literal `apps/loopover-ui/node_modules`, so it has been
failing on main and on every branch since.

Assert the contract that now holds, and tighten it in two places the old
test left open:
- both workspace globs, matching what package.json declares;
- the `npm-v2-` salt, asserted beside the paths, because widening the
  paths without bumping the salt is a no-op and the two must move
  together;
- the SAVE step's path list equals the RESTORE step's -- the save list
  decides what a future run can restore, so a narrowing there would stay
  invisible until some later job failed on a missing workspace.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant