fix(gateway): inline runtime into bundle to fix image crash-loop#708
Conversation
The gateway bundle imported @fro-bot/runtime as an external specifier that resolved to its src/index.ts entry, which is absent from the runtime image — crash-looping on boot with ERR_MODULE_NOT_FOUND. Inline the runtime into the gateway bundle (matching the action tier), drop the now-dead runtime copy from the image, and add a CI smoke that builds and boots the image to catch packaging regressions. Fixes #707
fro-bot
left a comment
There was a problem hiding this comment.
Verdict: PASS
The fix is correct, minimal, and well-targeted. Inlining @fro-bot/runtime via noExternal in packages/gateway/tsdown.config.ts makes the gateway bundle self-contained, which directly resolves the ERR_MODULE_NOT_FOUND crash-loop: the final image no longer carries packages/runtime/dist, and the bundle no longer emits a bare @fro-bot/runtime specifier that would resolve to the absent src/index.ts. Dropping the now-dead COPY lines from deploy/gateway.Dockerfile is the right follow-through. The new gateway-smoke CI job is a genuine regression guard for the exact packaging failure class that previously shipped undetected.
I verified the bundler change is consistent with the established pattern — the root action-tier tsdown.config.ts already lists @fro-bot/runtime under noExternal, so this brings the gateway tier in line.
Blocking issues
None
Non-blocking concerns
- Bundle-path assertion is hard-coded to
dist/main.mjs. The smoke job grepspackages/gateway/dist/main.mjsand runsnode dist/main.mjs, while the gatewaytsdown.config.tssetsoutDir: 'dist'without a fixed extension. If a future tsdown/format change emitsmain.js(or splits chunks), thegrep -qwould match nothing and silently pass, whileCMD ["node", "dist/main.mjs"]in the Dockerfile would break at runtime. Consider globbing the bundle dir (e.g. assert overpackages/gateway/dist/*.{js,mjs}) so the check can't be bypassed by an extension change, and keeping the DockerfileCMDand the test path derived from one source of truth. - Smoke test couples to two human-readable log strings (
Missing required secret: DISCORD_TOKENandERR_MODULE_NOT_FOUND). The negative assertion onERR_MODULE_NOT_FOUNDis robust. The positive assertion on the exactMissing required secret: DISCORD_TOKENphrasing will break if that config-error message is ever reworded; it's load-bearing for the test but not for behavior. A slightly more durable signal (e.g. a stable exit code for missing-secret, or a dedicated startup marker) would decouple the guard from copy changes. Low priority. gateway-smokerebuilds runtime + gateway from source even thoughneeds: [setup]and a separatebuildjob already compiles the workspace. This is acceptable isolation for a Docker build, just noting the duplicated compile cost.
Missing tests
- The smoke test proves the image doesn't crash on module resolution, but not that inlined runtime code is actually reachable at runtime. Since the failure mode here is specifically "imported symbol resolves to a missing file," reaching the
DISCORD_TOKENconfig check (which executes after module load) is a reasonable proxy and largely covers this. A direct positive assertion that a runtime-provided export executes would be stronger but is not required given the config-load checkpoint. - No assertion that the final image no longer contains
node_modules/.../@fro-bot/runtimeorpackages/runtime/dist. Adding a! test -e ...check would lock in the Dockerfile deletion so a future re-add is caught. Optional hardening.
Risk assessment (LOW/MED/HIGH): LOW
Blast radius is confined to the gateway image build and CI; no runtime API, secret handling, network surface, or auth path is touched. The smoke job boots with no secrets, so it cannot leak anything. The change mirrors the already-proven action-tier bundling pattern and replaces a hard crash-loop with a self-contained bundle, so regression likelihood is low. The only residual exposure is the bundle-path/log-string coupling noted above, which affects detection durability of the new guard rather than the correctness of the fix itself.
Run Summary
| Field | Value |
|---|---|
| Event | pull_request |
| Repository | fro-bot/agent |
| Run ID | 26706460915 |
| Cache | hit |
| Session | ses_18311febfffeST2iFz8lsVHBNg |
Summary
The gateway Docker image crash-looped on boot with
ERR_MODULE_NOT_FOUNDfor@fro-bot/runtime. The gateway bundle imported the runtime as an external bare specifier that resolved to itssrc/index.tsentry — a file absent from the runtime image, which ships onlydist/.Changes
packages/gateway/tsdown.config.ts) vianoExternal, matching how the action tier already bundles the runtime. The gateway bundle is now self-contained.deploy/gateway.Dockerfile— the inlined bundle no longer references@fro-bot/runtimeat runtime.@fro-bot/runtimeimport. This catches the packaging regression class that previously shipped undetected.Verification
@fro-bot/runtimeimports after build.ERR_MODULE_NOT_FOUND.Fixes #707