Skip to content

Bundler turbo build cache can be silently poisoned (hashed chunks + inputs miss the toolchain) #509

Description

@goosewobbler

Summary

@wdio/bundler#build can produce a self-inconsistent dist/ in the turbo remote cache, which is then replayed on every cache hit and breaks any package that builds through the bundler. This surfaced on #505 as repeated Build [<OS>] failures:

❌ Build failed: Rollup build failed: Cannot destructure property
   'injectDependencyPlugin' of '(intermediate value)' as it is undefined.
   node "…/packages/bundler/dist/cli.js" build

Mechanism

  • The bundler's compiled dist/cli.js loads its plugins via a content-hash-named dynamic chunk: await import('./plugins-<hash>.js').then(n => n.p).
  • packages/bundler/dist/ is gitignored — it is 100% built in CI / restored from the turbo remote cache.
  • When the cached cli.js and its plugins-<hash>.js chunk don't agree (the chunk doesn't export what cli.js expects), n.p is undefined → the destructure throws.
  • It stayed dormant for weeks: every service build was a turbo cache hit, so node bundler/dist/cli.js was never actually executed. The first service src/** cache-miss in a while (a launcher edit on feat(browser-mode): wire devServer auto-start into the four launchers (#417) #505) invoked the bundler CLI and hit the poisoned artifact. Green on one OS / red on another, and green locally (fresh, consistent dist/), are all consistent with a bad cached artifact rather than bad source.

Contributing factors

  1. build had no clean step — rollup's hashed chunks accumulate in dist/ across incremental builds, so a cached dist/** can contain a stale/foreign chunk alongside a newer cli.js.
  2. The bundler's turbo build.inputs don't capture the toolchain. inputs is ['src/**', 'tsconfig*.json', 'wdio-bundler.config.ts', 'rollup.config.ts', 'package.json'] — it does not include pnpm-lock.yaml. A rollup (or other build-dep) bump via a lockfile-only change keeps the same cache key while changing the actual build output → a future re-poison is possible.

Stopgap already shipped (on #505)

  • build(bundler): clean dist before rollup (shx rm -rf dist && rollup …) — busts the poisoned cache key (changes package.json) and forces a fresh, self-consistent rebuild that gets re-cached; prevents stale-chunk accumulation. (commit 01aa761a8)
  • @wdio/bundler declared as a devDependency on all bundler-consuming packages so turbo's ^build orders @wdio/bundler#build before consumers (commit 00c11d931).

This unblocks CI but doesn't permanently close factor #2.

Proposed durable fix (pick one/some)

  • Add pnpm-lock.yaml to the bundler's build.inputs (and any other package that runs a bundled toolchain) so a toolchain bump invalidates the cache key.
  • Give the plugins chunk a stable, non-hashed name (rollup output.chunkFileNames) or inline it, so cli.js ↔ chunk can't drift by filename.
  • Consider whether @wdio/bundler#build should be remote-cached at all, given it is the toolchain other builds depend on.

Notes

  • Adding the @wdio/bundler devDep also introduced harmless pnpm install-time WARNs (Failed to create bin … wdio-bundler … dist/cli.js) because the bin target doesn't exist until the bundler is built. Cosmetic; the build invokes the CLI by absolute path, not the linked bin.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:ciUpdates to Continuous Integration

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions