Files
openclaw/scripts/cloudflare/src/container.ts
T
Peter Steinberger 2c3e537cb8 fix(deploy): repair probes that silently pass and a blueprint that cannot boot (#122963)
Three defects proven by running the shipped configs:

Kubernetes probes checked only the status code against /startupz, but the
pinned image predates that route and the Control UI answers unknown paths
with a catch-all 200. A wedged pod was therefore marked Ready forever.
Probes now assert the JSON probe contract and target routes the pinned
image actually serves; verified in a kind cluster where the old command
exits 0 on the missing route and the new one exits 1.

render.yaml set no dockerCommand, so the image CMD ran without
--allow-unconfigured and a fresh Render disk exited 78 with 'Missing
config' before binding. Reproduced locally with Render's exact env.

The Cloudflare Container readiness poll had the same route mismatch
against operator-supplied official image digests; it now polls /healthz,
which every published image serves.

Also replaces an R2 verification step that could never fail: wrangler
cannot list object keys, so the documented command 404'd into || true.
2026-08-12 20:49:54 -07:00

82 lines
2.6 KiB
TypeScript

import { Container } from "@cloudflare/containers";
interface OpenClawContainerEnv {
ANTHROPIC_API_KEY?: string;
DISCORD_BOT_TOKEN?: string;
LITESTREAM_ACCESS_KEY_ID: string;
LITESTREAM_BUCKET: string;
LITESTREAM_ENDPOINT: string;
LITESTREAM_REGION: string;
LITESTREAM_SECRET_ACCESS_KEY: string;
OPENAI_API_KEY?: string;
OPENCLAW_GATEWAY_TOKEN: string;
OPENCLAW_WEBHOOK_ONLY: string;
SLACK_APP_TOKEN?: string;
SLACK_BOT_TOKEN?: string;
TELEGRAM_BOT_TOKEN?: string;
}
const OPTIONAL_SECRET_NAMES = [
"ANTHROPIC_API_KEY",
"DISCORD_BOT_TOKEN",
"OPENAI_API_KEY",
"SLACK_APP_TOKEN",
"SLACK_BOT_TOKEN",
"TELEGRAM_BOT_TOKEN",
] as const;
function buildContainerEnv(env: OpenClawContainerEnv): Record<string, string> {
const containerEnv: Record<string, string> = {
LITESTREAM_ACCESS_KEY_ID: env.LITESTREAM_ACCESS_KEY_ID,
LITESTREAM_BUCKET: env.LITESTREAM_BUCKET,
LITESTREAM_ENDPOINT: env.LITESTREAM_ENDPOINT,
LITESTREAM_REGION: env.LITESTREAM_REGION,
LITESTREAM_SECRET_ACCESS_KEY: env.LITESTREAM_SECRET_ACCESS_KEY,
OPENCLAW_GATEWAY_TOKEN: env.OPENCLAW_GATEWAY_TOKEN,
};
for (const [name, value] of Object.entries(containerEnv)) {
if (!value) {
throw new Error(`missing required Worker variable or secret: ${name}`);
}
}
for (const name of OPTIONAL_SECRET_NAMES) {
const value = env[name];
if (value) {
containerEnv[name] = value;
}
}
return containerEnv;
}
export class OpenClawContainer extends Container<OpenClawContainerEnv> {
override defaultPort = 8080;
// /healthz exists in every published OpenClaw image and answers as soon as the
// Gateway's listener is up, which is exactly what this readiness poll asks.
// Do not point this at a route the pinned image may not serve: the Control UI
// answers unknown paths with a catch-all 200, so a missing route would look
// permanently healthy instead of failing. /startupz additionally waits for
// startup work to finish and is the better signal once the derived image comes
// from a release that serves it.
override pingEndpoint = "localhost/healthz";
override sleepAfter = "10m";
private readonly webhookOnly: boolean;
constructor(ctx: unknown, env: OpenClawContainerEnv) {
super(ctx, env);
this.envVars = buildContainerEnv(env);
this.webhookOnly = env.OPENCLAW_WEBHOOK_ONLY === "true";
}
override async onActivityExpired(): Promise<void> {
// Socket channels need a continuously running process. Only an explicitly
// webhook-only installation may let the Container helper stop the instance.
if (this.webhookOnly) {
await super.onActivityExpired();
}
}
}