mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 23:21:30 +00:00
fix(ci): stabilize ui i18n and gateway watch checks
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
@@ -19,6 +20,14 @@ const DEFAULTS = {
|
||||
skipBuild: false,
|
||||
};
|
||||
|
||||
const WATCH_GATEWAY_SKIP_ENV = {
|
||||
OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1",
|
||||
OPENCLAW_SKIP_CANVAS_HOST: "1",
|
||||
OPENCLAW_SKIP_CHANNELS: "1",
|
||||
OPENCLAW_SKIP_CRON: "1",
|
||||
OPENCLAW_SKIP_GMAIL_WATCHER: "1",
|
||||
};
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { ...DEFAULTS };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
@@ -67,6 +76,10 @@ function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function removePathIfExists(targetPath) {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.replaceAll("\\", "/");
|
||||
}
|
||||
@@ -212,15 +225,40 @@ function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function buildTimedWatchCommand(pidFilePath, timeFilePath, isolatedHomeDir) {
|
||||
async function allocateLoopbackPort() {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("Failed to allocate watch regression port")));
|
||||
return;
|
||||
}
|
||||
const { port } = address;
|
||||
server.close((closeErr) => {
|
||||
if (closeErr) {
|
||||
reject(closeErr);
|
||||
return;
|
||||
}
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildTimedWatchCommand(pidFilePath, timeFilePath, isolatedHomeDir, port) {
|
||||
const shellSource = [
|
||||
'echo "$$" > "$OPENCLAW_WATCH_PID_FILE"',
|
||||
"exec node scripts/watch-node.mjs gateway --force --allow-unconfigured",
|
||||
'mkdir -p "$OPENCLAW_HOME/.openclaw"',
|
||||
`printf '%s\n' '{"gateway":{"controlUi":{"enabled":false}}}' > "$OPENCLAW_HOME/.openclaw/openclaw.json"`,
|
||||
`exec node scripts/watch-node.mjs gateway --force --allow-unconfigured --port ${String(port)} --token watch-regression-token`,
|
||||
].join("\n");
|
||||
const env = {
|
||||
OPENCLAW_WATCH_PID_FILE: pidFilePath,
|
||||
HOME: isolatedHomeDir,
|
||||
OPENCLAW_HOME: isolatedHomeDir,
|
||||
...WATCH_GATEWAY_SKIP_ENV,
|
||||
};
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
@@ -274,7 +312,17 @@ async function runTimedWatch(options, outputDir) {
|
||||
fs.writeFileSync(path.join(outputDir, "watch.home.txt"), `${isolatedHomeDir}\n`, "utf8");
|
||||
const stdoutPath = path.join(outputDir, "watch.stdout.log");
|
||||
const stderrPath = path.join(outputDir, "watch.stderr.log");
|
||||
const { command, args, env } = buildTimedWatchCommand(pidFilePath, timeFilePath, isolatedHomeDir);
|
||||
for (const stalePath of [pidFilePath, timeFilePath, stdoutPath, stderrPath]) {
|
||||
removePathIfExists(stalePath);
|
||||
}
|
||||
const port = await allocateLoopbackPort();
|
||||
fs.writeFileSync(path.join(outputDir, "watch.port.txt"), `${String(port)}\n`, "utf8");
|
||||
const { command, args, env } = buildTimedWatchCommand(
|
||||
pidFilePath,
|
||||
timeFilePath,
|
||||
isolatedHomeDir,
|
||||
port,
|
||||
);
|
||||
const child = spawn(command, args, {
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, ...env },
|
||||
@@ -371,6 +419,10 @@ function fail(message) {
|
||||
console.error(`FAIL: ${message}`);
|
||||
}
|
||||
|
||||
function warn(message) {
|
||||
console.error(`WARN: ${message}`);
|
||||
}
|
||||
|
||||
function detectWatchBuildReason(stdout, stderr) {
|
||||
const combined = `${stdout}\n${stderr}`;
|
||||
const match = combined.match(/Building TypeScript \(dist is stale: ([a-z_]+)/);
|
||||
@@ -479,6 +531,7 @@ async function main() {
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
|
||||
const failures = [];
|
||||
const warnings = [];
|
||||
if (watchTriggeredBuild && watchBuildReason === "dirty_watched_tree") {
|
||||
failures.push(
|
||||
"gateway:watch invalid local run: dirty watched source tree forced a rebuild during the watch window",
|
||||
@@ -501,11 +554,15 @@ async function main() {
|
||||
`LOUD ALARM: gateway:watch used ${cpuMs}ms CPU in ${options.windowMs}ms window, above loud-alarm threshold ${options.cpuFailMs}ms`,
|
||||
);
|
||||
} else if (cpuMs > options.cpuWarnMs) {
|
||||
failures.push(
|
||||
warnings.push(
|
||||
`gateway:watch used ${cpuMs}ms CPU in ${options.windowMs}ms window, above target ${options.cpuWarnMs}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const message of warnings) {
|
||||
warn(message);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
for (const message of failures) {
|
||||
fail(message);
|
||||
|
||||
@@ -331,7 +331,7 @@ const runOpenClaw = async (deps) => {
|
||||
|
||||
const syncRuntimeArtifacts = (deps) => {
|
||||
try {
|
||||
runRuntimePostBuild({ cwd: deps.cwd });
|
||||
deps.runRuntimePostBuild({ cwd: deps.cwd });
|
||||
} catch (error) {
|
||||
logRunner(
|
||||
`Failed to write runtime build artifacts: ${error?.message ?? "unknown error"}`,
|
||||
@@ -355,6 +355,8 @@ const writeBuildStamp = (deps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const shouldSkipCleanWatchRuntimeSync = (deps) => deps.env.OPENCLAW_WATCH_MODE === "1";
|
||||
|
||||
export async function runNodeMain(params = {}) {
|
||||
const deps = {
|
||||
spawn: params.spawn ?? spawn,
|
||||
@@ -366,6 +368,7 @@ export async function runNodeMain(params = {}) {
|
||||
cwd: params.cwd ?? process.cwd(),
|
||||
args: params.args ?? process.argv.slice(2),
|
||||
env: params.env ? { ...params.env } : { ...process.env },
|
||||
runRuntimePostBuild: params.runRuntimePostBuild ?? runRuntimePostBuild,
|
||||
};
|
||||
|
||||
deps.distRoot = path.join(deps.cwd, "dist");
|
||||
@@ -379,7 +382,7 @@ export async function runNodeMain(params = {}) {
|
||||
|
||||
const buildRequirement = resolveBuildRequirement(deps);
|
||||
if (!buildRequirement.shouldBuild) {
|
||||
if (!syncRuntimeArtifacts(deps)) {
|
||||
if (!shouldSkipCleanWatchRuntimeSync(deps) && !syncRuntimeArtifacts(deps)) {
|
||||
return 1;
|
||||
}
|
||||
return await runOpenClaw(deps);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -11,6 +10,7 @@ const WATCH_NODE_RUNNER = "scripts/run-node.mjs";
|
||||
const WATCH_RESTART_SIGNAL = "SIGTERM";
|
||||
const WATCH_RESTARTABLE_CHILD_EXIT_CODES = new Set([143]);
|
||||
const WATCH_RESTARTABLE_CHILD_SIGNALS = new Set(["SIGTERM"]);
|
||||
const WATCH_IGNORED_PATH_SEGMENTS = new Set([".git", "dist", "node_modules"]);
|
||||
|
||||
const buildRunnerArgs = (args) => [WATCH_NODE_RUNNER, ...args];
|
||||
|
||||
@@ -27,6 +27,13 @@ const resolveRepoPath = (filePath, cwd) => {
|
||||
return normalizePath(rawPath);
|
||||
};
|
||||
|
||||
const hasIgnoredPathSegment = (repoPath) =>
|
||||
normalizePath(repoPath)
|
||||
.split("/")
|
||||
.some((segment) => WATCH_IGNORED_PATH_SEGMENTS.has(segment));
|
||||
|
||||
const looksLikeDirectoryPath = (repoPath) => path.posix.extname(normalizePath(repoPath)) === "";
|
||||
|
||||
const isDirectoryLikeWatchedPath = (repoPath, watchPaths) => {
|
||||
const normalizedRepoPath = normalizePath(repoPath).replace(/\/$/, "");
|
||||
return watchPaths.some((watchPath) => {
|
||||
@@ -41,17 +48,15 @@ const isDirectoryLikeWatchedPath = (repoPath, watchPaths) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isIgnoredWatchPath = (filePath, cwd, watchPaths) => {
|
||||
const isIgnoredWatchPath = (filePath, cwd, watchPaths, stats) => {
|
||||
const repoPath = resolveRepoPath(filePath, cwd);
|
||||
const statPath = path.isAbsolute(String(filePath ?? ""))
|
||||
? String(filePath ?? "")
|
||||
: path.join(cwd, String(filePath ?? ""));
|
||||
try {
|
||||
if (fs.statSync(statPath).isDirectory() && isDirectoryLikeWatchedPath(repoPath, watchPaths)) {
|
||||
if (hasIgnoredPathSegment(repoPath)) {
|
||||
return true;
|
||||
}
|
||||
if (isDirectoryLikeWatchedPath(repoPath, watchPaths)) {
|
||||
if (stats?.isDirectory?.() || looksLikeDirectoryPath(repoPath)) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to path-based filtering for deleted paths and other transient races.
|
||||
}
|
||||
return !isRestartRelevantRunNodePath(repoPath);
|
||||
};
|
||||
@@ -109,7 +114,8 @@ export async function runWatchMain(params = {}) {
|
||||
|
||||
const watcher = deps.createWatcher(deps.watchPaths, {
|
||||
ignoreInitial: true,
|
||||
ignored: (watchPath) => isIgnoredWatchPath(watchPath, deps.cwd, deps.watchPaths),
|
||||
ignored: (watchPath, stats) =>
|
||||
isIgnoredWatchPath(watchPath, deps.cwd, deps.watchPaths, stats),
|
||||
});
|
||||
|
||||
const settle = (code) => {
|
||||
|
||||
Reference in New Issue
Block a user