Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

355 lines
12 KiB
JavaScript

#!/usr/bin/env node
// Drives cloud admin cloud admin dev cloud api dev automation with explicit environment and CI invariants.
import { spawn, spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { createConnection } from "node:net";
import path from "node:path";
import process from "node:process";
const repoRoot = path.resolve(import.meta.dirname, "../../../../..");
const cloudApiDir = path.join(repoRoot, "packages", "cloud", "api");
const require = createRequire(import.meta.url);
const rawArgs = process.argv.slice(2);
const withControlPlane = rawArgs.includes("--with-control-plane");
const args = rawArgs.filter((a) => a !== "--with-control-plane");
const host = process.env.PGLITE_HOST || "127.0.0.1";
const port = Number.parseInt(
process.env.DEV_CLOUD_PGLITE_PORT || process.env.PGLITE_PORT || "55432",
10,
);
const apiPort = process.env.API_DEV_PORT || "8787";
const maxConnections = process.env.PGLITE_MAX_CONNECTIONS || "16";
const startupTimeoutMs = Number.parseInt(
process.env.DEV_CLOUD_STARTUP_TIMEOUT_MS || "120000",
10,
);
const pollIntervalMs = 500;
function bunExecutable() {
if (process.env.BUN && existsSync(process.env.BUN)) return process.env.BUN;
// On Windows, Node can spawn `bun.exe` directly but NOT the extensionless npm
// shim (`spawn ENOENT`) nor a `.cmd` without `shell: true`. Probe the native
// `.exe` first so the npm shim never wins. On POSIX the binary is just `bun`.
const names = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
const home = process.env.HOME || process.env.USERPROFILE || "";
const dirs = [
path.resolve(home, ".bun/bin"),
...(process.env.PATH?.split(path.delimiter) ?? []),
];
for (const dir of dirs) {
for (const name of names) {
const candidate = path.resolve(dir, name);
if (existsSync(candidate)) return candidate;
}
}
if (process.env.npm_execpath?.includes("bun"))
return process.env.npm_execpath;
return process.platform === "win32" ? "bun.exe" : "bun";
}
function isRealNodeExecutable(candidate) {
if (!candidate || !existsSync(candidate)) return false;
const result = spawnSync(
candidate,
["-e", "process.exit(process.versions.bun ? 1 : 0)"],
{ stdio: "ignore" },
);
return result.status === 0;
}
function nodeExecutable() {
const candidates = [
process.env.NODE,
process.execPath,
...(process.env.PATH?.split(path.delimiter).map((entry) =>
path.resolve(entry, "node"),
) ?? []),
"/opt/homebrew/bin/node",
"/usr/local/bin/node",
"/usr/bin/node",
];
const seen = new Set();
for (const candidate of candidates) {
if (!candidate || seen.has(candidate)) continue;
seen.add(candidate);
if (isRealNodeExecutable(candidate)) return candidate;
}
return "node";
}
function wranglerScript() {
// wrangler's package.json `exports` does not expose `bin/wrangler.js` as a
// subpath, so require.resolve("wrangler/bin/wrangler.js") throws
// ERR_PACKAGE_PATH_NOT_EXPORTED on wrangler >=4. Resolve the package via its
// (exported) package.json and read the declared bin path instead.
const pkgJsonPath = require.resolve("wrangler/package.json", {
paths: [cloudApiDir, repoRoot],
});
const pkg = require(pkgJsonPath);
const binRel =
typeof pkg.bin === "string"
? pkg.bin
: (pkg.bin?.wrangler ?? "bin/wrangler.js");
return path.join(path.dirname(pkgJsonPath), binRel);
}
function parsePGliteDataDir(url) {
if (!url?.startsWith("pglite://")) return null;
const dataDir = url.slice("pglite://".length);
if (!dataDir || dataDir === "memory") return null;
return dataDir;
}
function shouldUsePGliteTcpBridge(env) {
const url = env.DATABASE_URL || env.TEST_DATABASE_URL || "";
return !url || url.startsWith("pglite://");
}
async function tcpOk() {
return new Promise((resolve) => {
const socket = createConnection({ host, port });
socket.setTimeout(1000);
socket.once("connect", () => {
socket.end();
resolve(true);
});
socket.once("timeout", () => {
socket.destroy();
resolve(false);
});
socket.once("error", () => {
socket.destroy();
resolve(false);
});
});
}
async function waitForTcp(child) {
const startedAt = Date.now();
while (Date.now() - startedAt < startupTimeoutMs) {
if (await tcpOk()) return;
if (child.exitCode !== null) {
throw new Error(`PGlite TCP server exited with code ${child.exitCode}`);
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
throw new Error(
`PGlite TCP server did not become reachable at ${host}:${port}`,
);
}
function runStep(label, command, stepArgs, env) {
const result = spawnSync(command, stepArgs, {
cwd: repoRoot,
env,
stdio: "inherit",
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${label} exited with code ${result.status}`);
}
}
async function main() {
const bun = bunExecutable();
let pgliteChild = null;
const env = {
...process.env,
NODE_ENV: process.env.NODE_ENV || "development",
API_DEV_PORT: apiPort,
};
if (shouldUsePGliteTcpBridge(env)) {
const configuredUrl = env.DATABASE_URL || env.TEST_DATABASE_URL || "";
const dataDir =
parsePGliteDataDir(configuredUrl) ||
env.DEV_CLOUD_PGLITE_DATA_DIR ||
env.PGLITE_DATA_DIR ||
".eliza/.pgdata";
env.DATABASE_URL = `postgresql://postgres@${host}:${port}/postgres`;
env.TEST_DATABASE_URL ||= env.DATABASE_URL;
if (!(await tcpOk())) {
pgliteChild = spawn(
bun,
["run", "packages/scripts/cloud/admin/dev/pglite-server.ts"],
{
cwd: repoRoot,
env: {
...env,
PGLITE_HOST: host,
PGLITE_PORT: String(port),
PGLITE_MAX_CONNECTIONS: maxConnections,
PGLITE_DATA_DIR: dataDir,
},
stdio: ["ignore", "inherit", "inherit"],
},
);
await waitForTcp(pgliteChild);
}
}
if (env.DEV_CLOUD_SKIP_MIGRATE !== "1") {
runStep("db:cloud:migrate", bun, ["run", "db:cloud:migrate"], env);
}
runStep(
"sync-api-dev-vars",
bun,
["run", "packages/scripts/cloud/admin/sync-api-dev-vars.ts"],
env,
);
// When the e2e harness runs (NODE_ENV=test), forward the test KMS backend via
// `--var`. wrangler's `[vars] NODE_ENV = "production"` block in wrangler.toml
// takes precedence over the shell NODE_ENV, which would otherwise cause
// `resolveKmsBackend()` in `@elizaos/security/kms` to default to the Steward
// backend and throw `KmsError("ELIZA_KMS_BACKEND=steward requires
// steward.{baseUrl, tokenProvider}")` for any route that touches encrypted
// fields. Cross-process e2e flows use the local backend with a deterministic
// root key so Worker routes can decrypt rows written by the control plane.
const isE2eTestMode =
process.env.NODE_ENV === "test" || process.env.CLOUD_E2E === "1";
const kmsBackend = process.env.ELIZA_KMS_BACKEND ?? "memory";
const localRootKey = process.env.ELIZA_LOCAL_ROOT_KEY;
// In e2e/test mode, also stub the Cloudflare registrar/DNS by default so the
// domain buy/check routes never hit the real Cloudflare API (overridable via
// ELIZA_CF_REGISTRAR_DEV_STUB).
const registrarStub = process.env.ELIZA_CF_REGISTRAR_DEV_STUB ?? "1";
// In e2e/test mode, neutralize the dedicated-agent public subdomain. The agent
// detail route synthesizes `web_ui_url = https://<id>.<ELIZA_CLOUD_AGENT_BASE_DOMAIN>`
// (wrangler.toml pins `elizacloud.ai`), and the client's readiness probe prefers
// that web_ui_url over the agent's `bridge_url`. There is no Worker-fronted
// `*.elizacloud.ai` ingress in the local mock stack, so that subdomain is
// unreachable — the dedicated agent's reachable base IS its `bridge_url` (the
// control-plane mock). The detail route feeds `containersEnv.publicBaseDomain()`
// into `getElizaAgentPublicWebUiUrl` as the EXPLICIT `{baseDomain}` option; a
// value that normalizes to empty makes that explicit-option path return null, so
// the reachable bridge wins and the shared→dedicated handoff import can complete.
// (The compat-envelope no-option path keeps its default and is intentionally not
// neutralized.) The apps domain (`CONTAINERS_PUBLIC_BASE_DOMAIN`) is a separate
// knob and is left untouched.
const agentBaseDomainOverride =
process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN ?? "https://";
const testModeVars = isE2eTestMode
? [
"--var",
"NODE_ENV:test",
"--var",
`ELIZA_KMS_BACKEND:${kmsBackend}`,
...(localRootKey
? ["--var", `ELIZA_LOCAL_ROOT_KEY:${localRootKey}`]
: []),
"--var",
`ELIZA_CF_REGISTRAR_DEV_STUB:${registrarStub}`,
"--var",
`ELIZA_CLOUD_AGENT_BASE_DOMAIN:${agentBaseDomainOverride}`,
]
: [];
// Redis selection must reach the Worker's `c.env`, not just this launcher's
// process.env: routes build their client via `buildRedisClient(c.env)`, and
// wrangler `--local` only exposes vars passed through wrangler.toml/.dev.vars/
// `--var` — ambient process.env does NOT leak into `c.env`. Without this, the
// e2e env's `MOCK_REDIS=1` is invisible inside the Worker and any nonce-storage
// route (e.g. SIWE nonce/verify) returns 503 "Nonce storage unavailable". Most
// redis consumers (rate-limit) fail open, so this only surfaced once a spec
// drove the real SIWE login. Forward whichever redis selector is set.
const redisDevVars = ["MOCK_REDIS", "REDIS_URL"].flatMap((key) => {
const value = process.env[key];
return value ? ["--var", `${key}:${value}`] : [];
});
const appDeployDevVars = [
"APPS_DEPLOY_ENABLED",
"APPS_DEPLOY_ALLOWED_ORG_IDS",
"APP_DEFAULT_IMAGE",
].flatMap((key) => {
const value = process.env[key];
return value ? ["--var", `${key}:${value}`] : [];
});
const wranglerArgs =
args.length > 0
? args
: [
"dev",
"--ip",
"127.0.0.1",
"--port",
apiPort,
"--local",
...testModeVars,
...redisDevVars,
...appDeployDevVars,
];
const useNodeWrangler = env.CLOUD_E2E === "1" && env.NODE_ENV === "test";
const wranglerCmd = useNodeWrangler ? nodeExecutable() : bun;
const wranglerSpawnArgs = useNodeWrangler
? [wranglerScript(), ...wranglerArgs]
: ["run", "wrangler", ...wranglerArgs];
const wrangler = spawn(wranglerCmd, wranglerSpawnArgs, {
cwd: cloudApiDir,
env,
stdio: "inherit",
});
// When --with-control-plane is passed, also boot the container-control-plane
// bun service on :8791 so the cloud-api can forward provisioning jobs to it
// (otherwise provision endpoints succeed but jobs queue forever).
let controlPlane = null;
if (withControlPlane) {
const controlPlaneEnv = {
...env,
// Control-plane reads DATABASE_URL directly (not through dev-vars).
DATABASE_URL:
env.DATABASE_URL || `postgresql://postgres@${host}:${port}/postgres`,
ELIZA_LOCAL_DOCKER_PROVIDER: env.ELIZA_LOCAL_DOCKER_PROVIDER || "1",
ENVIRONMENT: env.ENVIRONMENT || "local",
ELIZA_AGENT_IMAGE: env.ELIZA_AGENT_IMAGE || "eliza-cloud-agent:local",
ELIZA_AGENT_PORT: env.ELIZA_AGENT_PORT || "2138",
ELIZA_AGENT_BRIDGE_PORT: env.ELIZA_AGENT_BRIDGE_PORT || "18790",
NEXT_PUBLIC_API_URL:
env.NEXT_PUBLIC_API_URL || `http://127.0.0.1:${apiPort}`,
};
console.log("[cloud-api-dev] starting container-control-plane on :8791");
controlPlane = spawn(bun, ["run", "start"], {
cwd: path.join(
repoRoot,
"packages",
"cloud-services",
"container-control-plane",
),
env: controlPlaneEnv,
stdio: "inherit",
});
controlPlane.on("exit", (code) => {
console.warn(`[cloud-api-dev] control-plane exited (code ${code})`);
});
}
const shutdown = () => {
wrangler.kill("SIGTERM");
controlPlane?.kill("SIGTERM");
pgliteChild?.kill("SIGTERM");
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
wrangler.on("exit", (code, signal) => {
pgliteChild?.kill("SIGTERM");
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
}
main().catch((error) => {
console.error(error);
process.exit(1);
});