mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-20 17:21:33 +00:00
fix: complete lint toolchain reuse in worktrees (#111467)
* fix(scripts): complete worktree lint toolchain * fix(scripts): prepare complete lint pipeline
This commit is contained in:
committed by
GitHub
parent
f8e35afecc
commit
87fbc19fe3
@@ -1695,7 +1695,7 @@
|
||||
"ios:version:check": "node --import tsx scripts/ios-sync-versioning.ts --check",
|
||||
"ios:version:sync": "node --import tsx scripts/ios-sync-versioning.ts --write",
|
||||
"leak:embedded-run": "node --import tsx --expose-gc scripts/embedded-run-abort-leak.ts",
|
||||
"lint": "pnpm lint:ui:i18n && node scripts/run-oxlint-shards.mjs",
|
||||
"lint": "node scripts/run-lint.mjs",
|
||||
"lint:agent:ingress-owner": "node scripts/check-ingress-agent-owner-context.mjs",
|
||||
"lint:all": "node scripts/run-oxlint.mjs",
|
||||
"lint:apps": "pnpm lint:swift",
|
||||
|
||||
@@ -15,6 +15,17 @@ export function resolveRepoToolBinPath(
|
||||
resolveCommonDir?: (cwd: string) => string | null;
|
||||
},
|
||||
): string;
|
||||
/** Link a dependency-less worktree to the primary checkout toolchain selected above. */
|
||||
export function ensureRepoToolNodeModulesLink(
|
||||
toolPath: string,
|
||||
options?: {
|
||||
cwd?: string;
|
||||
fileExists?: (candidate: string) => boolean;
|
||||
resolveCommonDir?: (cwd: string) => string | null;
|
||||
symlink?: (target: string, path: string, type?: string) => void;
|
||||
platform?: NodeJS.Platform;
|
||||
},
|
||||
): string | null;
|
||||
/** Apply local tsgo defaults for declaration skipping, caching, throttling, and profiling. */
|
||||
export function applyLocalTsgoPolicy(
|
||||
args: string[],
|
||||
|
||||
@@ -59,6 +59,46 @@ export function resolveRepoToolBinPath(
|
||||
return fileExists(primaryPath) ? primaryPath : localPath;
|
||||
}
|
||||
|
||||
/** Link a dependency-less worktree to the primary checkout toolchain selected above. */
|
||||
export function ensureRepoToolNodeModulesLink(
|
||||
toolPath,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
fileExists = fs.existsSync,
|
||||
resolveCommonDir = resolveGitCommonDir,
|
||||
symlink = fs.symlinkSync,
|
||||
platform = process.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, name) {
|
||||
return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/** Resolve tsx's loader through the selected checkout toolchain. */
|
||||
export function resolveTsxImportSpecifier(options?: {
|
||||
resolveTool?: (toolName: string) => string;
|
||||
createRequireFrom?: (filename: string) => { resolve(packageName: string): string };
|
||||
ensureToolchain?: (toolPath: string) => string | null;
|
||||
}): string;
|
||||
/**
|
||||
* Lists entry-shim artifacts written by scripts/write-plugin-sdk-entry-dts.ts.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
// boundary imports resolve through public package surfaces.
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path, { resolve } from "node:path";
|
||||
import { isLocalCheckEnabled } from "./lib/local-heavy-check-runtime.mjs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
ensureRepoToolNodeModulesLink,
|
||||
isLocalCheckEnabled,
|
||||
resolveRepoToolBinPath,
|
||||
} from "./lib/local-heavy-check-runtime.mjs";
|
||||
import { parsePositiveInt } from "./lib/numeric-options.mjs";
|
||||
import { pluginSdkEntrypoints, publicPluginSdkEntrypoints } from "./lib/plugin-sdk-entries.mjs";
|
||||
import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs";
|
||||
@@ -31,6 +37,17 @@ let exitingAfterParentSignal = false;
|
||||
let parentSignalExitCode = 1;
|
||||
let parentSignalExitTimer;
|
||||
|
||||
/** Resolve tsx's loader through the selected checkout toolchain. */
|
||||
export function resolveTsxImportSpecifier({
|
||||
resolveTool = resolveRepoToolBinPath,
|
||||
createRequireFrom = createRequire,
|
||||
ensureToolchain = ensureRepoToolNodeModulesLink,
|
||||
} = {}) {
|
||||
const tsxBinPath = resolveTool("tsx");
|
||||
ensureToolchain(tsxBinPath);
|
||||
return pathToFileURL(createRequireFrom(tsxBinPath).resolve("tsx")).href;
|
||||
}
|
||||
|
||||
function listPackageDtsOutputsFromExports({ packageDir, outputPrefix }) {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, "packages", packageDir, "package.json"), "utf8"),
|
||||
@@ -1016,7 +1033,11 @@ async function main(argv = process.argv.slice(2)) {
|
||||
if (mode === "all" && (!entryShimsFresh || prerequisiteSteps.length > 0)) {
|
||||
await runNodeStep(
|
||||
"plugin-sdk boundary root shims",
|
||||
["--import", "tsx", resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts")],
|
||||
[
|
||||
"--import",
|
||||
resolveTsxImportSpecifier(),
|
||||
resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts"),
|
||||
],
|
||||
ROOT_SHIMS_TIMEOUT_MS,
|
||||
{ env: { NODE_OPTIONS: ROOT_SHIMS_NODE_OPTIONS } },
|
||||
);
|
||||
|
||||
39
scripts/run-lint.mjs
Normal file
39
scripts/run-lint.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Runs the complete lint pipeline after preparing a linked-worktree toolchain.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveRepoToolBinPath,
|
||||
} from "./lib/local-heavy-check-runtime.mjs";
|
||||
|
||||
function run(command, args, options) {
|
||||
const result = spawnSync(command, args, options);
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
return result.status ?? 1;
|
||||
}
|
||||
|
||||
const oxlintPath = resolveRepoToolBinPath("oxlint");
|
||||
const tsxPath = resolveRepoToolBinPath("tsx");
|
||||
ensureRepoToolNodeModulesLink(oxlintPath);
|
||||
const tsxImportSpecifier = pathToFileURL(createRequire(tsxPath).resolve("tsx")).href;
|
||||
|
||||
// Invoke the pre-step directly: running pnpm through a linked node_modules can
|
||||
// reconcile the owning checkout's dependency tree instead of merely running it.
|
||||
const uiI18nStatus = run(
|
||||
process.execPath,
|
||||
["--import", tsxImportSpecifier, path.resolve("scripts", "control-ui-i18n-verify.ts"), "verify"],
|
||||
{ env: process.env, stdio: "inherit" },
|
||||
);
|
||||
if (uiI18nStatus !== 0) {
|
||||
process.exitCode = uiI18nStatus;
|
||||
} else {
|
||||
process.exitCode = run(
|
||||
process.execPath,
|
||||
[path.resolve("scripts", "run-oxlint-shards.mjs"), ...process.argv.slice(2)],
|
||||
{ env: process.env, stdio: "inherit" },
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,11 @@ import { spawn, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import pMap from "p-map";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
resolveRepoToolBinPath,
|
||||
shouldAcquireLocalHeavyCheckLockForOxlint,
|
||||
} from "./lib/local-heavy-check-runtime.mjs";
|
||||
|
||||
@@ -258,6 +259,7 @@ export async function main(extraArgs = process.argv.slice(2), runtimeEnv = proce
|
||||
const selectedShards = filterOxlintShards(shards, shardArgs.only);
|
||||
|
||||
try {
|
||||
ensureRepoToolNodeModulesLink(resolveRepoToolBinPath("oxlint"));
|
||||
const prepareResult = spawnSync(
|
||||
process.execPath,
|
||||
[path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mjs")],
|
||||
@@ -390,6 +392,9 @@ export function resolveOxlintShardConcurrency({
|
||||
}
|
||||
|
||||
async function runShards({ concurrency, entries, env, extraArgs, runner }) {
|
||||
// Dependency-less worktrees establish their primary-checkout toolchain link
|
||||
// before this lazy import, avoiding a top-level package-resolution failure.
|
||||
const { default: pMap } = await import("p-map");
|
||||
const results = await pMap(
|
||||
entries,
|
||||
async (shard) => {
|
||||
|
||||
@@ -230,6 +230,11 @@ export async function main(argv = process.argv.slice(2), runtimeEnv = process.en
|
||||
: applyLocalOxlintPolicy(oxlintArgs, localEnv);
|
||||
const sparseTargets = filterSparseMissingOxlintTargets(policyArgs);
|
||||
const finalArgs = sparseTargets.args;
|
||||
const oxlintPath = resolveRepoToolBinPath("oxlint");
|
||||
const needsArtifactPreparation =
|
||||
!focusedConfig &&
|
||||
env.OPENCLAW_OXLINT_SKIP_PREPARE !== "1" &&
|
||||
shouldPrepareExtensionPackageBoundaryArtifacts(finalArgs);
|
||||
if (sparseTargets.skippedTargets.length > 0) {
|
||||
console.error(
|
||||
`[oxlint] sparse checkout is missing tracked target(s); skipping ${sparseTargets.skippedTargets.join(", ")}`,
|
||||
@@ -261,15 +266,10 @@ export async function main(argv = process.argv.slice(2), runtimeEnv = process.en
|
||||
: () => {};
|
||||
|
||||
try {
|
||||
if (
|
||||
!focusedConfig &&
|
||||
env.OPENCLAW_OXLINT_SKIP_PREPARE !== "1" &&
|
||||
shouldPrepareExtensionPackageBoundaryArtifacts(finalArgs)
|
||||
) {
|
||||
if (needsArtifactPreparation) {
|
||||
await prepareExtensionPackageBoundaryArtifacts(env);
|
||||
}
|
||||
|
||||
const oxlintPath = resolveRepoToolBinPath("oxlint");
|
||||
const status = await runManagedCommand({
|
||||
bin: oxlintPath,
|
||||
args: finalArgs,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { readFlagValue } from "./lib/arg-utils.mjs";
|
||||
import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
applyLocalTsgoPolicy,
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
resolveRepoToolBinPath,
|
||||
shouldAcquireLocalHeavyCheckLockForTsgo,
|
||||
@@ -48,6 +49,7 @@ try {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} else {
|
||||
ensureRepoToolNodeModulesLink(tsgoPath);
|
||||
const tsgo = createManagedCommandInvocation({
|
||||
args: finalArgs,
|
||||
bin: tsgoPath,
|
||||
|
||||
@@ -1535,6 +1535,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
],
|
||||
["scripts/run-oxlint.mjs", ["test/scripts/run-oxlint.test.ts"]],
|
||||
["scripts/run-oxlint-shards.mjs", ["test/scripts/run-oxlint.test.ts"]],
|
||||
["scripts/run-lint.mjs", ["test/scripts/run-oxlint.test.ts"]],
|
||||
["scripts/run-with-env.mjs", ["test/scripts/run-with-env.test.ts"]],
|
||||
["scripts/run-node.mjs", ["src/infra/run-node.test.ts"]],
|
||||
[
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
acquireLocalHeavyCheckLockSync,
|
||||
applyLocalOxlintPolicy,
|
||||
applyLocalTsgoPolicy,
|
||||
ensureRepoToolNodeModulesLink,
|
||||
resolveLocalHeavyCheckEnv,
|
||||
resolveRepoToolBinPath,
|
||||
shouldAcquireLocalHeavyCheckLockForOxlint,
|
||||
@@ -64,6 +65,51 @@ describe("local-heavy-check-runtime", () => {
|
||||
).toBe(localPath);
|
||||
});
|
||||
|
||||
it("links dependency-less worktrees to the selected checkout's modules", () => {
|
||||
const primaryRoot = createTempDir("openclaw-primary-toolchain-");
|
||||
const cwd = path.join(primaryRoot, ".codex", "worktrees", "task", "openclaw");
|
||||
const commonDir = path.join(primaryRoot, ".git");
|
||||
const primaryTsgo = path.join(primaryRoot, "node_modules", ".bin", "tsgo");
|
||||
const primaryNodeModules = path.join(primaryRoot, "node_modules");
|
||||
const localNodeModules = path.join(cwd, "node_modules");
|
||||
fs.mkdirSync(path.dirname(primaryTsgo), { recursive: true });
|
||||
fs.mkdirSync(cwd, { recursive: true });
|
||||
|
||||
expect(
|
||||
ensureRepoToolNodeModulesLink(primaryTsgo, {
|
||||
cwd,
|
||||
resolveCommonDir: () => commonDir,
|
||||
}),
|
||||
).toBe(localNodeModules);
|
||||
expect(fs.realpathSync(localNodeModules)).toBe(fs.realpathSync(primaryNodeModules));
|
||||
|
||||
// The stable link is idempotent for concurrent and later local runners.
|
||||
expect(
|
||||
ensureRepoToolNodeModulesLink(primaryTsgo, {
|
||||
cwd,
|
||||
resolveCommonDir: () => commonDir,
|
||||
}),
|
||||
).toBe(localNodeModules);
|
||||
});
|
||||
|
||||
it("leaves existing worktree node_modules directories locally owned", () => {
|
||||
const primaryRoot = createTempDir("openclaw-primary-toolchain-");
|
||||
const commonDir = path.join(primaryRoot, ".git");
|
||||
const primaryTsgo = path.join(primaryRoot, "node_modules", ".bin", "tsgo");
|
||||
const cwd = path.join(primaryRoot, "worktree");
|
||||
const localNodeModules = path.join(cwd, "node_modules");
|
||||
fs.mkdirSync(path.dirname(primaryTsgo), { recursive: true });
|
||||
fs.mkdirSync(localNodeModules, { recursive: true });
|
||||
|
||||
ensureRepoToolNodeModulesLink(primaryTsgo, {
|
||||
cwd,
|
||||
resolveCommonDir: () => commonDir,
|
||||
});
|
||||
|
||||
expect(fs.lstatSync(localNodeModules).isDirectory()).toBe(true);
|
||||
expect(fs.lstatSync(localNodeModules).isSymbolicLink()).toBe(false);
|
||||
});
|
||||
|
||||
it("reenables local heavy-check policy for local wrapper entrypoints", () => {
|
||||
expect(resolveLocalHeavyCheckEnv({ OPENCLAW_LOCAL_CHECK: "0", PATH: "/usr/bin" })).toEqual({
|
||||
OPENCLAW_LOCAL_CHECK: "1",
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
parseMode,
|
||||
resolveBoundaryEntryShimRequiredOutputs,
|
||||
resolveBoundaryRootShimsTimeoutMs,
|
||||
resolveTsxImportSpecifier,
|
||||
runNodeStep,
|
||||
runNodeSteps,
|
||||
runNodeStepsInParallel,
|
||||
@@ -100,6 +101,33 @@ async function waitForProcessExit(
|
||||
}
|
||||
|
||||
describe("prepare-extension-package-boundary-artifacts", () => {
|
||||
it("resolves the tsx loader from the selected checkout toolchain", () => {
|
||||
const tsxBinPath = "/primary/node_modules/.bin/tsx";
|
||||
const loaderPath = "/primary/node_modules/tsx/dist/loader.mjs";
|
||||
|
||||
expect(
|
||||
resolveTsxImportSpecifier({
|
||||
resolveTool: (toolName) => {
|
||||
expect(toolName).toBe("tsx");
|
||||
return tsxBinPath;
|
||||
},
|
||||
ensureToolchain: (toolPath) => {
|
||||
expect(toolPath).toBe(tsxBinPath);
|
||||
return "/worktree/node_modules";
|
||||
},
|
||||
createRequireFrom: (filename) => {
|
||||
expect(filename).toBe(tsxBinPath);
|
||||
return {
|
||||
resolve(packageName) {
|
||||
expect(packageName).toBe("tsx");
|
||||
return loaderPath;
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
).toBe(pathToFileURL(loaderPath).href);
|
||||
});
|
||||
|
||||
it("prefixes each completed line and flushes the trailing partial line", () => {
|
||||
let output = "";
|
||||
const writer = createPrefixedOutputWriter("boundary", {
|
||||
|
||||
@@ -69,9 +69,7 @@ describe("run-oxlint", () => {
|
||||
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mjs", "utf8");
|
||||
|
||||
expect(packageJson.scripts.check).toBe("node scripts/check.mjs");
|
||||
expect(packageJson.scripts.lint).toBe(
|
||||
"pnpm lint:ui:i18n && node scripts/run-oxlint-shards.mjs",
|
||||
);
|
||||
expect(packageJson.scripts.lint).toBe("node scripts/run-lint.mjs");
|
||||
expect(packageJson.scripts["lint:core"]).toBe(
|
||||
"node scripts/run-oxlint-shards.mjs --only=core --split-core",
|
||||
);
|
||||
@@ -82,6 +80,22 @@ describe("run-oxlint", () => {
|
||||
expect(shardedLintRunner).toContain('OPENCLAW_OXLINT_SKIP_PREPARE: "1"');
|
||||
});
|
||||
|
||||
it("prepares the worktree toolchain before the complete lint pre-step", () => {
|
||||
const packageJson = JSON.parse(readFileSync("package.json", "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
const lintRunner = readFileSync("scripts/run-lint.mjs", "utf8");
|
||||
|
||||
expect(packageJson.scripts.lint).toBe("node scripts/run-lint.mjs");
|
||||
expect(lintRunner.indexOf("ensureRepoToolNodeModulesLink(")).toBeGreaterThan(-1);
|
||||
expect(
|
||||
lintRunner.indexOf('path.resolve("scripts", "control-ui-i18n-verify.ts")'),
|
||||
).toBeGreaterThan(lintRunner.indexOf("ensureRepoToolNodeModulesLink("));
|
||||
expect(lintRunner.indexOf('path.resolve("scripts", "run-oxlint-shards.mjs")')).toBeGreaterThan(
|
||||
lintRunner.indexOf('path.resolve("scripts", "control-ui-i18n-verify.ts")'),
|
||||
);
|
||||
});
|
||||
|
||||
it("holds one parent heavy-check lock for sharded lint runs", () => {
|
||||
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mjs", "utf8");
|
||||
const skipLockIndex = shardedLintRunner.indexOf('env.OPENCLAW_OXLINT_SKIP_LOCK === "1"');
|
||||
|
||||
Reference in New Issue
Block a user