Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
630
scripts/run-additional-boundary-checks.mjs
Normal file
630
scripts/run-additional-boundary-checks.mjs
Normal file
@@ -0,0 +1,630 @@
|
||||
#!/usr/bin/env node
|
||||
// Runs additional architecture and boundary checks with sharding, concurrency,
|
||||
// timeout handling, and grouped CI output.
|
||||
import { spawn } from "node:child_process";
|
||||
import { performance } from "node:perf_hooks";
|
||||
|
||||
const DEFAULT_CHECK_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const DEFAULT_OUTPUT_MAX_BYTES = 512 * 1024;
|
||||
const TIMEOUT_KILL_GRACE_MS = 5_000;
|
||||
const PROCESS_GROUP_EXIT_POLL_MS = 25;
|
||||
const POST_FORCE_KILL_WAIT_MS = 1_000;
|
||||
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
|
||||
|
||||
/** Ordered list of supplemental boundary checks used by CI sharding. */
|
||||
export const BOUNDARY_CHECKS = [
|
||||
["prompt:snapshots:check", "pnpm", ["prompt:snapshots:check"]],
|
||||
["plugin-extension-boundary", "pnpm", ["run", "lint:plugins:no-extension-imports"]],
|
||||
["lint:tmp:no-random-messaging", "pnpm", ["run", "lint:tmp:no-random-messaging"]],
|
||||
["lint:tmp:channel-agnostic-boundaries", "pnpm", ["run", "lint:tmp:channel-agnostic-boundaries"]],
|
||||
["lint:tmp:tsgo-core-boundary", "pnpm", ["run", "lint:tmp:tsgo-core-boundary"]],
|
||||
["lint:tmp:no-raw-channel-fetch", "pnpm", ["run", "lint:tmp:no-raw-channel-fetch"]],
|
||||
["lint:tmp:no-raw-http2-imports", "pnpm", ["run", "lint:tmp:no-raw-http2-imports"]],
|
||||
["lint:agent:ingress-owner", "pnpm", ["run", "lint:agent:ingress-owner"]],
|
||||
[
|
||||
"lint:plugins:no-register-http-handler",
|
||||
"pnpm",
|
||||
["run", "lint:plugins:no-register-http-handler"],
|
||||
],
|
||||
[
|
||||
"lint:plugins:no-monolithic-plugin-sdk-entry-imports",
|
||||
"pnpm",
|
||||
["run", "lint:plugins:no-monolithic-plugin-sdk-entry-imports"],
|
||||
],
|
||||
[
|
||||
"lint:plugins:no-extension-src-imports",
|
||||
"pnpm",
|
||||
["run", "lint:plugins:no-extension-src-imports"],
|
||||
],
|
||||
[
|
||||
"lint:plugins:no-extension-test-core-imports",
|
||||
"pnpm",
|
||||
["run", "lint:plugins:no-extension-test-core-imports"],
|
||||
],
|
||||
[
|
||||
"lint:plugins:plugin-sdk-subpaths-exported",
|
||||
"pnpm",
|
||||
["run", "lint:plugins:plugin-sdk-subpaths-exported"],
|
||||
],
|
||||
["deps:root-ownership:check", "pnpm", ["deps:root-ownership:check"]],
|
||||
["web-search-provider-boundary", "pnpm", ["run", "lint:web-search-provider-boundaries"]],
|
||||
["web-fetch-provider-boundary", "pnpm", ["run", "lint:web-fetch-provider-boundaries"]],
|
||||
[
|
||||
"extension-src-outside-plugin-sdk-boundary",
|
||||
"pnpm",
|
||||
["run", "lint:extensions:no-src-outside-plugin-sdk"],
|
||||
],
|
||||
[
|
||||
"extension-plugin-sdk-internal-boundary",
|
||||
"pnpm",
|
||||
["run", "lint:extensions:no-plugin-sdk-internal"],
|
||||
],
|
||||
[
|
||||
"extension-relative-outside-package-boundary",
|
||||
"pnpm",
|
||||
["run", "lint:extensions:no-relative-outside-package"],
|
||||
],
|
||||
[
|
||||
"lint:extensions:telegram-grammy-types",
|
||||
"pnpm",
|
||||
["run", "lint:extensions:telegram-grammy-types"],
|
||||
],
|
||||
["lint:ui:no-raw-window-open", "pnpm", ["lint:ui:no-raw-window-open"]],
|
||||
].map(([label, command, args]) => ({ label, command, args }));
|
||||
|
||||
/**
|
||||
* Resolves the configured boundary-check concurrency.
|
||||
*/
|
||||
export function resolveConcurrency(value, fallback = 4, label = "concurrency") {
|
||||
return resolvePositiveInteger(value, fallback, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses positive integer CLI/env options with a fallback.
|
||||
*/
|
||||
export function resolvePositiveInteger(value, fallback, label = "value") {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return fallback;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
throw new Error(`${label} must be a positive integer; got: ${value}`);
|
||||
}
|
||||
const parsed = Number(text);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
||||
throw new Error(`${label} must be a positive integer; got: ${value}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resolveTimerTimeoutMs(valueMs) {
|
||||
const value = Number(valueMs);
|
||||
if (!Number.isFinite(value)) {
|
||||
return MAX_TIMER_TIMEOUT_MS;
|
||||
}
|
||||
return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one N/TOTAL shard selector into zero-based index form.
|
||||
*/
|
||||
export function parseShardSpec(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const match = String(value).match(/^(\d+)\/(\d+)$/u);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid shard spec '${value}' (expected N/TOTAL)`);
|
||||
}
|
||||
const index = Number(match[1]);
|
||||
const count = Number(match[2]);
|
||||
if (
|
||||
!Number.isSafeInteger(index) ||
|
||||
!Number.isSafeInteger(count) ||
|
||||
index < 1 ||
|
||||
count < 1 ||
|
||||
index > count
|
||||
) {
|
||||
throw new Error(`Invalid shard spec '${value}' (expected 1 <= N <= TOTAL)`);
|
||||
}
|
||||
return { count, index: index - 1, label: `${index}/${count}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list of N/TOTAL shard selectors.
|
||||
*/
|
||||
export function parseShardSelection(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
const shard = parseShardSpec(part);
|
||||
if (!shard) {
|
||||
throw new Error(`Invalid shard spec '${value}'`);
|
||||
}
|
||||
return shard;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects checks whose ordinal belongs to the requested shard set.
|
||||
*/
|
||||
export function selectChecksForShard(checks, shardSpec) {
|
||||
const shards =
|
||||
typeof shardSpec === "string"
|
||||
? parseShardSelection(shardSpec)
|
||||
: Array.isArray(shardSpec)
|
||||
? shardSpec
|
||||
: shardSpec
|
||||
? [shardSpec]
|
||||
: null;
|
||||
if (!shards || shards.length === 0) {
|
||||
return checks;
|
||||
}
|
||||
return checks.filter((_check, index) =>
|
||||
shards.some((shard) => index % shard.count === shard.index),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a check command for CI group output.
|
||||
*/
|
||||
export function formatCommand({ command, args }) {
|
||||
return [command, ...args].join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps only the tail of noisy check output so failure logs stay bounded.
|
||||
*/
|
||||
export function createBoundedOutputBuffer(maxBytes = DEFAULT_OUTPUT_MAX_BYTES) {
|
||||
const limit = Math.max(1, maxBytes);
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
let truncated = false;
|
||||
|
||||
const append = (value) => {
|
||||
const text = String(value);
|
||||
const textBytes = Buffer.byteLength(text);
|
||||
if (textBytes >= limit) {
|
||||
const buffer = Buffer.from(text);
|
||||
const tail = buffer.subarray(buffer.length - limit).toString("utf8");
|
||||
chunks.splice(0, chunks.length, tail);
|
||||
bytes = Buffer.byteLength(tail);
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
chunks.push(text);
|
||||
bytes += textBytes;
|
||||
while (bytes > limit && chunks.length > 0) {
|
||||
const first = chunks[0];
|
||||
const firstBytes = Buffer.byteLength(first);
|
||||
const overflow = bytes - limit;
|
||||
if (firstBytes <= overflow) {
|
||||
chunks.shift();
|
||||
bytes -= firstBytes;
|
||||
truncated = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(first);
|
||||
const tail = buffer.subarray(overflow).toString("utf8");
|
||||
chunks[0] = tail;
|
||||
bytes = chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0);
|
||||
truncated = true;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
append,
|
||||
read() {
|
||||
const output = chunks.join("");
|
||||
return truncated ? `[output truncated to last ${limit} bytes]\n${output}` : output;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function terminateChild(child, signal) {
|
||||
if (process.platform !== "win32" && child.pid) {
|
||||
try {
|
||||
process.kill(-child.pid, signal);
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
child.kill(signal);
|
||||
}
|
||||
|
||||
function processGroupAlive(child) {
|
||||
if (process.platform === "win32" || !child.pid) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
process.kill(-child.pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return error?.code === "EPERM";
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForProcessGroupExit(child, timeoutMs) {
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadlineAt) {
|
||||
if (!processGroupAlive(child)) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolvePoll) => {
|
||||
setTimeout(resolvePoll, PROCESS_GROUP_EXIT_POLL_MS);
|
||||
});
|
||||
}
|
||||
return !processGroupAlive(child);
|
||||
}
|
||||
|
||||
async function finishTerminatedProcessTree(child, timeoutKillGraceMs = TIMEOUT_KILL_GRACE_MS) {
|
||||
if (processGroupAlive(child)) {
|
||||
await waitForProcessGroupExit(child, timeoutKillGraceMs);
|
||||
}
|
||||
if (processGroupAlive(child)) {
|
||||
terminateChild(child, "SIGKILL");
|
||||
await waitForProcessGroupExit(child, POST_FORCE_KILL_WAIT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
function terminateActiveChildren(activeChildren, signal) {
|
||||
for (const child of activeChildren) {
|
||||
terminateChild(child, signal);
|
||||
}
|
||||
}
|
||||
|
||||
function installActiveChildCleanup(activeChildren) {
|
||||
let active = true;
|
||||
let shutdownChildren = [];
|
||||
let shutdownPromise = null;
|
||||
let shutdownForceKillTimer = null;
|
||||
let resolveShutdownForceKill = null;
|
||||
const removeHandlers = () => {
|
||||
for (const [signal, handler] of signalHandlers) {
|
||||
process.off(signal, handler);
|
||||
}
|
||||
process.off("exit", exitHandler);
|
||||
};
|
||||
const forceKillShutdownChildren = () => {
|
||||
if (shutdownForceKillTimer) {
|
||||
clearTimeout(shutdownForceKillTimer);
|
||||
shutdownForceKillTimer = null;
|
||||
}
|
||||
terminateActiveChildren(shutdownChildren, "SIGKILL");
|
||||
resolveShutdownForceKill?.();
|
||||
};
|
||||
const cleanup = (signal, { waitForExit = false } = {}) => {
|
||||
if (!active) {
|
||||
return shutdownPromise ?? Promise.resolve();
|
||||
}
|
||||
active = false;
|
||||
shutdownChildren = [...activeChildren];
|
||||
terminateActiveChildren(shutdownChildren, signal);
|
||||
if (!waitForExit) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
shutdownPromise = new Promise((resolveForceKill) => {
|
||||
resolveShutdownForceKill = resolveForceKill;
|
||||
// Keep this timer ref'ed: once the leader exits, group liveness can look
|
||||
// gone while descendants are still running and still need the force kill.
|
||||
shutdownForceKillTimer = setTimeout(forceKillShutdownChildren, TIMEOUT_KILL_GRACE_MS);
|
||||
})
|
||||
.then(() =>
|
||||
Promise.all(
|
||||
shutdownChildren.map((child) => waitForProcessGroupExit(child, POST_FORCE_KILL_WAIT_MS)),
|
||||
),
|
||||
)
|
||||
.then(() => undefined);
|
||||
return shutdownPromise;
|
||||
};
|
||||
const signalHandlers = new Map();
|
||||
const signals =
|
||||
process.platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
||||
for (const signal of signals) {
|
||||
const handler = () => {
|
||||
if (shutdownPromise) {
|
||||
forceKillShutdownChildren();
|
||||
return;
|
||||
}
|
||||
void cleanup(signal, { waitForExit: true }).finally(() => {
|
||||
removeHandlers();
|
||||
process.kill(process.pid, signal);
|
||||
});
|
||||
};
|
||||
signalHandlers.set(signal, handler);
|
||||
process.on(signal, handler);
|
||||
}
|
||||
const exitHandler = () => {
|
||||
void cleanup("SIGTERM");
|
||||
};
|
||||
process.once("exit", exitHandler);
|
||||
|
||||
return () => {
|
||||
if (shutdownPromise) {
|
||||
return;
|
||||
}
|
||||
active = false;
|
||||
removeHandlers();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one boundary check with timeout and process-group termination.
|
||||
*/
|
||||
export function runSingleCheck(
|
||||
check,
|
||||
{
|
||||
activeChildren,
|
||||
checkTimeoutMs = DEFAULT_CHECK_TIMEOUT_MS,
|
||||
cwd,
|
||||
env,
|
||||
outputMaxBytes = DEFAULT_OUTPUT_MAX_BYTES,
|
||||
},
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
const resolvedCheckTimeoutMs = resolveTimerTimeoutMs(checkTimeoutMs);
|
||||
const startedAt = performance.now();
|
||||
const child = spawn(check.command, check.args, {
|
||||
cwd,
|
||||
env,
|
||||
detached: process.platform !== "win32",
|
||||
shell: false,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
activeChildren?.add(child);
|
||||
const output = createBoundedOutputBuffer(outputMaxBytes);
|
||||
let settled = false;
|
||||
let timedOut = false;
|
||||
let forceKillTimer = null;
|
||||
const finish = (code, signal) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
if (forceKillTimer) {
|
||||
clearTimeout(forceKillTimer);
|
||||
}
|
||||
activeChildren?.delete(child);
|
||||
resolve({
|
||||
check,
|
||||
code: timedOut ? 1 : (code ?? 1),
|
||||
durationMs: Math.round(performance.now() - startedAt),
|
||||
signal,
|
||||
timedOut,
|
||||
output: output.read(),
|
||||
});
|
||||
};
|
||||
const finishAfterTimeoutTeardown = async (code, signal) => {
|
||||
await finishTerminatedProcessTree(child, TIMEOUT_KILL_GRACE_MS);
|
||||
finish(code, signal);
|
||||
};
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
output.append(
|
||||
`\n[boundary-check] ${check.label} timed out after ${formatDuration(resolvedCheckTimeoutMs)}; terminating process group\n`,
|
||||
);
|
||||
terminateChild(child, "SIGTERM");
|
||||
forceKillTimer = setTimeout(() => {
|
||||
output.append(
|
||||
`[boundary-check] ${check.label} still running after ${formatDuration(TIMEOUT_KILL_GRACE_MS)}; sending SIGKILL\n`,
|
||||
);
|
||||
terminateChild(child, "SIGKILL");
|
||||
}, TIMEOUT_KILL_GRACE_MS);
|
||||
forceKillTimer.unref?.();
|
||||
}, resolvedCheckTimeoutMs);
|
||||
timeout.unref?.();
|
||||
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => output.append(chunk));
|
||||
child.stderr.on("data", (chunk) => output.append(chunk));
|
||||
child.on("error", (error) => {
|
||||
output.append(`${error.stack ?? error.message}\n`);
|
||||
finish(1, null);
|
||||
});
|
||||
child.on("close", (code, signal) => {
|
||||
if (timedOut) {
|
||||
void finishAfterTimeoutTeardown(code, signal);
|
||||
return;
|
||||
}
|
||||
finish(code, signal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
if (!Number.isFinite(ms)) {
|
||||
return "";
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
}
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
function writeGroupedResult(result, output) {
|
||||
const success = result.code === 0;
|
||||
output.write(`::group::${result.check.label}\n`);
|
||||
output.write(`$ ${formatCommand(result.check)}\n`);
|
||||
if (result.output) {
|
||||
output.write(result.output.endsWith("\n") ? result.output : `${result.output}\n`);
|
||||
}
|
||||
if (success) {
|
||||
output.write(`[ok] ${result.check.label} in ${formatDuration(result.durationMs)}\n`);
|
||||
} else {
|
||||
const suffix = result.timedOut
|
||||
? " (timeout)"
|
||||
: result.signal
|
||||
? ` (signal ${result.signal})`
|
||||
: ` (exit ${result.code})`;
|
||||
output.write(
|
||||
`::error title=${result.check.label} failed::${result.check.label} failed${suffix} after ${formatDuration(result.durationMs)}\n`,
|
||||
);
|
||||
}
|
||||
output.write("::endgroup::\n");
|
||||
}
|
||||
|
||||
function writeTimingSummary(results, output) {
|
||||
output.write("Additional boundary check timings:\n");
|
||||
for (const result of [...results].toSorted((left, right) => right.durationMs - left.durationMs)) {
|
||||
output.write(
|
||||
`${result.check.label.padEnd(48)} ${formatDuration(result.durationMs).padStart(8)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs boundary checks with bounded concurrency and returns the failure count.
|
||||
*/
|
||||
export async function runChecks(
|
||||
checks = BOUNDARY_CHECKS,
|
||||
{
|
||||
checkTimeoutMs = DEFAULT_CHECK_TIMEOUT_MS,
|
||||
concurrency = 4,
|
||||
cwd = process.cwd(),
|
||||
env = process.env,
|
||||
output = process.stdout,
|
||||
outputMaxBytes = DEFAULT_OUTPUT_MAX_BYTES,
|
||||
} = {},
|
||||
) {
|
||||
const results = Array.from({ length: checks.length });
|
||||
const activeChildren = new Set();
|
||||
const removeActiveChildCleanup = installActiveChildCleanup(activeChildren);
|
||||
let nextIndex = 0;
|
||||
let active = 0;
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => {
|
||||
const launch = () => {
|
||||
if (nextIndex >= checks.length && active === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
while (active < concurrency && nextIndex < checks.length) {
|
||||
const index = nextIndex;
|
||||
const check = checks[nextIndex++];
|
||||
active += 1;
|
||||
void runSingleCheck(check, {
|
||||
activeChildren,
|
||||
checkTimeoutMs,
|
||||
cwd,
|
||||
env,
|
||||
outputMaxBytes,
|
||||
})
|
||||
.then((result) => {
|
||||
results[index] = result;
|
||||
})
|
||||
.finally(() => {
|
||||
active -= 1;
|
||||
launch();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
launch();
|
||||
});
|
||||
} finally {
|
||||
removeActiveChildCleanup();
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
for (const result of results) {
|
||||
writeGroupedResult(result, output);
|
||||
if (result.code !== 0) {
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
writeTimingSummary(results, output);
|
||||
return failures;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return `Usage: node scripts/run-additional-boundary-checks.mjs [--shard <N/TOTAL>[,<N/TOTAL>]]
|
||||
|
||||
Runs supplemental architecture and boundary checks with bounded concurrency.
|
||||
|
||||
Options:
|
||||
--shard <spec> Run only checks selected by one or more N/TOTAL shard specs
|
||||
-h, --help Show this help
|
||||
`;
|
||||
}
|
||||
|
||||
export function parseCliArgs(args, env = process.env) {
|
||||
let shardSpec = env.OPENCLAW_ADDITIONAL_BOUNDARY_SHARD ?? "";
|
||||
let help = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "-h" || arg === "--help") {
|
||||
help = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--shard") {
|
||||
const value = args[index + 1];
|
||||
if (!value || value.startsWith("-")) {
|
||||
throw new Error("--shard requires a value");
|
||||
}
|
||||
shardSpec = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--shard=")) {
|
||||
const value = arg.slice("--shard=".length);
|
||||
if (!value) {
|
||||
throw new Error("--shard requires a value");
|
||||
}
|
||||
shardSpec = value;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
return { help, shardSpec };
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
try {
|
||||
const cliArgs = parseCliArgs(process.argv.slice(2), process.env);
|
||||
if (cliArgs.help) {
|
||||
process.stdout.write(usage());
|
||||
process.exitCode = 0;
|
||||
} else {
|
||||
const concurrencyRaw =
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY ??
|
||||
process.env.OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY;
|
||||
const concurrencyLabel =
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY === undefined
|
||||
? "OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY"
|
||||
: "OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY";
|
||||
const concurrency = resolveConcurrency(concurrencyRaw, 4, concurrencyLabel);
|
||||
const checkTimeoutMs = resolvePositiveInteger(
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS,
|
||||
DEFAULT_CHECK_TIMEOUT_MS,
|
||||
"OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS",
|
||||
);
|
||||
const outputMaxBytes = resolvePositiveInteger(
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_OUTPUT_MAX_BYTES,
|
||||
DEFAULT_OUTPUT_MAX_BYTES,
|
||||
"OPENCLAW_ADDITIONAL_BOUNDARY_OUTPUT_MAX_BYTES",
|
||||
);
|
||||
const shards = parseShardSelection(cliArgs.shardSpec);
|
||||
const checks = selectChecksForShard(BOUNDARY_CHECKS, shards);
|
||||
if (shards) {
|
||||
process.stdout.write(
|
||||
`Running ${checks.length}/${BOUNDARY_CHECKS.length} additional boundary checks (shard ${shards.map((shard) => shard.label).join(",")})\n`,
|
||||
);
|
||||
}
|
||||
const failures = await runChecks(checks, { checkTimeoutMs, concurrency, outputMaxBytes });
|
||||
process.exitCode = failures === 0 ? 0 : 1;
|
||||
}
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${usage()}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user