diff --git a/components/gateway-image.ts b/components/gateway-image.ts index a5f9947..89689a8 100644 --- a/components/gateway-image.ts +++ b/components/gateway-image.ts @@ -168,8 +168,8 @@ export class GatewayImage extends pulumi.ComponentResource { ); // The resource that downstream depends on — either a single Image or an Index. - // imageDigestTrigger is the registry manifest digest — changes on every push, - // used to force RemoteImage replacement (triggers, not pullTriggers). + // imageDigestTrigger is the build output digest — changes on every rebuild, + // used alongside the registry manifest digest in RemoteImage pullTriggers. let image: pulumi.Resource; let imageDigestTrigger: pulumi.Output; @@ -339,17 +339,19 @@ export class GatewayImage extends pulumi.ComponentResource { return match.sha256Digest; }); - // pullTriggers with registry digest as source of truth. When the digest - // changes, Pulumi replaces the resource (destroy + create). forceRemove - // ensures the destroy step removes the image even when running containers - // reference it — without this, findImage() finds the stale local tag and - // skips the pull entirely (kreuzwerker/terraform-provider-docker behavior). + // Two triggers ensure the pull always fires: + // 1. pullDigest (registry manifest) — catches out-of-band pushes and + // state desync (registry is the source of truth) + // 2. imageDigestTrigger (build output) — catches same-deploy rebuilds + // where the image hasn't been pushed to the registry yet at plan time + // forceRemove ensures destroy removes the image even when running + // containers reference it (findImage() short-circuit workaround). const pulledImage = new docker.RemoteImage( `${name}-pull`, { name: pullTag, platform: args.platform, - pullTriggers: [pullDigest], + pullTriggers: [pullDigest, imageDigestTrigger], forceRemove: true, }, { diff --git a/templates/dockerfile.ts b/templates/dockerfile.ts index 4f07677..949a1c3 100644 --- a/templates/dockerfile.ts +++ b/templates/dockerfile.ts @@ -149,9 +149,29 @@ RUN mkdir -p "\${OPENCLAW_CONFIG_DIR}" "\${OPENCLAW_WORKSPACE_DIR}" && \\ RUN SHARP_IGNORE_GLOBAL_LIBVIPS=1 NODE_OPTIONS=--max-old-space-size=2048 \\ npm install -g --no-fund --no-audit "openclaw@\${OPENCLAW_VERSION}" -# CLI symlink for consistent access across users -RUN ln -sf "$(npm root -g)/openclaw/dist/entry.js" /usr/local/bin/openclaw && \\ - chmod 755 "$(npm root -g)/openclaw/dist/entry.js" +# WORKAROUND: Route CLI commands through Bun instead of Node. +# +# OpenClaw's bundled JS (~69MB) takes 7+ seconds to parse on Skylake Xeon CPUs +# under Node's V8 engine. The gateway enforces a 3s handshake timeout on WS +# connections (DEFAULT_HANDSHAKE_TIMEOUT_MS in server-constants.ts). CLI commands +# open a WebSocket to the gateway, but V8's synchronous startup blocks the event +# loop so the CLI can't respond to the WS challenge before the 3s timeout expires. +# +# Bun's JavaScriptCore engine parses the same bundles in ~150ms, avoiding the issue. +# Both the gateway server and CLI invocations run through this Bun wrapper (the +# Dockerfile CMD resolves to /usr/local/bin/openclaw, which execs bun). This is +# acceptable — Bun's Node.js compatibility covers OpenClaw's runtime needs. +# +# Upstream refs: +# https://github.com/openclaw/openclaw/issues/45560 +# https://github.com/openclaw/openclaw/issues/45940 +# +# Remove this workaround when upstream bumps the handshake timeout or optimizes +# CLI startup. +RUN OPENCLAW_ENTRY="$(npm root -g)/openclaw/dist/entry.js" && \\ + echo '#!/bin/sh' > /usr/local/bin/openclaw && \\ + echo "exec bun \\"$OPENCLAW_ENTRY\\" \\"\\$@\\"" >> /usr/local/bin/openclaw && \\ + chmod 755 /usr/local/bin/openclaw # Optional: install Chromium + Xvfb for browser automation. ${renderBrowserBlock(opts.installBrowser ?? false)} diff --git a/tests/templates.test.ts b/tests/templates.test.ts index b96dbc5..64fc98c 100644 --- a/tests/templates.test.ts +++ b/tests/templates.test.ts @@ -189,10 +189,10 @@ describe("renderDockerfile", () => { expect(df).toContain("chmod 700 /usr/local/bin/firewall-bypass"); }); - it("creates CLI symlink", () => { + it("creates CLI wrapper", () => { const df = renderDockerfile(defaultOpts); - expect(df).toContain("ln -sf"); expect(df).toContain("/usr/local/bin/openclaw"); + expect(df).toContain("exec bun"); }); it("does not install Tailscale CLI (handled by sidecar container)", () => {