Skip to content

fix(workers): use require.main === module guard so require() doesn't start workers#239

Merged
therealbrad merged 1 commit intomainfrom
fix/workers-main-guard
Apr 23, 2026
Merged

fix(workers): use require.main === module guard so require() doesn't start workers#239
therealbrad merged 1 commit intomainfrom
fix/workers-main-guard

Conversation

@therealbrad
Copy link
Copy Markdown
Contributor

Summary

  • Replace the broken ESM-style import.meta.url main guard in all 14 workers with the canonical CJS pattern require.main === module.
  • Drop the now-unused pathToFileURL import from each worker.

Why it matters — unblocks releases

PR #237's worker smoke test has been failing on every release since it merged. Root cause: esbuild compiles workers to CommonJS and polyfills import.meta as a plain object whose .url is undefined. The old guard was:

if (
  (typeof import.meta !== "undefined" &&
    import.meta.url === pathToFileURL(process.argv[1]).href) ||
  typeof import.meta === "undefined" ||
  (import.meta as any).url === undefined
) { startWorker()... }

At runtime import.meta.url === void 0 is always true in the CJS output, so the guard always fires — meaning require("./forecastWorker") actually runs startWorker(), opens Valkey connections, etc. Three workers (forecastWorker, repoCacheWorker, testmoImportWorker) then process.exit(1) synchronously when Valkey isn't reachable, killing the CI smoke test mid-loop.

With this change, require() in the smoke test only loads the module graph — exactly the guarantee #237 was supposed to provide. PM2 still starts each worker normally because it invokes them directly as node dist/workers/<name>.js (so require.main === module).

Scope

All 14 worker files in testplanit/workers/ except generateFromUrlWorker.ts (which didn't have the guard):

  • auditLogWorker · autoTagWorker · budgetAlertWorker · copyMoveWorker · duplicateScanWorker · elasticsearchReindexWorker · emailWorker · forecastWorker · magicSelectWorker · notificationWorker · repoCacheWorker · stepSequenceScanWorker · syncWorker · testmoImportWorker

Net: +26 / -112 lines.

Test plan

  • CI: smoke-test steps (AMD64 + ARM64) pass after merge — that's the primary validation this is fixing
  • Local: node scripts/smoke-test-workers.js passes against a freshly built dist/
  • Post-merge: verify PM2 still starts all workers in the :latest-workers image (bbva / production pods should come up healthy on next rollout)

🤖 Generated with Claude Code

…start workers

The previous guard combined an ESM-style import.meta check with a CJS
fallback:

    if (
      (typeof import.meta !== "undefined" &&
        import.meta.url === pathToFileURL(process.argv[1]).href) ||
      typeof import.meta === "undefined" ||
      (import.meta as any).url === undefined
    ) { startWorker()... }

esbuild compiles each worker to CommonJS (platform: node, format: cjs)
and polyfills `import.meta` as a plain object whose `.url` is
`undefined`. At runtime that makes `import.meta.url === void 0` always
true, so the guard always fires — meaning `require("./forecastWorker")`
unintentionally invokes `startWorker()` and all its connection logic.

Three workers (forecastWorker, repoCacheWorker, testmoImportWorker)
call `process.exit(1)` synchronously when Valkey is unreachable, so the
CI smoke test added in #237 died mid-loop and releases blocked on the
smoke-test step.

Fix: replace with the canonical CJS pattern `require.main === module`.
esbuild preserves `require`/`module` in CJS output, and the smoke test
can now `require()` each worker without triggering startup side effects
— matching the intent stated in the original comment.

Drops the `pathToFileURL` import from each worker (was only used by
the old guard).
@therealbrad therealbrad merged commit 900ad63 into main Apr 23, 2026
5 checks passed
@therealbrad therealbrad deleted the fix/workers-main-guard branch April 23, 2026 20:11
@therealbrad
Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.22.9 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

therealbrad added a commit that referenced this pull request Apr 24, 2026
…t run scheduling (#241)

* fix(workers): use require.main === module guard so require() doesn't start workers

The previous guard combined an ESM-style import.meta check with a CJS
fallback:

    if (
      (typeof import.meta !== "undefined" &&
        import.meta.url === pathToFileURL(process.argv[1]).href) ||
      typeof import.meta === "undefined" ||
      (import.meta as any).url === undefined
    ) { startWorker()... }

esbuild compiles each worker to CommonJS (platform: node, format: cjs)
and polyfills `import.meta` as a plain object whose `.url` is
`undefined`. At runtime that makes `import.meta.url === void 0` always
true, so the guard always fires — meaning `require("./forecastWorker")`
unintentionally invokes `startWorker()` and all its connection logic.

Three workers (forecastWorker, repoCacheWorker, testmoImportWorker)
call `process.exit(1)` synchronously when Valkey is unreachable, so the
CI smoke test added in #237 died mid-loop and releases blocked on the
smoke-test step.

Fix: replace with the canonical CJS pattern `require.main === module`.
esbuild preserves `require`/`module` in CJS output, and the smoke test
can now `require()` each worker without triggering startup side effects
— matching the intent stated in the original comment.

Drops the `pathToFileURL` import from each worker (was only used by
the old guard).

* fix(workers): guard generateFromUrlWorker + stub env in smoke test

v0.22.9 smoke test surfaced two issues hidden behind the main-guard
bug fixed in the previous commit:

1. generateFromUrlWorker.ts had no main guard at all — it called
   startGenerateFromUrlWorker() unconditionally at module scope, so
   require()'ing it in the smoke test attempted to construct a BullMQ
   Worker with no Valkey and crashed. Wrapped in
   `if (require.main === module)` to match the other 14 workers.

2. env.js validates DATABASE_URL / NEXTAUTH_SECRET / NEXTAUTH_URL at
   module-load time via @t3-oss/env-nextjs, so any worker whose
   transitive imports reach env.js (syncWorker,
   elasticsearchReindexWorker, copyMoveWorker, duplicateScanWorker,
   magicSelectWorker) threw during require with zod 'invalid_type'
   errors. Added dummy env shims at the top of smoke-test-workers.js
   using `||=` so real CI-provided values still win. The smoke test
   is verifying module-graph integrity, not runtime config
   correctness.

Together with the main-guard fix, the smoke test should now complete
cleanly for every worker.

* chore(workers): shorten NEXTAUTH_SECRET stub to fit prettier printWidth

* fix(scheduler): add require.main guard so smoke-test require() doesn't run scheduling

scheduler.ts called scheduleJobs() at module top-level, so the v0.22.10
smoke test's require() of dist/scheduler.js tried to connect to Valkey
and exited 1 ('Required queues are not initialized. Cannot schedule
jobs.') even with SKIP_VALKEY_CONNECTION=true (which makes queues no-op
but still marks them unavailable).

Same pattern as the workers fix in #239/#240: gate the runtime code
with `if (require.main === module)`. Production start-workers.sh
invokes scheduler directly via `tsx scheduler.ts`, so require.main IS
module at runtime and scheduleJobs() still runs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant