mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(gateway): prevent control-plane polling stalls (#124891)
* fix(gateway): avoid repeated control-plane scans * fix(tooling): allow concurrent worktree validation * fix(ci): refresh protocol and runner inputs * perf(ui): defer hidden session refreshes * fix(ui): resolve session refresh lint failure * fix(update): preserve pre-cache update channel * fix(update): normalize cached update channel * fix(gateway): lifecycle-cache update install identity * fix(ui): preserve manual history retry after layout scroll * test(codex): repair side-question tool schema fixture
This commit is contained in:
committed by
GitHub
parent
d24cb13320
commit
bf9b25ab7e
@@ -11,17 +11,9 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
|
||||
- For changed-file verification, prefer `scripts/check-changed.mjs` and keep lane classification in `scripts/changed-lanes.mjs`. Use `node scripts/check-changed.mjs --dry-run [--staged|-- <files...>]` to inspect the plan before running anything expensive. Do not copy path-scope rules into new hooks or ad hoc CI snippets.
|
||||
- For one/few lint files, prefer direct `node scripts/run-oxlint.mjs --tsconfig <matching config> <files...>` over sharded `pnpm lint`; `check-changed.mjs` owns this targeting for core, extension, and script diffs.
|
||||
|
||||
## Local Heavy-Check Lock
|
||||
|
||||
- Respect the local heavy-check lock behavior in `scripts/lib/local-heavy-check-runtime.mts`.
|
||||
- Do not bypass that lock for real heavy commands just to make a local loop look faster.
|
||||
- Metadata-only or explicitly narrow commands may skip the lock when the existing helper logic says that is safe.
|
||||
- If you change the lock heuristics, add or update the narrow tests under `test/scripts/`.
|
||||
|
||||
## PR Prepare Gates
|
||||
|
||||
- `scripts/pr` serializes review, prepare, and merge operations per PR across linked worktrees; `scripts/pr gc` skips active or indeterminate locks. Its subcommand classification table is the canonical wrapper trust boundary: a mismatched local wrapper may run only a classified `advisory` subcommand with `--dev-wrapper` or `OPENCLAW_PR_DEV_WRAPPER=1`; classified `landing` subcommands always require canonical/origin-main wrapper code. A worktree whose wrapper differs from origin/main (stale base or wrapper-editing branch) loudly substitutes the canonical checkout's wrapper when that checkout is clean and byte-identical to fetched `refs/remotes/origin/main`; it refuses only when no anchor-matching wrapper is available. A successful command return is the trusted synchronous-completion contract: every PR-state-mutating child must be joined before returning, and such work must never daemonize or explicitly escape both the operation group and lock-notification FD. Release on clean exit requires the leader's completion marker; an escaped descendant that merely holds the notify pipe then produces a loud warned release instead of retention (#124583), while all failure shapes still retain. A failed command auto-releases only while its explicit pre-side-effect validation marker remains active; failures after mutation/tool launch, interruptions, and controller loss stay locked because detached children cannot be disproved. After verifying no child tools remain, use the reported exact-OID `scripts/pr lock-recover` command. Never bypass or delete these refs manually.
|
||||
- `scripts/pr prepare-gates` holds the heavy-check lock for its whole local gate block (`scripts/pr-gates-lock.mts`), so concurrent gate runs across `.worktrees` queue as units instead of dying on child lock timeouts or vitest no-output watchdog kills.
|
||||
- `OPENCLAW_PR_GATES_REMOTE=testbox` runs the full-suite `pnpm test` gate on a Blacksmith Testbox through `scripts/crabbox-wrapper.mjs` (same delegation as `check:changed`); `pnpm build`/`pnpm check` stay local. The `tbx_` lease id and Actions run URL land in `.local/gates.env` (`REMOTE_GATES_*`) and `.local/prep.md`. Use it for reviewed trusted code when a loaded host makes the local 88-shard run stall-kill; contributor/fork code stays on secretless CI or sanitized AWS unless a maintainer explicitly approves credentialed execution.
|
||||
|
||||
## Generated Outputs
|
||||
|
||||
+20
-35
@@ -30,11 +30,7 @@ import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-fa
|
||||
import { printTimingSummary } from "./lib/check-timing-summary.mts";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
import { runWithFailedTrailer } from "./lib/failed-trailer.mts";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
withLocalHeavyCheckLockHeld,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
import { resolveLocalCheckEnv } from "./lib/local-check-runtime.mts";
|
||||
import { runManagedCommand } from "./lib/managed-child-process.mts";
|
||||
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mts";
|
||||
import { createSparseTsgoSkipEnv } from "./lib/tsgo-sparse-guard.mts";
|
||||
@@ -156,8 +152,8 @@ if (!isDirectRun()) {
|
||||
await ensureChangedCheckRuntimeDependencies(["package.json"]);
|
||||
}
|
||||
|
||||
export function createChangedCheckChildEnv(baseEnv: NodeJS.ProcessEnv = process.env) {
|
||||
return withLocalHeavyCheckLockHeld(resolveLocalHeavyCheckEnv(baseEnv));
|
||||
function createChangedCheckChildEnv(baseEnv: NodeJS.ProcessEnv = process.env) {
|
||||
return resolveLocalCheckEnv(baseEnv);
|
||||
}
|
||||
|
||||
function hasAndroidVersionSyncPath(paths: string[]) {
|
||||
@@ -1047,41 +1043,30 @@ async function runChangedCheck(result: ChangedLaneResult, options: ChangedCheckR
|
||||
return 0;
|
||||
}
|
||||
await ensureChangedCheckRuntimeDependencies(result.paths);
|
||||
const baseEnv = resolveLocalHeavyCheckEnv(options.env ?? process.env);
|
||||
const baseEnv = resolveLocalCheckEnv(options.env ?? process.env);
|
||||
const childEnv = createChangedCheckChildEnv(baseEnv);
|
||||
const plan = createChangedCheckPlan(result, {
|
||||
...options,
|
||||
env: childEnv,
|
||||
});
|
||||
const releaseLock = options.dryRun
|
||||
? () => {}
|
||||
: acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env: baseEnv,
|
||||
toolName: "check:changed",
|
||||
});
|
||||
|
||||
try {
|
||||
printPlan(result, plan, options);
|
||||
printPlan(result, plan, options);
|
||||
|
||||
if (options.dryRun) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const timings: ChangedCheckTiming[] = [];
|
||||
for (const command of plan.commands) {
|
||||
const status = await runPlanCommand(command, timings);
|
||||
if (status !== 0) {
|
||||
printSummary(timings, options);
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
printSummary(timings, options);
|
||||
if (options.dryRun) {
|
||||
return 0;
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
|
||||
const timings: ChangedCheckTiming[] = [];
|
||||
for (const command of plan.commands) {
|
||||
const status = await runPlanCommand(command, timings);
|
||||
if (status !== 0) {
|
||||
printSummary(timings, options);
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
printSummary(timings, options);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sameArgs(left: string[], right: string[]) {
|
||||
@@ -1132,7 +1117,7 @@ export function createPnpmManagedCommand<T extends ChangedCheckCommand>(
|
||||
command: T,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
) {
|
||||
const commandEnv = command.env ?? resolveLocalHeavyCheckEnv(env);
|
||||
const commandEnv = command.env ?? resolveLocalCheckEnv(env);
|
||||
if (isOpenEndedTruthyValue(commandEnv.CI) || isOpenEndedTruthyValue(commandEnv.GITHUB_ACTIONS)) {
|
||||
const shimmedEnv = prependCorepackPnpmShim(commandEnv);
|
||||
return {
|
||||
@@ -1195,7 +1180,7 @@ async function runCommand(
|
||||
status = await runManagedCommand({
|
||||
bin: command.bin,
|
||||
args: command.args,
|
||||
env: command.env ?? resolveLocalHeavyCheckEnv(),
|
||||
env: command.env ?? resolveLocalCheckEnv(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mts";
|
||||
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
|
||||
import { runManagedCommand } from "./lib/managed-child-process.mts";
|
||||
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// Enforces core tsgo project boundaries and sparse-checkout safety.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mts";
|
||||
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
|
||||
import { createManagedCommandInvocation } from "./lib/managed-child-process.mts";
|
||||
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
||||
import {
|
||||
|
||||
@@ -19,10 +19,6 @@ import type { Readable } from "node:stream";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
withLocalHeavyCheckLockHeld,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
|
||||
// Two concurrent plans halve the serial tail of packed jobs. Children run with
|
||||
// inner test-projects parallelism 1 so a job never exceeds two Vitest runs;
|
||||
@@ -146,9 +142,7 @@ export function buildChildEnv(
|
||||
delete childEnv.OPENCLAW_VITEST_INCLUDE_FILE;
|
||||
}
|
||||
}
|
||||
// This wrapper owns the shared lock, so every nested heavy-check wrapper must
|
||||
// inherit that ownership rather than queueing behind its own parent.
|
||||
return withLocalHeavyCheckLockHeld(childEnv);
|
||||
return childEnv;
|
||||
}
|
||||
|
||||
export function pruneFsModuleCache(root: string, maxBytes = FS_MODULE_CACHE_MAX_BYTES) {
|
||||
@@ -392,14 +386,5 @@ if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
||||
// Bins holding spawn/signal-timing suites are marked planConcurrency 1 by
|
||||
// the planner; overlapping them with a sibling Vitest run causes flakes.
|
||||
const planConcurrency = Number(process.env.OPENCLAW_NODE_TEST_PLAN_CONCURRENCY) || undefined;
|
||||
const releaseLock = acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
toolName: "test",
|
||||
});
|
||||
try {
|
||||
process.exitCode = await runShardPlans(plans, { concurrency: planConcurrency });
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
process.exitCode = await runShardPlans(plans, { concurrency: planConcurrency });
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolveRepoToolBinPath } from "./lib/local-heavy-check-runtime.mts";
|
||||
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
|
||||
import { repairMintlifyAccordionIndentation } from "./lib/mintlify-accordion.mjs";
|
||||
import { outputTail } from "./lib/output-tail.mts";
|
||||
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const LOCK_POLL_MS = 500;
|
||||
const LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const OWNER_WRITE_GRACE_MS = 30 * 1000;
|
||||
const SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
||||
|
||||
type LockOwner = { pid?: unknown };
|
||||
|
||||
function isProcessAlive(pid: unknown) {
|
||||
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
|
||||
}
|
||||
}
|
||||
|
||||
function shouldReclaimLock(lockDir: string, ownerPath: string) {
|
||||
try {
|
||||
const owner = JSON.parse(fs.readFileSync(ownerPath, "utf8")) as LockOwner;
|
||||
return !isProcessAlive(owner.pid);
|
||||
} catch {
|
||||
try {
|
||||
return Date.now() - fs.statSync(lockDir).mtimeMs >= OWNER_WRITE_GRACE_MS;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function acquireExtensionPackageBoundaryArtifactLockSync(rootDir: string) {
|
||||
// The generated declarations live in this checkout. Keeping ownership beside
|
||||
// them prevents same-worktree writers without coupling independent worktrees.
|
||||
const lockDir = path.join(rootDir, "dist", ".extension-package-boundary-artifacts.lock");
|
||||
const ownerPath = path.join(lockDir, "owner.json");
|
||||
const startedAt = Date.now();
|
||||
let reportedWait = false;
|
||||
|
||||
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
|
||||
for (;;) {
|
||||
try {
|
||||
fs.mkdirSync(lockDir);
|
||||
fs.writeFileSync(
|
||||
ownerPath,
|
||||
`${JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
let released = false;
|
||||
const release = () => {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
released = true;
|
||||
fs.rmSync(lockDir, { force: true, recursive: true });
|
||||
};
|
||||
process.once("exit", release);
|
||||
return () => {
|
||||
process.off("exit", release);
|
||||
release();
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) {
|
||||
throw error;
|
||||
}
|
||||
if (shouldReclaimLock(lockDir, ownerPath)) {
|
||||
fs.rmSync(lockDir, { force: true, recursive: true });
|
||||
continue;
|
||||
}
|
||||
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
|
||||
throw new Error(`timed out waiting for plugin package-boundary artifacts in ${rootDir}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
if (!reportedWait) {
|
||||
console.error("[plugin package-boundary artifacts] waiting for the current writer...");
|
||||
reportedWait = true;
|
||||
}
|
||||
Atomics.wait(SLEEP_BUFFER, 0, 0, LOCK_POLL_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
// Applies local resource policy for expensive check commands.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const GIB = 1024 ** 3;
|
||||
const DEFAULT_LOCAL_GO_GC = "30";
|
||||
const DEFAULT_LOCAL_GO_MAX_PROCS = 2;
|
||||
const DEFAULT_LOCAL_GO_MEMORY_LIMIT = "3GiB";
|
||||
const DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE = ".artifacts/tsgo-cache/root.tsbuildinfo";
|
||||
const DEFAULT_FAST_LOCAL_CHECK_MIN_MEMORY_BYTES = 48 * GIB;
|
||||
const DEFAULT_FAST_LOCAL_CHECK_MIN_CPUS = 12;
|
||||
|
||||
type Env = NodeJS.ProcessEnv;
|
||||
type Resources = {
|
||||
logicalCpuCount: number;
|
||||
totalMemoryBytes: number;
|
||||
};
|
||||
|
||||
type LocalCheckMode = "auto" | "full" | "throttled";
|
||||
type RepoToolOptions = {
|
||||
cwd?: string;
|
||||
fileExists?: (candidate: string) => boolean;
|
||||
resolveCommonDir?: (cwd: string) => string | null;
|
||||
};
|
||||
|
||||
/** Return whether local check safeguards are enabled for an environment. */
|
||||
export function isLocalCheckEnabled(env: Env) {
|
||||
const raw = env.OPENCLAW_LOCAL_CHECK?.trim().toLowerCase();
|
||||
return raw !== "0" && raw !== "false";
|
||||
}
|
||||
|
||||
function isCiLikeEnv(env: Env = process.env) {
|
||||
return env.CI === "true" || env.GITHUB_ACTIONS === "true";
|
||||
}
|
||||
|
||||
/** Ensure local check runs opt into safeguard environment outside CI. */
|
||||
export function resolveLocalCheckEnv(env: Env = process.env) {
|
||||
if (isCiLikeEnv(env) || isLocalCheckEnabled(env)) {
|
||||
return env;
|
||||
}
|
||||
|
||||
return {
|
||||
...env,
|
||||
OPENCLAW_LOCAL_CHECK: "1",
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve a repo tool from this worktree or the primary checkout's installed toolchain. */
|
||||
export function resolveRepoToolBinPath(
|
||||
toolName: string,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
fileExists = fs.existsSync,
|
||||
resolveCommonDir = resolveGitCommonDir,
|
||||
}: RepoToolOptions = {},
|
||||
) {
|
||||
const localPath = path.resolve(cwd, "node_modules", ".bin", toolName);
|
||||
if (fileExists(localPath)) {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
const commonDir = resolveCommonDir(cwd);
|
||||
if (!commonDir || path.basename(commonDir) !== ".git") {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
// Linked worktrees share the primary checkout's .git directory. Its parent
|
||||
// owns the installed toolchain that dependency-less worktrees can reuse.
|
||||
const primaryPath = path.join(path.dirname(commonDir), "node_modules", ".bin", toolName);
|
||||
return fileExists(primaryPath) ? primaryPath : localPath;
|
||||
}
|
||||
|
||||
/** Link a dependency-less worktree to the primary checkout toolchain selected above. */
|
||||
export function ensureRepoToolNodeModulesLink(
|
||||
toolPath: string,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
fileExists = fs.existsSync,
|
||||
resolveCommonDir = resolveGitCommonDir,
|
||||
symlink = fs.symlinkSync,
|
||||
platform = process.platform,
|
||||
}: RepoToolOptions & {
|
||||
symlink?: typeof fs.symlinkSync;
|
||||
platform?: NodeJS.Platform;
|
||||
} = {},
|
||||
) {
|
||||
const localNodeModules = path.resolve(cwd, "node_modules");
|
||||
if (fileExists(localNodeModules)) {
|
||||
return localNodeModules;
|
||||
}
|
||||
|
||||
const commonDir = resolveCommonDir(cwd);
|
||||
if (!commonDir || path.basename(commonDir) !== ".git") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primaryNodeModules = path.join(path.dirname(commonDir), "node_modules");
|
||||
const toolNodeModules = path.dirname(path.dirname(path.resolve(toolPath)));
|
||||
if (toolNodeModules !== path.resolve(primaryNodeModules) || !fileExists(primaryNodeModules)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Match run-vitest.mjs's hydrated-toolchain behavior: keep one stable link
|
||||
// so compilers can resolve imports from worktree source paths.
|
||||
symlink(primaryNodeModules, localNodeModules, platform === "win32" ? "junction" : "dir");
|
||||
} catch (error) {
|
||||
// Another local runner may have installed the same stable link concurrently.
|
||||
if (!fileExists(localNodeModules)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return localNodeModules;
|
||||
}
|
||||
|
||||
function hasFlag(args: string[], name: string) {
|
||||
return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
||||
}
|
||||
|
||||
function hasOxlintFormatArg(args: string[]) {
|
||||
return args.some(
|
||||
(arg) =>
|
||||
arg === "--format" ||
|
||||
arg.startsWith("--format=") ||
|
||||
arg === "-f" ||
|
||||
arg.startsWith("-f=") ||
|
||||
(arg.startsWith("-f") && arg.length > 2),
|
||||
);
|
||||
}
|
||||
|
||||
/** Apply the shared memory and scheduler limits for Go-backed check helpers. */
|
||||
function applyThrottledGoRuntimeEnv(env: Env, hostResources: Resources) {
|
||||
if (!env.GOMAXPROCS) {
|
||||
env.GOMAXPROCS = String(
|
||||
Math.min(DEFAULT_LOCAL_GO_MAX_PROCS, Math.max(1, hostResources.logicalCpuCount)),
|
||||
);
|
||||
}
|
||||
if (!env.GOGC) {
|
||||
env.GOGC = DEFAULT_LOCAL_GO_GC;
|
||||
}
|
||||
if (!env.GOMEMLIMIT) {
|
||||
env.GOMEMLIMIT = DEFAULT_LOCAL_GO_MEMORY_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply local tsgo defaults for declaration skipping, caching, throttling, and profiling. */
|
||||
export function applyLocalTsgoPolicy(args: string[], env: Env, hostResources: Resources) {
|
||||
const nextEnv = { ...env };
|
||||
const nextArgs = [...args];
|
||||
const defaultProjectRun = nextArgs.length === 0;
|
||||
|
||||
if (!hasFlag(nextArgs, "--declaration") && !nextArgs.includes("-d")) {
|
||||
insertBeforeSeparator(nextArgs, "--declaration", "false");
|
||||
}
|
||||
|
||||
if (!isLocalCheckEnabled(nextEnv)) {
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
if (defaultProjectRun) {
|
||||
insertBeforeSeparator(nextArgs, "--incremental");
|
||||
insertBeforeSeparator(
|
||||
nextArgs,
|
||||
"--tsBuildInfoFile",
|
||||
nextEnv.OPENCLAW_TSGO_BUILD_INFO_FILE ?? DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE,
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedHostResources = resolveHostResources(hostResources);
|
||||
if (shouldThrottleLocalChecks(nextEnv, resolvedHostResources, "auto")) {
|
||||
insertBeforeSeparator(nextArgs, "--singleThreaded");
|
||||
insertBeforeSeparator(nextArgs, "--checkers", "1");
|
||||
applyThrottledGoRuntimeEnv(nextEnv, resolvedHostResources);
|
||||
}
|
||||
if (nextEnv.OPENCLAW_TSGO_PPROF_DIR && !hasFlag(nextArgs, "--pprofDir")) {
|
||||
insertBeforeSeparator(nextArgs, "--pprofDir", nextEnv.OPENCLAW_TSGO_PPROF_DIR);
|
||||
}
|
||||
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
/** Apply local oxlint defaults for type-aware checking and throttled worker settings. */
|
||||
export function applyLocalOxlintPolicy(args: string[], env: Env, hostResources: Resources) {
|
||||
const nextEnv = { ...env };
|
||||
const nextArgs = [...args];
|
||||
|
||||
insertBeforeSeparator(nextArgs, "--type-aware");
|
||||
insertBeforeSeparator(nextArgs, "--tsconfig", "config/tsconfig/oxlint.json");
|
||||
if (
|
||||
!hasFlag(nextArgs, "--report-unused-disable-directives") &&
|
||||
!hasFlag(nextArgs, "--report-unused-disable-directives-severity")
|
||||
) {
|
||||
insertBeforeSeparator(nextArgs, "--report-unused-disable-directives-severity", "error");
|
||||
}
|
||||
if (nextEnv.GITHUB_ACTIONS === "true" && !hasOxlintFormatArg(nextArgs)) {
|
||||
insertBeforeSeparator(nextArgs, "--format", "stylish");
|
||||
}
|
||||
|
||||
if (shouldThrottleLocalChecks(nextEnv, hostResources)) {
|
||||
if (!hasFlag(nextArgs, "--threads")) {
|
||||
insertBeforeSeparator(nextArgs, "--threads=1");
|
||||
}
|
||||
// Oxlint's thread flag does not govern the Go tsgolint helper.
|
||||
applyThrottledGoRuntimeEnv(nextEnv, hostResources);
|
||||
}
|
||||
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
function shouldThrottleLocalChecks(
|
||||
env: Env,
|
||||
hostResources: Resources | undefined,
|
||||
defaultMode: LocalCheckMode = "throttled",
|
||||
) {
|
||||
if (!isLocalCheckEnabled(env)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mode = readLocalCheckMode(env, defaultMode);
|
||||
if (mode === "throttled") {
|
||||
return true;
|
||||
}
|
||||
if (mode === "full") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedHostResources = resolveHostResources(hostResources);
|
||||
return (
|
||||
resolvedHostResources.totalMemoryBytes < DEFAULT_FAST_LOCAL_CHECK_MIN_MEMORY_BYTES ||
|
||||
resolvedHostResources.logicalCpuCount < DEFAULT_FAST_LOCAL_CHECK_MIN_CPUS
|
||||
);
|
||||
}
|
||||
|
||||
function resolveGitCommonDir(cwd: string) {
|
||||
const result = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
|
||||
if (result.status === 0) {
|
||||
const raw = result.stdout.trim();
|
||||
if (raw.length > 0) {
|
||||
return path.resolve(cwd, raw);
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(cwd, ".git");
|
||||
}
|
||||
|
||||
function insertBeforeSeparator(args: string[], ...items: [string, ...string[]]) {
|
||||
if (hasFlag(args, items[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const separatorIndex = args.indexOf("--");
|
||||
const insertIndex = separatorIndex === -1 ? args.length : separatorIndex;
|
||||
args.splice(insertIndex, 0, ...items);
|
||||
}
|
||||
|
||||
function readLocalCheckMode(env: Env, defaultMode: LocalCheckMode) {
|
||||
const raw = env.OPENCLAW_LOCAL_CHECK_MODE?.trim().toLowerCase();
|
||||
if (raw === "throttled" || raw === "low-memory") {
|
||||
return "throttled";
|
||||
}
|
||||
if (raw === "full" || raw === "fast") {
|
||||
return "full";
|
||||
}
|
||||
return defaultMode;
|
||||
}
|
||||
|
||||
function resolveHostResources(hostResources: Resources | undefined) {
|
||||
if (hostResources) {
|
||||
return hostResources;
|
||||
}
|
||||
|
||||
return {
|
||||
totalMemoryBytes: os.totalmem(),
|
||||
logicalCpuCount:
|
||||
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
|
||||
};
|
||||
}
|
||||
@@ -1,595 +0,0 @@
|
||||
// Applies local resource policy and process locks for expensive check commands.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const GIB = 1024 ** 3;
|
||||
const DEFAULT_LOCAL_GO_GC = "30";
|
||||
const DEFAULT_LOCAL_GO_MAX_PROCS = 2;
|
||||
const DEFAULT_LOCAL_GO_MEMORY_LIMIT = "3GiB";
|
||||
const DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE = ".artifacts/tsgo-cache/root.tsbuildinfo";
|
||||
const DEFAULT_LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const DEFAULT_LOCK_POLL_MS = 500;
|
||||
const DEFAULT_LOCK_PROGRESS_MS = 15 * 1000;
|
||||
const DEFAULT_STALE_LOCK_MS = 30 * 1000;
|
||||
const DEFAULT_FAST_LOCAL_CHECK_MIN_MEMORY_BYTES = 48 * GIB;
|
||||
const DEFAULT_FAST_LOCAL_CHECK_MIN_CPUS = 12;
|
||||
const SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
||||
|
||||
type Env = NodeJS.ProcessEnv;
|
||||
type Resources = {
|
||||
logicalCpuCount: number;
|
||||
totalMemoryBytes: number;
|
||||
};
|
||||
|
||||
type LocalCheckMode = "auto" | "full" | "throttled";
|
||||
type RepoToolOptions = {
|
||||
cwd?: string;
|
||||
fileExists?: (candidate: string) => boolean;
|
||||
resolveCommonDir?: (cwd: string) => string | null;
|
||||
};
|
||||
type LockOwner = Partial<Record<"cwd" | "pid" | "tool", unknown>>;
|
||||
|
||||
type LocalHeavyCheckLockParams = {
|
||||
cwd: string;
|
||||
env?: Env;
|
||||
lockName?: string;
|
||||
toolName: string;
|
||||
};
|
||||
|
||||
/** Return whether local-heavy-check safeguards are enabled for an environment. */
|
||||
export function isLocalCheckEnabled(env: Env) {
|
||||
const raw = env.OPENCLAW_LOCAL_CHECK?.trim().toLowerCase();
|
||||
return raw !== "0" && raw !== "false";
|
||||
}
|
||||
|
||||
function isCiLikeEnv(env: Env = process.env) {
|
||||
return env.CI === "true" || env.GITHUB_ACTIONS === "true";
|
||||
}
|
||||
|
||||
/** Ensure local check runs opt into safeguard environment outside CI. */
|
||||
export function resolveLocalHeavyCheckEnv(env: Env = process.env) {
|
||||
if (isCiLikeEnv(env) || isLocalCheckEnabled(env)) {
|
||||
return env;
|
||||
}
|
||||
|
||||
return {
|
||||
...env,
|
||||
OPENCLAW_LOCAL_CHECK: "1",
|
||||
};
|
||||
}
|
||||
|
||||
/** Mark every nested heavy-check wrapper as covered by one parent-held lock. */
|
||||
export function withLocalHeavyCheckLockHeld(env: Env): Env {
|
||||
return {
|
||||
...env,
|
||||
OPENCLAW_OXLINT_SKIP_LOCK: "1",
|
||||
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
|
||||
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve a repo tool from this worktree or the primary checkout's installed toolchain. */
|
||||
export function resolveRepoToolBinPath(
|
||||
toolName: string,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
fileExists = fs.existsSync,
|
||||
resolveCommonDir = resolveGitCommonDir,
|
||||
}: RepoToolOptions = {},
|
||||
) {
|
||||
const localPath = path.resolve(cwd, "node_modules", ".bin", toolName);
|
||||
if (fileExists(localPath)) {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
const commonDir = resolveCommonDir(cwd);
|
||||
if (!commonDir || path.basename(commonDir) !== ".git") {
|
||||
return localPath;
|
||||
}
|
||||
|
||||
// Linked worktrees share the primary checkout's .git directory. Its parent
|
||||
// owns the installed toolchain that dependency-less worktrees can reuse.
|
||||
const primaryPath = path.join(path.dirname(commonDir), "node_modules", ".bin", toolName);
|
||||
return fileExists(primaryPath) ? primaryPath : localPath;
|
||||
}
|
||||
|
||||
/** Link a dependency-less worktree to the primary checkout toolchain selected above. */
|
||||
export function ensureRepoToolNodeModulesLink(
|
||||
toolPath: string,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
fileExists = fs.existsSync,
|
||||
resolveCommonDir = resolveGitCommonDir,
|
||||
symlink = fs.symlinkSync,
|
||||
platform = process.platform,
|
||||
}: RepoToolOptions & {
|
||||
symlink?: typeof fs.symlinkSync;
|
||||
platform?: NodeJS.Platform;
|
||||
} = {},
|
||||
) {
|
||||
const localNodeModules = path.resolve(cwd, "node_modules");
|
||||
if (fileExists(localNodeModules)) {
|
||||
return localNodeModules;
|
||||
}
|
||||
|
||||
const commonDir = resolveCommonDir(cwd);
|
||||
if (!commonDir || path.basename(commonDir) !== ".git") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primaryNodeModules = path.join(path.dirname(commonDir), "node_modules");
|
||||
const toolNodeModules = path.dirname(path.dirname(path.resolve(toolPath)));
|
||||
if (toolNodeModules !== path.resolve(primaryNodeModules) || !fileExists(primaryNodeModules)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Match run-vitest.mjs's hydrated-toolchain behavior: keep one stable link
|
||||
// so compilers can resolve imports from worktree source paths.
|
||||
symlink(primaryNodeModules, localNodeModules, platform === "win32" ? "junction" : "dir");
|
||||
} catch (error) {
|
||||
// Another local runner may have installed the same stable link concurrently.
|
||||
if (!fileExists(localNodeModules)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return localNodeModules;
|
||||
}
|
||||
|
||||
function hasFlag(args: string[], name: string) {
|
||||
return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
||||
}
|
||||
|
||||
function hasOxlintFormatArg(args: string[]) {
|
||||
return args.some(
|
||||
(arg) =>
|
||||
arg === "--format" ||
|
||||
arg.startsWith("--format=") ||
|
||||
arg === "-f" ||
|
||||
arg.startsWith("-f=") ||
|
||||
(arg.startsWith("-f") && arg.length > 2),
|
||||
);
|
||||
}
|
||||
|
||||
/** Apply the shared memory and scheduler limits for Go-backed check helpers. */
|
||||
function applyThrottledGoRuntimeEnv(env: Env, hostResources: Resources) {
|
||||
if (!env.GOMAXPROCS) {
|
||||
env.GOMAXPROCS = String(
|
||||
Math.min(DEFAULT_LOCAL_GO_MAX_PROCS, Math.max(1, hostResources.logicalCpuCount)),
|
||||
);
|
||||
}
|
||||
if (!env.GOGC) {
|
||||
env.GOGC = DEFAULT_LOCAL_GO_GC;
|
||||
}
|
||||
if (!env.GOMEMLIMIT) {
|
||||
env.GOMEMLIMIT = DEFAULT_LOCAL_GO_MEMORY_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply local tsgo defaults for declaration skipping, caching, throttling, and profiling. */
|
||||
export function applyLocalTsgoPolicy(args: string[], env: Env, hostResources: Resources) {
|
||||
const nextEnv = { ...env };
|
||||
const nextArgs = [...args];
|
||||
const defaultProjectRun = nextArgs.length === 0;
|
||||
|
||||
if (!hasFlag(nextArgs, "--declaration") && !nextArgs.includes("-d")) {
|
||||
insertBeforeSeparator(nextArgs, "--declaration", "false");
|
||||
}
|
||||
|
||||
if (!isLocalCheckEnabled(nextEnv)) {
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
if (defaultProjectRun) {
|
||||
insertBeforeSeparator(nextArgs, "--incremental");
|
||||
insertBeforeSeparator(
|
||||
nextArgs,
|
||||
"--tsBuildInfoFile",
|
||||
nextEnv.OPENCLAW_TSGO_BUILD_INFO_FILE ?? DEFAULT_LOCAL_TSGO_BUILD_INFO_FILE,
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedHostResources = resolveHostResources(hostResources);
|
||||
if (shouldThrottleLocalHeavyChecks(nextEnv, resolvedHostResources, "auto")) {
|
||||
insertBeforeSeparator(nextArgs, "--singleThreaded");
|
||||
insertBeforeSeparator(nextArgs, "--checkers", "1");
|
||||
applyThrottledGoRuntimeEnv(nextEnv, resolvedHostResources);
|
||||
}
|
||||
if (nextEnv.OPENCLAW_TSGO_PPROF_DIR && !hasFlag(nextArgs, "--pprofDir")) {
|
||||
insertBeforeSeparator(nextArgs, "--pprofDir", nextEnv.OPENCLAW_TSGO_PPROF_DIR);
|
||||
}
|
||||
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
/** Apply local oxlint defaults for type-aware checking and throttled worker settings. */
|
||||
export function applyLocalOxlintPolicy(args: string[], env: Env, hostResources: Resources) {
|
||||
const nextEnv = { ...env };
|
||||
const nextArgs = [...args];
|
||||
|
||||
insertBeforeSeparator(nextArgs, "--type-aware");
|
||||
insertBeforeSeparator(nextArgs, "--tsconfig", "config/tsconfig/oxlint.json");
|
||||
if (
|
||||
!hasFlag(nextArgs, "--report-unused-disable-directives") &&
|
||||
!hasFlag(nextArgs, "--report-unused-disable-directives-severity")
|
||||
) {
|
||||
insertBeforeSeparator(nextArgs, "--report-unused-disable-directives-severity", "error");
|
||||
}
|
||||
if (nextEnv.GITHUB_ACTIONS === "true" && !hasOxlintFormatArg(nextArgs)) {
|
||||
insertBeforeSeparator(nextArgs, "--format", "stylish");
|
||||
}
|
||||
|
||||
if (shouldThrottleLocalHeavyChecks(nextEnv, hostResources)) {
|
||||
if (!hasFlag(nextArgs, "--threads")) {
|
||||
insertBeforeSeparator(nextArgs, "--threads=1");
|
||||
}
|
||||
// Oxlint's thread flag does not govern the Go tsgolint helper.
|
||||
applyThrottledGoRuntimeEnv(nextEnv, hostResources);
|
||||
}
|
||||
|
||||
return { env: nextEnv, args: nextArgs };
|
||||
}
|
||||
|
||||
/** Decide whether an oxlint invocation needs the local heavy-check lock. */
|
||||
export function shouldAcquireLocalHeavyCheckLockForOxlint(
|
||||
args: string[],
|
||||
{ cwd = process.cwd(), env = process.env }: { cwd?: string; env?: Env } = {},
|
||||
) {
|
||||
if (env.OPENCLAW_OXLINT_FORCE_LOCK === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
args.some(
|
||||
(arg) =>
|
||||
arg === "--help" ||
|
||||
arg === "-h" ||
|
||||
arg === "--version" ||
|
||||
arg === "-V" ||
|
||||
arg === "--rules" ||
|
||||
arg === "--print-config" ||
|
||||
arg === "--init",
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const separatorIndex = args.indexOf("--");
|
||||
const candidateArgs = (() => {
|
||||
if (separatorIndex !== -1) {
|
||||
return args.slice(separatorIndex + 1);
|
||||
}
|
||||
const firstFlagIndex = args.findIndex((arg) => arg.startsWith("-"));
|
||||
return firstFlagIndex === -1 ? args : args.slice(0, firstFlagIndex);
|
||||
})();
|
||||
const explicitTargets = candidateArgs.filter((arg) => arg.length > 0 && !arg.startsWith("-"));
|
||||
if (explicitTargets.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !explicitTargets.every((target) => {
|
||||
try {
|
||||
return fs.statSync(path.resolve(cwd, target)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Decide whether a tsgo invocation needs the local heavy-check lock. */
|
||||
export function shouldAcquireLocalHeavyCheckLockForTsgo(args: string[], env: Env = process.env) {
|
||||
if (env.OPENCLAW_TSGO_FORCE_LOCK === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !args.some(
|
||||
(arg) =>
|
||||
arg === "--help" ||
|
||||
arg === "-h" ||
|
||||
arg === "--version" ||
|
||||
arg === "-v" ||
|
||||
arg === "--init" ||
|
||||
arg === "--showConfig",
|
||||
);
|
||||
}
|
||||
|
||||
function shouldThrottleLocalHeavyChecks(
|
||||
env: Env,
|
||||
hostResources: Resources | undefined,
|
||||
defaultMode: LocalCheckMode = "throttled",
|
||||
) {
|
||||
if (!isLocalCheckEnabled(env)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mode = readLocalCheckMode(env, defaultMode);
|
||||
if (mode === "throttled") {
|
||||
return true;
|
||||
}
|
||||
if (mode === "full") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedHostResources = resolveHostResources(hostResources);
|
||||
return (
|
||||
resolvedHostResources.totalMemoryBytes < DEFAULT_FAST_LOCAL_CHECK_MIN_MEMORY_BYTES ||
|
||||
resolvedHostResources.logicalCpuCount < DEFAULT_FAST_LOCAL_CHECK_MIN_CPUS
|
||||
);
|
||||
}
|
||||
|
||||
/** Acquire a filesystem lock for one local heavy check and return its release callback. */
|
||||
export function acquireLocalHeavyCheckLockSync(params: LocalHeavyCheckLockParams) {
|
||||
const env = params.env ?? process.env;
|
||||
|
||||
if (!isLocalCheckEnabled(env)) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const locksDir = resolveHeavyCheckLocksDir(params.cwd, env);
|
||||
const lockDir = path.join(locksDir, `${params.lockName ?? "heavy-check"}.lock`);
|
||||
const ownerPath = path.join(lockDir, "owner.json");
|
||||
const timeoutMs = readPositiveInt(
|
||||
env.OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS,
|
||||
DEFAULT_LOCK_TIMEOUT_MS,
|
||||
"OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS",
|
||||
);
|
||||
const pollMs = readPositiveInt(
|
||||
env.OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS,
|
||||
DEFAULT_LOCK_POLL_MS,
|
||||
"OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS",
|
||||
);
|
||||
const progressMs = readPositiveInt(
|
||||
env.OPENCLAW_HEAVY_CHECK_LOCK_PROGRESS_MS,
|
||||
DEFAULT_LOCK_PROGRESS_MS,
|
||||
"OPENCLAW_HEAVY_CHECK_LOCK_PROGRESS_MS",
|
||||
);
|
||||
const staleLockMs = readPositiveInt(
|
||||
env.OPENCLAW_HEAVY_CHECK_STALE_LOCK_MS,
|
||||
DEFAULT_STALE_LOCK_MS,
|
||||
"OPENCLAW_HEAVY_CHECK_STALE_LOCK_MS",
|
||||
);
|
||||
const startedAt = Date.now();
|
||||
let waitLogBudget = 1;
|
||||
let lastProgressAt = startedAt;
|
||||
const consumeInitialWaitLog = () => waitLogBudget-- > 0;
|
||||
const consumeProgressLog = (now: number) => {
|
||||
if (now - lastProgressAt < progressMs) {
|
||||
return false;
|
||||
}
|
||||
lastProgressAt = now;
|
||||
return true;
|
||||
};
|
||||
|
||||
fs.mkdirSync(locksDir, { recursive: true });
|
||||
if (!params.lockName) {
|
||||
cleanupLegacyLockDirs(locksDir, staleLockMs);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
try {
|
||||
fs.mkdirSync(lockDir);
|
||||
writeOwnerFile(ownerPath, {
|
||||
pid: process.pid,
|
||||
tool: params.toolName,
|
||||
cwd: params.cwd,
|
||||
hostname: os.hostname(),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
return () => {
|
||||
fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
};
|
||||
} catch (error) {
|
||||
if (!isAlreadyExistsError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const owner = readOwnerFile(ownerPath);
|
||||
if (shouldReclaimLock(owner, lockDir, staleLockMs)) {
|
||||
fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
if (elapsedMs >= timeoutMs) {
|
||||
const ownerLabel = describeOwner(owner);
|
||||
throw new Error(
|
||||
`[${params.toolName}] timed out waiting for the local heavy-check lock at ${lockDir}${
|
||||
ownerLabel ? ` (${ownerLabel})` : ""
|
||||
}. If no local heavy checks are still running, remove the stale lock and retry.`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
||||
if (consumeInitialWaitLog()) {
|
||||
const ownerLabel = describeOwner(owner);
|
||||
console.error(
|
||||
`[${params.toolName}] queued behind the local heavy-check lock${
|
||||
ownerLabel ? ` held by ${ownerLabel}` : ""
|
||||
}...`,
|
||||
);
|
||||
} else if (consumeProgressLog(Date.now())) {
|
||||
const ownerLabel = describeOwner(owner);
|
||||
console.error(
|
||||
`[${params.toolName}] still waiting ${formatElapsedMs(elapsedMs)} for the local heavy-check lock${
|
||||
ownerLabel ? ` held by ${ownerLabel}` : ""
|
||||
}...`,
|
||||
);
|
||||
}
|
||||
|
||||
sleepSync(pollMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHeavyCheckLocksDir(cwd: string, env: Env) {
|
||||
const lockScope = env.OPENCLAW_HEAVY_CHECK_LOCK_SCOPE?.trim().toLowerCase();
|
||||
if (lockScope === "worktree") {
|
||||
return path.join(resolveGitWorktreeRoot(cwd), ".artifacts", "openclaw-local-checks");
|
||||
}
|
||||
|
||||
return path.join(resolveGitCommonDir(cwd), "openclaw-local-checks");
|
||||
}
|
||||
|
||||
function resolveGitWorktreeRoot(cwd: string) {
|
||||
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
|
||||
if (result.status === 0) {
|
||||
const raw = result.stdout.trim();
|
||||
if (raw.length > 0) {
|
||||
return path.resolve(cwd, raw);
|
||||
}
|
||||
}
|
||||
|
||||
return cwd;
|
||||
}
|
||||
|
||||
function resolveGitCommonDir(cwd: string) {
|
||||
const result = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
|
||||
if (result.status === 0) {
|
||||
const raw = result.stdout.trim();
|
||||
if (raw.length > 0) {
|
||||
return path.resolve(cwd, raw);
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(cwd, ".git");
|
||||
}
|
||||
|
||||
function cleanupLegacyLockDirs(locksDir: string, staleLockMs: number) {
|
||||
for (const legacyLockName of ["test"]) {
|
||||
const legacyLockDir = path.join(locksDir, `${legacyLockName}.lock`);
|
||||
if (!fs.existsSync(legacyLockDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const owner = readOwnerFile(path.join(legacyLockDir, "owner.json"));
|
||||
if (shouldReclaimLock(owner, legacyLockDir, staleLockMs)) {
|
||||
fs.rmSync(legacyLockDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertBeforeSeparator(args: string[], ...items: [string, ...string[]]) {
|
||||
if (hasFlag(args, items[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const separatorIndex = args.indexOf("--");
|
||||
const insertIndex = separatorIndex === -1 ? args.length : separatorIndex;
|
||||
args.splice(insertIndex, 0, ...items);
|
||||
}
|
||||
|
||||
function readLocalCheckMode(env: Env, defaultMode: LocalCheckMode) {
|
||||
const raw = env.OPENCLAW_LOCAL_CHECK_MODE?.trim().toLowerCase();
|
||||
if (raw === "throttled" || raw === "low-memory") {
|
||||
return "throttled";
|
||||
}
|
||||
if (raw === "full" || raw === "fast") {
|
||||
return "full";
|
||||
}
|
||||
return defaultMode;
|
||||
}
|
||||
|
||||
function resolveHostResources(hostResources: Resources | undefined) {
|
||||
if (hostResources) {
|
||||
return hostResources;
|
||||
}
|
||||
|
||||
return {
|
||||
totalMemoryBytes: os.totalmem(),
|
||||
logicalCpuCount:
|
||||
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
|
||||
};
|
||||
}
|
||||
|
||||
function readPositiveInt(rawValue: string | undefined, fallback: number, label: string) {
|
||||
const text = rawValue?.trim();
|
||||
if (!text) {
|
||||
return fallback;
|
||||
}
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
throw new Error(`${label} must be a positive integer; got: ${rawValue}`);
|
||||
}
|
||||
const parsed = Number(text);
|
||||
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
||||
throw new Error(`${label} must be a positive integer; got: ${rawValue}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function writeOwnerFile(ownerPath: string, owner: unknown) {
|
||||
fs.writeFileSync(ownerPath, `${JSON.stringify(owner, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function readOwnerFile(ownerPath: string): LockOwner | null {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(ownerPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isAlreadyExistsError(error: unknown) {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
||||
}
|
||||
|
||||
function shouldReclaimLock(owner: LockOwner | null, lockDir: string, staleLockMs: number) {
|
||||
if (owner && typeof owner.pid === "number") {
|
||||
return !isProcessAlive(owner.pid);
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(lockDir);
|
||||
return Date.now() - stats.mtimeMs >= staleLockMs;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number) {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
|
||||
}
|
||||
}
|
||||
|
||||
function describeOwner(owner: LockOwner | null) {
|
||||
if (!owner || typeof owner !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const tool = typeof owner.tool === "string" ? owner.tool : "unknown-tool";
|
||||
const pid = typeof owner.pid === "number" ? `pid ${owner.pid}` : "unknown pid";
|
||||
const cwd = typeof owner.cwd === "string" ? owner.cwd : "unknown cwd";
|
||||
return `${tool}, ${pid}, cwd ${cwd}`;
|
||||
}
|
||||
|
||||
function formatElapsedMs(elapsedMs: number) {
|
||||
if (elapsedMs < 1000) {
|
||||
return `${elapsedMs}ms`;
|
||||
}
|
||||
const seconds = elapsedMs / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${seconds.toFixed(seconds >= 10 ? 0 : 1)}s`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainderSeconds = Math.round(seconds % 60);
|
||||
return `${minutes}m ${remainderSeconds}s`;
|
||||
}
|
||||
|
||||
function sleepSync(ms: number) {
|
||||
Atomics.wait(SLEEP_BUFFER, 0, 0, ms);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Holds the shared local heavy-check lock for a whole scripts/pr gate block so
|
||||
// concurrent gate runs across .worktrees serialize before their first command
|
||||
// instead of dying mid-test on child lock timeouts or no-output watchdog kills.
|
||||
import fs from "node:fs";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
|
||||
// A queued gate block legitimately waits out another full gate run; the
|
||||
// 10-minute per-command default in the lock runtime is far too short for that.
|
||||
const DEFAULT_GATE_LOCK_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
||||
const PARENT_WATCH_INTERVAL_MS = 500;
|
||||
|
||||
function parseArgs(argv: string[]): { statusFile: string } {
|
||||
const args = { statusFile: "" };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
if (argv[index] === "--status-file") {
|
||||
args.statusFile = argv[index + 1] ?? "";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown option: ${argv[index]}`);
|
||||
}
|
||||
if (!args.statusFile) {
|
||||
throw new Error("Usage: node scripts/pr-gates-lock.mts --status-file <path>");
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const { statusFile } = parseArgs(process.argv.slice(2));
|
||||
const baseEnv = resolveLocalHeavyCheckEnv(process.env);
|
||||
const env = baseEnv.OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS
|
||||
? baseEnv
|
||||
: { ...baseEnv, OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS: String(DEFAULT_GATE_LOCK_TIMEOUT_MS) };
|
||||
|
||||
const parentPid = process.ppid;
|
||||
const release = acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
toolName: "pr-gates",
|
||||
});
|
||||
let released = false;
|
||||
const releaseOnce = () => {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
released = true;
|
||||
release();
|
||||
};
|
||||
|
||||
process.on("exit", releaseOnce);
|
||||
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
||||
process.on(signal, () => {
|
||||
releaseOnce();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(statusFile, "acquired\n");
|
||||
|
||||
// The owner-pid reclaim in the lock runtime already covers a SIGKILLed
|
||||
// holder; this watch releases within half a second when the gate shell dies.
|
||||
// ppid 1 also counts as dead: orphans reparent to init/launchd, and a
|
||||
// helper that started orphaned has no gate block to hold the lock for.
|
||||
setInterval(() => {
|
||||
if (process.ppid !== parentPid || process.ppid === 1) {
|
||||
releaseOnce();
|
||||
process.exit(0);
|
||||
}
|
||||
}, PARENT_WATCH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -122,59 +122,11 @@ resolve_pr_gates_remote_mode() {
|
||||
esac
|
||||
}
|
||||
|
||||
PR_GATES_LOCK_PID=""
|
||||
PR_GATES_LOCK_STATUS_FILE=""
|
||||
|
||||
acquire_pr_gates_lock() {
|
||||
# Serialize whole gate blocks across .worktrees on the shared heavy-check
|
||||
# lock; a queued gate run waits here, before its first command, instead of
|
||||
# dying on child lock timeouts or shard no-output watchdog kills mid-test.
|
||||
if [ "${OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD:-}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
PR_GATES_LOCK_STATUS_FILE=$(mktemp)
|
||||
# Use the canonical helper: the PR branch under test may predate it.
|
||||
local scripts_dir="${script_parent_dir:-}"
|
||||
if [ -z "$scripts_dir" ]; then
|
||||
scripts_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
fi
|
||||
node "$scripts_dir/pr-gates-lock.mts" --status-file "$PR_GATES_LOCK_STATUS_FILE" &
|
||||
PR_GATES_LOCK_PID=$!
|
||||
while [ ! -s "$PR_GATES_LOCK_STATUS_FILE" ]; do
|
||||
if ! kill -0 "$PR_GATES_LOCK_PID" 2>/dev/null; then
|
||||
wait "$PR_GATES_LOCK_PID" 2>/dev/null || true
|
||||
PR_GATES_LOCK_PID=""
|
||||
echo "Failed to acquire the shared local heavy-check lock for prepare gates."
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
# Same held-lock contract check-changed uses for its children: gate stages
|
||||
# must not re-acquire the lock the block holder already owns.
|
||||
export OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD=1
|
||||
export OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD=1
|
||||
export OPENCLAW_OXLINT_SKIP_LOCK=1
|
||||
}
|
||||
|
||||
prepare_local_gate_workspace() {
|
||||
pin_worktree_bundled_plugins_dir
|
||||
acquire_pr_gates_lock
|
||||
bootstrap_deps_if_needed
|
||||
}
|
||||
|
||||
release_pr_gates_lock() {
|
||||
if [ -z "${PR_GATES_LOCK_PID:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
kill "$PR_GATES_LOCK_PID" 2>/dev/null || true
|
||||
wait "$PR_GATES_LOCK_PID" 2>/dev/null || true
|
||||
PR_GATES_LOCK_PID=""
|
||||
rm -f "$PR_GATES_LOCK_STATUS_FILE"
|
||||
PR_GATES_LOCK_STATUS_FILE=""
|
||||
unset OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD OPENCLAW_OXLINT_SKIP_LOCK
|
||||
}
|
||||
|
||||
run_remote_testbox_full_test_gate() {
|
||||
local label="$1"
|
||||
local log_file="$2"
|
||||
@@ -320,7 +272,6 @@ run_prepare_push_retry_gates() {
|
||||
local remote_gates_run_url=""
|
||||
|
||||
if [ "$docs_only" = "true" ]; then
|
||||
release_pr_gates_lock
|
||||
gates_mode="docs_only"
|
||||
# No test ran: carry the prior full-gates proof and how it was produced.
|
||||
full_gates_head="${FULL_GATES_HEAD_SHA:-}"
|
||||
@@ -328,7 +279,6 @@ run_prepare_push_retry_gates() {
|
||||
remote_gates_lease_id="${REMOTE_GATES_LEASE_ID:-}"
|
||||
remote_gates_run_url="${REMOTE_GATES_RUN_URL:-}"
|
||||
elif [ "$gates_remote_mode" = "testbox" ]; then
|
||||
release_pr_gates_lock
|
||||
gates_mode="remote_testbox"
|
||||
run_remote_testbox_full_test_gate \
|
||||
"pnpm test (lease-retry, blacksmith-testbox)" \
|
||||
@@ -342,7 +292,6 @@ run_prepare_push_retry_gates() {
|
||||
echo "Remote testbox lease-retry gate stamp: $remote_gates_lease_id${remote_gates_run_url:+ ($remote_gates_run_url)}"
|
||||
else
|
||||
run_quiet_logged "pnpm test (lease-retry)" ".local/lease-retry-test.log" pnpm test
|
||||
release_pr_gates_lock
|
||||
fi
|
||||
|
||||
write_gates_env_stamp \
|
||||
@@ -476,7 +425,6 @@ prepare_gates() {
|
||||
run_quiet_logged "pnpm check" ".local/gates-check.log" pnpm check
|
||||
|
||||
if [ "$docs_only" = "true" ]; then
|
||||
release_pr_gates_lock
|
||||
gates_mode="docs_only"
|
||||
previous_full_gates_head=""
|
||||
remote_gates_provider=""
|
||||
@@ -484,9 +432,6 @@ prepare_gates() {
|
||||
remote_gates_run_url=""
|
||||
echo "Docs-only change detected with high confidence; skipping pnpm test."
|
||||
elif [ "$gates_remote_mode" = "testbox" ]; then
|
||||
# The full suite runs on a Blacksmith Testbox, so free the local lock
|
||||
# for other heavy work while we wait on remote proof.
|
||||
release_pr_gates_lock
|
||||
gates_mode="remote_testbox"
|
||||
echo "Running pnpm test on Blacksmith Testbox (OPENCLAW_PR_GATES_REMOTE=testbox)."
|
||||
run_remote_testbox_full_test_gate \
|
||||
@@ -512,7 +457,6 @@ prepare_gates() {
|
||||
echo "Running pnpm test with host-aware scheduling defaults."
|
||||
run_quiet_logged "pnpm test" ".local/gates-test.log" pnpm test
|
||||
fi
|
||||
release_pr_gates_lock
|
||||
remote_gates_provider=""
|
||||
remote_gates_lease_id=""
|
||||
remote_gates_run_url=""
|
||||
|
||||
@@ -16,11 +16,12 @@ import {
|
||||
MAX_TIMER_TIMEOUT_MS,
|
||||
resolveTimerTimeoutMs,
|
||||
} from "../packages/normalization-core/src/number-coercion.ts";
|
||||
import { acquireExtensionPackageBoundaryArtifactLockSync } from "./lib/extension-package-boundary-artifact-lock.mts";
|
||||
import {
|
||||
ensureRepoToolNodeModulesLink,
|
||||
isLocalCheckEnabled,
|
||||
resolveRepoToolBinPath,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
} from "./lib/local-check-runtime.mts";
|
||||
import { parsePositiveInt } from "./lib/numeric-options.mjs";
|
||||
import {
|
||||
listPluginSdkDeclarationOutputs,
|
||||
@@ -989,7 +990,7 @@ export async function runNodeStepsInParallel(steps: NodeStep[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses serial or parallel artifact execution based on local heavy-check policy.
|
||||
* Chooses serial or parallel artifact execution based on local check policy.
|
||||
*/
|
||||
export async function runNodeSteps(steps: NodeStep[], env: NodeJS.ProcessEnv = process.env) {
|
||||
if (!isLocalCheckEnabled(env)) {
|
||||
@@ -1108,7 +1109,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
prerequisiteSteps.push({
|
||||
label: "plugin-sdk boundary dts",
|
||||
args: [runTsgoScript, "-p", "tsconfig.plugin-sdk.dts.json", "--declaration", "true"],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: ROOT_BOUNDARY_TIMEOUT_MS,
|
||||
stamp: {
|
||||
path: ROOT_DTS_STAMP,
|
||||
@@ -1127,7 +1127,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
prerequisiteSteps.push({
|
||||
label: "plugin-sdk package boundary dts",
|
||||
args: [runTsgoScript, "-p", "packages/plugin-sdk/tsconfig.json", "--declaration", "true"],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: ROOT_BOUNDARY_TIMEOUT_MS,
|
||||
stamp: {
|
||||
path: PACKAGE_DTS_STAMP,
|
||||
@@ -1162,7 +1161,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
"--tsBuildInfoFile",
|
||||
"dist/plugin-sdk/extensions/qa-channel/.tsbuildinfo",
|
||||
],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: 300_000,
|
||||
stamp: {
|
||||
path: QA_CHANNEL_DTS_STAMP,
|
||||
@@ -1196,7 +1194,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
"--tsBuildInfoFile",
|
||||
"dist/plugin-sdk/extensions/memory-core/.tsbuildinfo",
|
||||
],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: 300_000,
|
||||
stamp: {
|
||||
path: MEMORY_CORE_DTS_STAMP,
|
||||
@@ -1230,7 +1227,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
"--tsBuildInfoFile",
|
||||
"dist/plugin-sdk/extensions/matrix/.tsbuildinfo",
|
||||
],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: 300_000,
|
||||
stamp: {
|
||||
path: MATRIX_DTS_STAMP,
|
||||
@@ -1264,7 +1260,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
"--tsBuildInfoFile",
|
||||
"dist/plugin-sdk/extensions/discord/.tsbuildinfo",
|
||||
],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: 300_000,
|
||||
stamp: {
|
||||
path: DISCORD_DTS_STAMP,
|
||||
@@ -1298,7 +1293,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
"--tsBuildInfoFile",
|
||||
"dist/plugin-sdk/extensions/slack/.tsbuildinfo",
|
||||
],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: 300_000,
|
||||
stamp: {
|
||||
path: SLACK_DTS_STAMP,
|
||||
@@ -1332,7 +1326,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
"--tsBuildInfoFile",
|
||||
"dist/plugin-sdk/extensions/whatsapp/.tsbuildinfo",
|
||||
],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: 300_000,
|
||||
stamp: {
|
||||
path: WHATSAPP_DTS_STAMP,
|
||||
@@ -1366,7 +1359,6 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
"--tsBuildInfoFile",
|
||||
"dist/plugin-sdk/extensions/telegram/.tsbuildinfo",
|
||||
],
|
||||
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
timeoutMs: 300_000,
|
||||
stamp: {
|
||||
path: TELEGRAM_DTS_STAMP,
|
||||
@@ -1421,10 +1413,15 @@ async function main(argv: string[] = process.argv.slice(2)) {
|
||||
}
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
await main();
|
||||
const releaseArtifactLock = acquireExtensionPackageBoundaryArtifactLockSync(repoRoot);
|
||||
try {
|
||||
await main();
|
||||
} finally {
|
||||
releaseArtifactLock();
|
||||
}
|
||||
}
|
||||
|
||||
+24
-41
@@ -6,12 +6,7 @@ import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
applyLocalTsgoPolicy,
|
||||
resolveRepoToolBinPath,
|
||||
shouldAcquireLocalHeavyCheckLockForTsgo,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
import { applyLocalTsgoPolicy, resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
|
||||
import { createManagedCommandInvocation } from "./lib/managed-child-process.mts";
|
||||
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
||||
import { TSGO_CORE_TEST_SHARDS, type TsgoCoreTestShard } from "./lib/tsgo-core-test-shards.mts";
|
||||
@@ -170,43 +165,31 @@ function runTsgo(
|
||||
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
|
||||
totalMemoryBytes: os.totalmem(),
|
||||
});
|
||||
const releaseLock = shouldAcquireLocalHeavyCheckLockForTsgo(finalArgs, env)
|
||||
? acquireLocalHeavyCheckLockSync({
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
toolName: "tsgo-profile",
|
||||
})
|
||||
: () => {};
|
||||
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const tsgo = createManagedCommandInvocation({
|
||||
args: finalArgs,
|
||||
bin: tsgoPath,
|
||||
env,
|
||||
});
|
||||
const result = spawnSync(tsgo.command, tsgo.args, {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: params.maxBuffer ?? 128 * 1024 * 1024,
|
||||
shell: tsgo.shell,
|
||||
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
|
||||
});
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const stdout = result.stdout ?? "";
|
||||
const stderr = result.stderr ?? "";
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
const output = [stdout, stderr].filter(Boolean).join("\n");
|
||||
throw new Error(`${label} failed with exit code ${result.status ?? 1}\n${output}`);
|
||||
}
|
||||
return { elapsedMs, stdout, stderr };
|
||||
} finally {
|
||||
releaseLock();
|
||||
const tsgo = createManagedCommandInvocation({
|
||||
args: finalArgs,
|
||||
bin: tsgoPath,
|
||||
env,
|
||||
});
|
||||
const result = spawnSync(tsgo.command, tsgo.args, {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: params.maxBuffer ?? 128 * 1024 * 1024,
|
||||
shell: tsgo.shell,
|
||||
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
|
||||
});
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const stdout = result.stdout ?? "";
|
||||
const stderr = result.stderr ?? "";
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
const output = [stdout, stderr].filter(Boolean).join("\n");
|
||||
throw new Error(`${label} failed with exit code ${result.status ?? 1}\n${output}`);
|
||||
}
|
||||
return { elapsedMs, stdout, stderr };
|
||||
}
|
||||
|
||||
function parseDiagnostics(output: string): Diagnostics {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveRepoToolBinPath,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
} from "./lib/local-check-runtime.mts";
|
||||
|
||||
function run(command: string, args: string[], options: SpawnSyncOptions) {
|
||||
const result = spawnSync(command, args, options);
|
||||
|
||||
@@ -4,12 +4,10 @@ import fs, { type Dirent } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
resolveLocalCheckEnv,
|
||||
resolveRepoToolBinPath,
|
||||
shouldAcquireLocalHeavyCheckLockForOxlint,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
} from "./lib/local-check-runtime.mts";
|
||||
import { shouldPrepareExtensionPackageBoundaryArtifacts } from "./run-oxlint.mts";
|
||||
|
||||
const DEFAULT_WINDOWS_EXTENSION_CHUNK_SIZE = 8;
|
||||
@@ -254,86 +252,62 @@ export async function main(
|
||||
) {
|
||||
const runner = path.resolve("scripts", "run-oxlint.mjs");
|
||||
const shardArgs = parseShardRunnerArgs(extraArgs);
|
||||
const env = resolveLocalHeavyCheckEnv(runtimeEnv);
|
||||
const hasMetadataOnlyFlag = shardArgs.oxlintArgs.some((arg) =>
|
||||
["--help", "-h", "--version", "-V", "--rules", "--print-config", "--init"].includes(arg),
|
||||
const env = resolveLocalCheckEnv(runtimeEnv);
|
||||
const shards = createOxlintShards({
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
platform: process.platform,
|
||||
splitCore: shardArgs.splitCore,
|
||||
});
|
||||
const selectedShards = selectCoreOxlintStripe(
|
||||
filterOxlintShards(shards, shardArgs.only),
|
||||
shardArgs.coreStripe,
|
||||
);
|
||||
const shouldAcquireParentLock =
|
||||
!hasMetadataOnlyFlag ||
|
||||
shouldAcquireLocalHeavyCheckLockForOxlint(shardArgs.oxlintArgs, {
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
});
|
||||
const releaseLock =
|
||||
env.OPENCLAW_OXLINT_SKIP_LOCK === "1"
|
||||
? () => {}
|
||||
: shouldAcquireParentLock
|
||||
? acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
toolName: "oxlint shards",
|
||||
})
|
||||
: () => {};
|
||||
|
||||
try {
|
||||
const shards = createOxlintShards({
|
||||
cwd: process.cwd(),
|
||||
ensureRepoToolNodeModulesLink(resolveRepoToolBinPath("oxlint"));
|
||||
const prepareResult = shouldPrepareExtensionPackageBoundaryArtifactsForShards(
|
||||
selectedShards,
|
||||
shardArgs.oxlintArgs,
|
||||
)
|
||||
? spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mts"),
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env,
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (prepareResult?.error) {
|
||||
throw prepareResult.error;
|
||||
}
|
||||
if (prepareResult && (prepareResult.status ?? 1) !== 0) {
|
||||
process.exitCode = prepareResult.status ?? 1;
|
||||
} else {
|
||||
const shardConcurrency = resolveOxlintShardConcurrency({
|
||||
env,
|
||||
platform: process.platform,
|
||||
splitCore: shardArgs.splitCore,
|
||||
});
|
||||
const selectedShards = selectCoreOxlintStripe(
|
||||
filterOxlintShards(shards, shardArgs.only),
|
||||
shardArgs.coreStripe,
|
||||
const hostResources = resolveHostResources();
|
||||
// stderr: stdout may carry machine-readable oxlint output for callers.
|
||||
console.error(
|
||||
`[oxlint] shard concurrency ${Math.max(1, Math.min(shardConcurrency, selectedShards.length))} ` +
|
||||
`(cpus=${hostResources.logicalCpuCount}, memGB=${Math.round(hostResources.totalMemoryBytes / 1024 ** 3)})`,
|
||||
);
|
||||
|
||||
ensureRepoToolNodeModulesLink(resolveRepoToolBinPath("oxlint"));
|
||||
const prepareResult = shouldPrepareExtensionPackageBoundaryArtifactsForShards(
|
||||
selectedShards,
|
||||
shardArgs.oxlintArgs,
|
||||
)
|
||||
? spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mts"),
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env,
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (prepareResult?.error) {
|
||||
throw prepareResult.error;
|
||||
}
|
||||
if (prepareResult && (prepareResult.status ?? 1) !== 0) {
|
||||
process.exitCode = prepareResult.status ?? 1;
|
||||
} else {
|
||||
const shardConcurrency = resolveOxlintShardConcurrency({
|
||||
env,
|
||||
platform: process.platform,
|
||||
splitCore: shardArgs.splitCore,
|
||||
});
|
||||
const hostResources = resolveHostResources();
|
||||
// stderr: stdout may carry machine-readable oxlint output for callers.
|
||||
console.error(
|
||||
`[oxlint] shard concurrency ${Math.max(1, Math.min(shardConcurrency, selectedShards.length))} ` +
|
||||
`(cpus=${hostResources.logicalCpuCount}, memGB=${Math.round(hostResources.totalMemoryBytes / 1024 ** 3)})`,
|
||||
);
|
||||
const results = await runShards({
|
||||
concurrency: Math.max(1, Math.min(shardConcurrency, selectedShards.length)),
|
||||
entries: selectedShards,
|
||||
env,
|
||||
extraArgs: shardArgs.oxlintArgs,
|
||||
runner,
|
||||
});
|
||||
process.exitCode = results.find((status) => status !== 0) ?? 0;
|
||||
}
|
||||
} finally {
|
||||
releaseLock();
|
||||
const results = await runShards({
|
||||
concurrency: Math.max(1, Math.min(shardConcurrency, selectedShards.length)),
|
||||
entries: selectedShards,
|
||||
env,
|
||||
extraArgs: shardArgs.oxlintArgs,
|
||||
runner,
|
||||
});
|
||||
process.exitCode = results.find((status) => status !== 0) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +514,6 @@ export async function runShard({ env, extraArgs, runner, shard }: ShardRunnerOpt
|
||||
detached: useProcessGroup,
|
||||
env: {
|
||||
...env,
|
||||
OPENCLAW_OXLINT_SKIP_LOCK: "1",
|
||||
OPENCLAW_OXLINT_SKIP_PREPARE: "1",
|
||||
},
|
||||
});
|
||||
|
||||
+18
-51
@@ -1,16 +1,14 @@
|
||||
// Runs oxlint with local heavy-check policy, sparse-checkout filtering, and
|
||||
// Runs oxlint with local resource policy, sparse-checkout filtering, and
|
||||
// plugin package-boundary artifact preparation when needed.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
applyLocalOxlintPolicy,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
resolveLocalCheckEnv,
|
||||
resolveRepoToolBinPath,
|
||||
shouldAcquireLocalHeavyCheckLockForOxlint,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
} from "./lib/local-check-runtime.mts";
|
||||
import { createManagedCommandInvocation, runManagedCommand } from "./lib/managed-child-process.mts";
|
||||
import { resolvePathEnvKey } from "./windows-cmd-helpers.mjs";
|
||||
|
||||
@@ -227,27 +225,14 @@ function resolveOxlintToolchainEnv(
|
||||
}
|
||||
|
||||
async function prepareExtensionPackageBoundaryArtifacts(env: NodeJS.ProcessEnv) {
|
||||
const releaseArtifactsLock = acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
const status = await runManagedCommand({
|
||||
bin: process.execPath,
|
||||
args: PREPARE_EXTENSION_BOUNDARY_ARGS,
|
||||
env,
|
||||
toolName: "extension-package-boundary-artifacts",
|
||||
lockName: "extension-package-boundary-artifacts",
|
||||
});
|
||||
|
||||
try {
|
||||
const status = await runManagedCommand({
|
||||
bin: process.execPath,
|
||||
args: PREPARE_EXTENSION_BOUNDARY_ARGS,
|
||||
env,
|
||||
});
|
||||
|
||||
if (status !== 0) {
|
||||
throw new Error(
|
||||
`prepare-extension-package-boundary-artifacts failed with exit code ${status}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
releaseArtifactsLock();
|
||||
if (status !== 0) {
|
||||
throw new Error(`prepare-extension-package-boundary-artifacts failed with exit code ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +245,7 @@ async function main(
|
||||
) {
|
||||
const focusedConfig = argv.includes(OPENCLAW_FOCUSED_CONFIG_FLAG);
|
||||
const oxlintArgs = argv.filter((arg) => arg !== OPENCLAW_FOCUSED_CONFIG_FLAG);
|
||||
const localEnv = resolveLocalHeavyCheckEnv(runtimeEnv);
|
||||
const localEnv = resolveLocalCheckEnv(runtimeEnv);
|
||||
// Focused configs are syntax-only guards; keep wrapper process handling
|
||||
// without the broad type-aware policy or package artifact preparation.
|
||||
const { args: policyArgs, env } = focusedConfig
|
||||
@@ -292,34 +277,16 @@ async function main(
|
||||
return;
|
||||
}
|
||||
|
||||
const releaseLock =
|
||||
env.OPENCLAW_OXLINT_SKIP_LOCK === "1" || focusedConfig
|
||||
? () => {}
|
||||
: shouldAcquireLocalHeavyCheckLockForOxlint(finalArgs, {
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
})
|
||||
? acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
toolName: "oxlint",
|
||||
})
|
||||
: () => {};
|
||||
|
||||
try {
|
||||
if (needsArtifactPreparation) {
|
||||
await prepareExtensionPackageBoundaryArtifacts(env);
|
||||
}
|
||||
|
||||
const status = await runManagedCommand({
|
||||
bin: oxlintPath,
|
||||
args: finalArgs,
|
||||
env: resolveOxlintToolchainEnv(oxlintPath, env),
|
||||
});
|
||||
process.exitCode = status;
|
||||
} finally {
|
||||
releaseLock();
|
||||
if (needsArtifactPreparation) {
|
||||
await prepareExtensionPackageBoundaryArtifacts(env);
|
||||
}
|
||||
|
||||
const status = await runManagedCommand({
|
||||
bin: oxlintPath,
|
||||
args: finalArgs,
|
||||
env: resolveOxlintToolchainEnv(oxlintPath, env),
|
||||
});
|
||||
process.exitCode = status;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
||||
import {
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveRepoToolBinPath,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
} from "./lib/local-check-runtime.mts";
|
||||
|
||||
const stylelintPath = resolveRepoToolBinPath("stylelint");
|
||||
ensureRepoToolNodeModulesLink(stylelintPath);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Run bounded test graphs in fresh processes so one shard's checker heap cannot
|
||||
// accumulate while the next shard loads. Hold one lock across the full sequence.
|
||||
// accumulate while the next shard loads.
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
import { resolveLocalCheckEnv } from "./lib/local-check-runtime.mts";
|
||||
import { signalExitCode } from "./lib/managed-child-process.mts";
|
||||
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
||||
import {
|
||||
@@ -48,15 +45,7 @@ if (concurrencyFlagIndex >= 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const env = resolveLocalHeavyCheckEnv(process.env);
|
||||
const releaseLock =
|
||||
env.OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD === "1"
|
||||
? () => {}
|
||||
: acquireLocalHeavyCheckLockSync({
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
toolName: "tsgo:core:test",
|
||||
});
|
||||
const env = resolveLocalCheckEnv(process.env);
|
||||
|
||||
function runShard(config: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -65,7 +54,7 @@ function runShard(config: string): Promise<number> {
|
||||
[path.join(repoRoot, "scripts/run-tsgo.mjs"), "-b", config, "--builders", "1"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: { ...env, OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
|
||||
env,
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
@@ -76,26 +65,22 @@ function runShard(config: string): Promise<number> {
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const queue = [...shards];
|
||||
let failureCode = 0;
|
||||
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
||||
for (;;) {
|
||||
const shard = queue.shift();
|
||||
// Stop draining after the first failure so the exit stays prompt.
|
||||
if (!shard || failureCode !== 0) {
|
||||
return;
|
||||
}
|
||||
const code = await runShard(shard.config);
|
||||
if (code !== 0 && failureCode === 0) {
|
||||
failureCode = code;
|
||||
}
|
||||
const queue = [...shards];
|
||||
let failureCode = 0;
|
||||
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
||||
for (;;) {
|
||||
const shard = queue.shift();
|
||||
// Stop draining after the first failure so the exit stays prompt.
|
||||
if (!shard || failureCode !== 0) {
|
||||
return;
|
||||
}
|
||||
const code = await runShard(shard.config);
|
||||
if (code !== 0 && failureCode === 0) {
|
||||
failureCode = code;
|
||||
}
|
||||
});
|
||||
await Promise.all(workers);
|
||||
if (failureCode !== 0) {
|
||||
process.exitCode = failureCode;
|
||||
}
|
||||
} finally {
|
||||
releaseLock();
|
||||
});
|
||||
await Promise.all(workers);
|
||||
if (failureCode !== 0) {
|
||||
process.exitCode = failureCode;
|
||||
}
|
||||
|
||||
+30
-46
@@ -1,17 +1,15 @@
|
||||
// Runs tsgo through local heavy-check policy and sparse-checkout guards.
|
||||
// Runs tsgo through local resource policy and sparse-checkout guards.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { readFlagValue } from "./lib/arg-utils.mts";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
applyLocalTsgoPolicy,
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
resolveLocalCheckEnv,
|
||||
resolveRepoToolBinPath,
|
||||
shouldAcquireLocalHeavyCheckLockForTsgo,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
} from "./lib/local-check-runtime.mts";
|
||||
import { createManagedCommandInvocation } from "./lib/managed-child-process.mts";
|
||||
import {
|
||||
getSparseTsgoGuardError,
|
||||
@@ -26,7 +24,7 @@ function main(): void {
|
||||
};
|
||||
const { args: finalArgs, env } = applyLocalTsgoPolicy(
|
||||
process.argv.slice(2),
|
||||
resolveLocalHeavyCheckEnv(process.env),
|
||||
resolveLocalCheckEnv(process.env),
|
||||
hostResources,
|
||||
);
|
||||
|
||||
@@ -36,49 +34,35 @@ function main(): void {
|
||||
fs.mkdirSync(path.dirname(path.resolve(tsBuildInfoFile)), { recursive: true });
|
||||
}
|
||||
const sparseGuardError = getSparseTsgoGuardError(finalArgs, { cwd: process.cwd() });
|
||||
const releaseLock =
|
||||
sparseGuardError ||
|
||||
env.OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD === "1" ||
|
||||
!shouldAcquireLocalHeavyCheckLockForTsgo(finalArgs, env)
|
||||
? () => {}
|
||||
: acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
toolName: "tsgo",
|
||||
});
|
||||
|
||||
try {
|
||||
if (sparseGuardError) {
|
||||
console.error(sparseGuardError);
|
||||
if (shouldSkipSparseTsgoGuardError(env)) {
|
||||
console.error("[tsgo] skipping sparse-missing project because OPENCLAW_TSGO_SPARSE_SKIP=1");
|
||||
process.exitCode = 0;
|
||||
} else {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
if (sparseGuardError) {
|
||||
console.error(sparseGuardError);
|
||||
if (shouldSkipSparseTsgoGuardError(env)) {
|
||||
console.error("[tsgo] skipping sparse-missing project because OPENCLAW_TSGO_SPARSE_SKIP=1");
|
||||
process.exitCode = 0;
|
||||
} else {
|
||||
ensureRepoToolNodeModulesLink(tsgoPath);
|
||||
const tsgo = createManagedCommandInvocation({
|
||||
args: finalArgs,
|
||||
bin: tsgoPath,
|
||||
env,
|
||||
});
|
||||
const result = spawnSync(tsgo.command, tsgo.args, {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: tsgo.shell,
|
||||
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
process.exitCode = result.status ?? 1;
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} finally {
|
||||
releaseLock();
|
||||
return;
|
||||
}
|
||||
|
||||
ensureRepoToolNodeModulesLink(tsgoPath);
|
||||
const tsgo = createManagedCommandInvocation({
|
||||
args: finalArgs,
|
||||
bin: tsgoPath,
|
||||
env,
|
||||
});
|
||||
const result = spawnSync(tsgo.command, tsgo.args, {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
shell: tsgo.shell,
|
||||
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
process.exitCode = result.status ?? 1;
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
|
||||
@@ -5,10 +5,6 @@ import fs from "node:fs";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import pMap from "p-map";
|
||||
import { formatMs } from "./lib/check-timing-summary.mts";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
withLocalHeavyCheckLockHeld,
|
||||
} from "./lib/local-heavy-check-runtime.mts";
|
||||
import {
|
||||
isCiLikeEnv,
|
||||
resolveLocalFullSuiteProfile,
|
||||
@@ -42,7 +38,6 @@ import {
|
||||
resolveParallelFullSuiteConcurrency,
|
||||
resolveChangedTestTargetPlanForArgs,
|
||||
resolveChangedTargetArgs,
|
||||
shouldAcquireLocalHeavyCheckLock,
|
||||
shouldRetryVitestNoOutputTimeout,
|
||||
type FailedVitestShard,
|
||||
type VitestRunSpec as BaseVitestRunSpec,
|
||||
@@ -59,19 +54,6 @@ type VitestCommandOutcome = {
|
||||
|
||||
type ShardTiming = NonNullable<ReturnType<typeof createShardTimingSample>>;
|
||||
|
||||
// Keep this shim so `pnpm test -- src/foo.test.ts` still forwards filters
|
||||
// cleanly instead of leaking pnpm's passthrough sentinel to Vitest.
|
||||
let releaseLock = () => {};
|
||||
let lockReleased = false;
|
||||
|
||||
const releaseLockOnce = () => {
|
||||
if (lockReleased) {
|
||||
return;
|
||||
}
|
||||
lockReleased = true;
|
||||
releaseLock();
|
||||
};
|
||||
|
||||
function isWrapperMetadataRequest(args: string[]) {
|
||||
for (const arg of args) {
|
||||
if (arg === "--") {
|
||||
@@ -193,7 +175,6 @@ async function runLoggedVitestSpec(spec: VitestRunSpec) {
|
||||
}
|
||||
if (result.signal) {
|
||||
console.error(`[test] ${spec.config} exited by signal ${result.signal}`);
|
||||
releaseLockOnce();
|
||||
process.kill(process.pid, result.signal);
|
||||
return null;
|
||||
}
|
||||
@@ -312,7 +293,7 @@ async function main() {
|
||||
baseEnv,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
let runSpecs = applyDefaultMultiSpecVitestCachePaths(
|
||||
const runSpecs = applyDefaultMultiSpecVitestCachePaths(
|
||||
applyDefaultVitestNoOutputTimeout(
|
||||
applyFullExtensionsHeapBudget(rawRunSpecs, { env: baseEnv }),
|
||||
{
|
||||
@@ -328,21 +309,6 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const acquiresLocalHeavyCheckLock = shouldAcquireLocalHeavyCheckLock(runSpecs, baseEnv);
|
||||
releaseLock = acquiresLocalHeavyCheckLock
|
||||
? acquireLocalHeavyCheckLockSync({
|
||||
cwd: process.cwd(),
|
||||
env: baseEnv,
|
||||
toolName: "test",
|
||||
})
|
||||
: () => {};
|
||||
if (acquiresLocalHeavyCheckLock || baseEnv.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD === "1") {
|
||||
runSpecs = runSpecs.map((spec) => ({
|
||||
...spec,
|
||||
env: withLocalHeavyCheckLockHeld(spec.env),
|
||||
}));
|
||||
}
|
||||
|
||||
const isFullSuiteRun =
|
||||
targetArgs.length === 0 &&
|
||||
changedTargetArgs === null &&
|
||||
@@ -390,7 +356,6 @@ async function main() {
|
||||
for (const line of formatFailedShardDigest(failures)) {
|
||||
console.error(line);
|
||||
}
|
||||
releaseLockOnce();
|
||||
if (parallelExitCode !== 0) {
|
||||
process.exitCode = parallelExitCode;
|
||||
}
|
||||
@@ -412,7 +377,6 @@ async function main() {
|
||||
exitCode = exitCode || result.code;
|
||||
if (spec.continueOnFailure !== true) {
|
||||
printTestSummary("failed", timings.length, performance.now() - suiteStartedAt);
|
||||
releaseLockOnce();
|
||||
process.exitCode = result.code;
|
||||
return;
|
||||
}
|
||||
@@ -425,7 +389,6 @@ async function main() {
|
||||
performance.now() - suiteStartedAt,
|
||||
);
|
||||
|
||||
releaseLockOnce();
|
||||
if (exitCode !== 0) {
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
@@ -444,7 +407,6 @@ function printTestSummary(
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
releaseLockOnce();
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -4223,31 +4223,6 @@ function filterPlansForContractIncludeFile(plans: VitestRunPlan[], env: NodeJS.P
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldAcquireLocalHeavyCheckLock(
|
||||
runSpecs: Array<Pick<VitestRunSpec, "config" | "includePatterns" | "watchMode">>,
|
||||
env = process.env,
|
||||
) {
|
||||
if (env.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD === "1") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (env.OPENCLAW_TEST_PROJECTS_FORCE_LOCK === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const runSpec = runSpecs.length === 1 ? runSpecs[0] : undefined;
|
||||
if (!runSpec) {
|
||||
return true;
|
||||
}
|
||||
return !(
|
||||
(runSpec.config === TOOLING_VITEST_CONFIG ||
|
||||
runSpec.config === TOOLING_ISOLATED_VITEST_CONFIG) &&
|
||||
!runSpec.watchMode &&
|
||||
Array.isArray(runSpec.includePatterns) &&
|
||||
runSpec.includePatterns.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function expandVitestIncludePatterns(includePatterns: string[], cwd: string) {
|
||||
const candidateFiles = includePatterns.some(isGlobTarget)
|
||||
? listExplicitTestTargetFilesForCwd(cwd)
|
||||
|
||||
Reference in New Issue
Block a user