fix(crabbox): start remote checks from code-only worktrees (#163035)

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Vincent Koc 2026-10-02 11:07:18 +07:00 • committed by GitHub
parent 0fa2dddedb
commit 2e705107ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 244 additions and 39 deletions

View file

@ -59,7 +59,10 @@ without changing the receipt or substituting today's main or the check's base.
`OPENCLAW_PR_TOOLING_ROOT` selects a full checkout of the same repository for
materialized wrappers' third-party dependencies; otherwise `openclaw.pr.toolingRoot` in the
canonical checkout's Git config applies, then the canonical checkout itself.
The standalone CI watcher resolves missing packages from the same tooling root when its checkout has no `node_modules`, with the same explicit-root identity checks and exact package versions.
The standalone CI watcher and Crabbox entrypoint resolve missing third-party
packages from the same tooling root when their checkout has no `node_modules`,
with the same explicit-root identity checks and exact package versions. They
never link an installation or resolve workspace packages from another checkout.
The wrapper still selects and verifies code against the existing trust anchor.
Installed package versions must exactly match the anchor manifest. On mismatch,
an explicitly selected, separate, clean `main` checkout is fetched, fast-forwarded,

View file

@ -5,4 +5,6 @@ await runTsxCliShim(import.meta.url, {
implementation: "./crabbox-wrapper.mts",
detached: process.platform !== "win32",
terminationOwner: "implementation",
toolingDependencies: "crabbox",
failureTool: "crabbox",
});

View file

@ -0,0 +1,5 @@
export function toolingDependencyOptions(
checkout: string,
consumer: string,
options?: { tsx?: boolean },
): { execArgv?: string[]; tsxImport?: string };

View file

@ -1,10 +1,42 @@
import { spawnSync } from "node:child_process";
import { readFileSync, realpathSync, statSync } from "node:fs";
import { registerHooks } from "node:module";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { createRequire, registerHooks } from "node:module";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { resolveConfiguredModulesDir } from "./tsx-cli-shim.mjs";
function contained(root, target) {
const physical = realpathSync(target);
const rel = relative(root, physical);
if (!rel || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute(rel)) {
throw new Error("Tooling package escapes its installed dependency owner.");
}
return physical;
}
function qualifiedPackage(checkout, root, specifier, consumer) {
const name = specifier
.split("/")
.slice(0, specifier.startsWith("@") ? 2 : 1)
.join("/");
const manifest = JSON.parse(readFileSync(join(checkout, "package.json"), "utf8"));
const required =
manifest.dependencies?.[name] ??
manifest.devDependencies?.[name] ??
manifest.optionalDependencies?.[name];
const modules = realpathSync(join(root, "node_modules"));
// A workspace link would execute another checkout's source. Only installed
// third-party packages can fill missing dependencies in this checkout.
const directory = contained(modules, join(modules, name));
const installed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
if (!required || installed.name !== name || installed.version !== required) {
throw new Error(
`Installed ${consumer} dependency '${name}' has version ${installed.version}; this checkout requires ${required ?? "undeclared"}. Tooling root: ${root}. Refresh that tooling root with pnpm install --frozen-lockfile in a clean main checkout or install dependencies locally.`,
);
}
return directory;
}
// Keep repository identity aligned with scripts/pr's tooling-root bootstrap.
function repository(url, root) {
if (/^(?:\.?\.?\/|\/)/.test(url)) {
@ -20,7 +52,7 @@ function repository(url, root) {
.toLowerCase()}`;
}
export function watchPrCiDependencyOptions(checkout) {
export function toolingDependencyOptions(checkout, consumer, { tsx = false } = {}) {
// A configured pnpm modules directory keeps the shim's existing link contract.
if (
statSync(join(checkout, "node_modules"), { throwIfNoEntry: false })?.isDirectory() ||
@ -93,15 +125,23 @@ export function watchPrCiDependencyOptions(checkout) {
const hook = new URL(import.meta.url);
hook.searchParams.set("root", root);
hook.searchParams.set("checkout", checkout);
console.error(`[watch-pr-ci] resolving missing packages from scripts/pr tooling root ${root}`);
hook.searchParams.set("consumer", consumer);
let tsxImport;
if (tsx) {
const directory = qualifiedPackage(checkout, root, "tsx/esm", consumer);
const require = createRequire(join(directory, "package.json"));
tsxImport = pathToFileURL(contained(directory, require.resolve("tsx/esm"))).href;
}
console.error(`[${consumer}] resolving missing packages from scripts/pr tooling root ${root}`);
// A node_modules link would change scripts/pr's wrapper selection in this checkout.
return { execArgv: ["--import", hook.href] };
return { execArgv: ["--import", hook.href], ...(tsxImport ? { tsxImport } : {}) };
}
const params = new URL(import.meta.url).searchParams;
const root = params.get("root");
if (root) {
const checkout = params.get("checkout");
const consumer = params.get("consumer");
const parentURL = pathToFileURL(join(root, "package.json")).href;
registerHooks({
resolve(specifier, context, nextResolve) {
@ -118,23 +158,8 @@ if (root) {
}
resolved = nextResolve(specifier, { ...context, parentURL });
}
const name = specifier
.split("/")
.slice(0, specifier.startsWith("@") ? 2 : 1)
.join("/");
const manifest = JSON.parse(readFileSync(join(checkout, "package.json"), "utf8"));
const requiredVersion =
manifest.dependencies?.[name] ??
manifest.devDependencies?.[name] ??
manifest.optionalDependencies?.[name];
const installedVersion = JSON.parse(
readFileSync(join(root, "node_modules", name, "package.json"), "utf8"),
).version;
if (!requiredVersion || installedVersion !== requiredVersion) {
throw new Error(
`Installed watch-pr-ci dependency '${name}' has version ${installedVersion}; this checkout requires ${requiredVersion ?? "undeclared"}. Tooling root: ${root}. Refresh that tooling root with pnpm install --frozen-lockfile in a clean main checkout or install dependencies locally.`,
);
}
const directory = qualifiedPackage(checkout, root, specifier, consumer);
contained(directory, fileURLToPath(resolved.url));
return resolved;
},
});

View file

@ -9,9 +9,11 @@ export type CliShimOptions = {
forceKillDelayMs?: number;
stdio?: StdioOptions;
terminationOwner?: "implementation";
toolingDependencies?: string;
};
export function resolveForwardedNodeCompilerArgs(execArgv?: readonly string[]): string[];
export function resolveConfiguredModulesDir(checkoutRoot: string): string | undefined;
export function resolveTsxImport(checkoutRoot: string): string;
export function registerToolingTsx(): Promise<void>;
export function runNodeCliShim(moduleUrl: string | URL, options: CliShimOptions): Promise<void>;

View file

@ -54,7 +54,7 @@ export function resolveTsxImport(checkoutRoot) {
);
}
export async function registerToolingTsx() {
function configureToolingTsx() {
// tsx indexes the entire shared disk cache before expiration, coupling startup
// to other checkouts' cache size. This flag retains its in-process Map and
// reaches descendant tooling before their loaders initialize.
@ -70,6 +70,10 @@ export async function registerToolingTsx() {
) {
process.env.TSX_TSCONFIG_PATH = checkoutTsconfig;
}
}
export async function registerToolingTsx() {
configureToolingTsx();
await import(resolveTsxImport(SHIM_CHECKOUT_ROOT));
}
@ -202,6 +206,25 @@ export function runNodeCliShim(moduleUrl, options = {}) {
return runCliShim(moduleUrl, options, []);
}
export function runTsxCliShim(moduleUrl, options = {}) {
export async function runTsxCliShim(moduleUrl, options = {}) {
if (options.toolingDependencies) {
try {
const { toolingDependencyOptions } = await import("./tooling-dependencies.mjs");
const tooling = toolingDependencyOptions(SHIM_CHECKOUT_ROOT, options.toolingDependencies, {
tsx: true,
});
if (tooling.tsxImport) {
configureToolingTsx();
// Install qualified resolution before TSX loads source imports. Keeping
// its absolute preload also preserves fork() without a workspace link.
return runCliShim(moduleUrl, options, [...tooling.execArgv, "--import", tooling.tsxImport]);
}
} catch (error) {
console.error(error);
writeFailureTrailer(options.failureTool, 1);
process.exitCode = 1;
return;
}
}
return runCliShim(moduleUrl, options, ["--import", new URL("../tsx.mjs", import.meta.url).href]);
}

View file

@ -177,7 +177,7 @@ scripts/lib/vitest-worker-artifacts.mts
scripts/lib/vitest-worker-cache-policy.mts
scripts/lib/vitest-worker-declarations.mts
scripts/lib/vitest-worker-run.mts
scripts/lib/watch-pr-ci-dependencies.mjs
scripts/lib/tooling-dependencies.mjs
scripts/lib/watch-pr-ci-rollup.mts
scripts/lib/windows-taskkill.mjs
scripts/pnpm-runner.mts

View file

@ -1,11 +1,11 @@
#!/usr/bin/env node
import { fileURLToPath } from "node:url";
import { toolingDependencyOptions } from "./lib/tooling-dependencies.mjs";
import { runNodeCliShim } from "./lib/tsx-cli-shim.mjs";
import { watchPrCiDependencyOptions } from "./lib/watch-pr-ci-dependencies.mjs";
try {
await runNodeCliShim(import.meta.url, {
...watchPrCiDependencyOptions(fileURLToPath(new URL("..", import.meta.url))),
...toolingDependencyOptions(fileURLToPath(new URL("..", import.meta.url)), "watch-pr-ci"),
implementation: "./watch-pr-ci.mts",
// Native PR supervision owns this pipe; the metadata helper forwards it to gh.
// Inheriting only stdin/stdout/stderr would leave its environment flag dangling.

View file

@ -115,6 +115,10 @@ export async function withShimFixture<T>(
"scripts/lib/local-check-runtime.mts",
path.join(checkoutRoot, "scripts", "lib", "local-check-runtime.mts"),
);
copyFileSync(
"scripts/lib/tooling-dependencies.mjs",
path.join(checkoutRoot, "scripts", "lib", "tooling-dependencies.mjs"),
);
writeFileSync(path.join(checkoutRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
outcome = {
value: await run({

View file

@ -0,0 +1,91 @@
import { spawnSync } from "node:child_process";
import { copyFileSync, mkdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
export function createToolingDependencyFixture(root: string) {
const checkout = join(root, "checkout");
const tooling = join(root, "tooling #root");
const lib = join(checkout, "scripts", "lib");
mkdirSync(lib, { recursive: true });
for (const directory of [checkout, tooling]) {
for (const args of [
["init", "--quiet", directory],
["-C", directory, "remote", "add", "origin", "https://github.com/openclaw/openclaw.git"],
]) {
const result = spawnSync("git", args, { encoding: "utf8" });
if (result.status !== 0) {
throw new Error(result.stderr);
}
}
}
for (const name of ["tsx-cli-shim.mjs", "tooling-dependencies.mjs", "local-check-runtime.mts"]) {
copyFileSync(resolve("scripts/lib", name), join(lib, name));
}
for (const name of ["tsx.mjs", "crabbox-wrapper.mjs"]) {
copyFileSync(resolve("scripts", name), join(checkout, "scripts", name));
}
writeFileSync(
join(checkout, "package.json"),
JSON.stringify({ devDependencies: { tsx: "1.0.0", "fixture-pkg": "1.0.0" } }),
);
function writePackage(name: string, source: string, version = "1.0.0") {
const directory = join(tooling, "node_modules", name);
mkdirSync(directory, { recursive: true });
writeFileSync(
join(directory, "package.json"),
JSON.stringify({
name,
version,
type: "module",
exports: name === "tsx" ? { "./esm": "./index.mjs" } : "./index.mjs",
}),
);
writeFileSync(join(directory, "index.mjs"), source);
return directory;
}
// Exercise the preload contract without installing a compiler in the fixture.
writePackage("tsx", 'process.env.TOOLING_FIXTURE_PRELOADED = "1";');
writePackage("fixture-pkg", 'export default "qualified";');
writeFileSync(
join(checkout, "scripts/crabbox-wrapper.mts"),
`import assert from "node:assert/strict";
import value from "fixture-pkg";
assert.equal(value, "qualified");
assert.equal(process.env.TOOLING_FIXTURE_PRELOADED, "1");
assert.equal(process.env.TSX_DISABLE_CACHE, "1");
assert.equal(process.argv[2], "--help");
console.log("qualified bootstrap OK");
`,
);
writeFileSync(
join(checkout, "scripts/ordinary.mjs"),
'import { runTsxCliShim } from "./lib/tsx-cli-shim.mjs"; await runTsxCliShim(import.meta.url, { implementation: "./crabbox-wrapper.mts" });',
);
const env: NodeJS.ProcessEnv = {
...process.env,
NODE_OPTIONS: "",
NODE_PATH: "",
OPENCLAW_PR_TOOLING_ROOT: tooling,
};
for (const name of [
"PNPM_CONFIG_MODULES_DIR",
"pnpm_config_modules_dir",
"npm_config_modules_dir",
"OPENCLAW_PR_GIT",
"TOOLING_FIXTURE_PRELOADED",
]) {
delete env[name];
}
return {
checkout,
tooling,
writePackage,
run: (entrypoint = "crabbox-wrapper.mjs") =>
spawnSync(process.execPath, [join(checkout, "scripts", entrypoint), "--help"], {
cwd: root,
env,
encoding: "utf8",
timeout: 10_000,
}),
};
}

View file

@ -0,0 +1,49 @@
import { existsSync, mkdirSync, renameSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { afterEach, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
import { createToolingDependencyFixture } from "./tooling-dependencies.test-support.mts";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
it("bootstraps the opted-in entrypoint without linking dependencies", () => {
const fixture = createToolingDependencyFixture(tempDirs.make("openclaw-tooling-bootstrap-"));
const result = fixture.run();
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toBe("qualified bootstrap OK\n");
expect(existsSync(join(fixture.checkout, "node_modules"))).toBe(false);
const ordinary = fixture.run("ordinary.mjs");
expect(ordinary.status).toBe(1);
expect(ordinary.stderr).toContain("Repository dependencies are missing");
expect(ordinary.stdout).toBe("");
expect(existsSync(join(fixture.checkout, "node_modules"))).toBe(false);
});
it.each(["tsx", "fixture-pkg"])("rejects stale %s before executing its source", (name) => {
const fixture = createToolingDependencyFixture(tempDirs.make("openclaw-tooling-version-"));
fixture.writePackage(name, 'console.log("STALE PACKAGE EXECUTED");', "0.0.0-stale");
const result = fixture.run();
expect(result.status).toBe(1);
expect(result.stderr).toContain(`'${name}' has version 0.0.0-stale`);
expect(result.stderr).toContain("requires 1.0.0");
expect(result.stderr.trimEnd()).toMatch(/\[crabbox\] FAILED \(exit 1\)$/);
expect(result.stdout + result.stderr).not.toContain("STALE PACKAGE EXECUTED");
expect(existsSync(join(fixture.checkout, "node_modules"))).toBe(false);
});
it.each(["tsx", "fixture-pkg"])("refuses %s linked to another checkout's source", (name) => {
const root = tempDirs.make("openclaw-tooling-workspace-");
const fixture = createToolingDependencyFixture(root);
const workspace = join(root, "workspace");
mkdirSync(workspace);
const installed = join(fixture.tooling, "node_modules", name);
const external = join(workspace, name);
renameSync(installed, external);
symlinkSync(external, installed, process.platform === "win32" ? "junction" : "dir");
const result = fixture.run();
expect(result.status).toBe(1);
expect(result.stderr).toContain("Tooling package escapes its installed dependency owner");
expect(result.stdout).toBe("");
expect(existsSync(join(fixture.checkout, "node_modules"))).toBe(false);
});

View file

@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { afterEach, expect, it, vi } from "vitest";
import { watchPrCiDependencyOptions } from "../../scripts/lib/watch-pr-ci-dependencies.mjs";
import { toolingDependencyOptions } from "../../scripts/lib/tooling-dependencies.mjs";
import { requireNodeTool } from "../helpers/node-toolchain.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
@ -104,7 +104,7 @@ it.each([
} else {
vi.stubEnv("PNPM_CONFIG_MODULES_DIR", join(tooling, "node_modules"));
}
expect(watchPrCiDependencyOptions(checkout)).toEqual({});
expect(toolingDependencyOptions(checkout, "watch-pr-ci")).toEqual({});
expect(notice).not.toHaveBeenCalled();
} else if (["missing", "different", "subdirectory", "sparse"].includes(source)) {
const reasons: Record<string, string> = {
@ -113,10 +113,10 @@ it.each([
subdirectory: "Not a repository top level",
sparse: "Sparse checkout",
};
expect(() => watchPrCiDependencyOptions(checkout)).toThrow(reasons[source]);
expect(() => toolingDependencyOptions(checkout, "watch-pr-ci")).toThrow(reasons[source]);
expect(notice).not.toHaveBeenCalled();
} else {
watchPrCiDependencyOptions(checkout);
toolingDependencyOptions(checkout, "watch-pr-ci");
expect(notice.mock.calls).toEqual([
[
`[watch-pr-ci] resolving missing packages from scripts/pr tooling root ${source === "config" ? tooling : canonical}`,
@ -153,11 +153,7 @@ it("checks fallback versions before loading through the watcher child and preser
optionalDependencies: { "@fixture/scoped": "1.0.0" },
}),
);
for (const file of [
"tsx-cli-shim.mjs",
"local-check-runtime.mts",
"watch-pr-ci-dependencies.mjs",
]) {
for (const file of ["tsx-cli-shim.mjs", "local-check-runtime.mts", "tooling-dependencies.mjs"]) {
copyFileSync(resolve("scripts/lib", file), join(lib, file));
}
copyFileSync(resolve("scripts/watch-pr-ci.mjs"), join(checkout, "scripts/watch-pr-ci.mjs"));
@ -166,7 +162,12 @@ it("checks fallback versions before loading through the watcher child and preser
writePackage(tooling, "@fixture/scoped", "scoped");
writeFileSync(
join(tooling, "node_modules/@fixture/scoped/package.json"),
JSON.stringify({ version: "1.0.0", type: "module", exports: { "./subpath": "./index.mjs" } }),
JSON.stringify({
name: "@fixture/scoped",
version: "1.0.0",
type: "module",
exports: { "./subpath": "./index.mjs" },
}),
);
writePackage(tooling, "local-pkg", "fallback");
writeFileSync(