mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(scripts): resolve worktree implementation dependencies (#130950)
* fix(scripts): resolve worktree implementation dependencies * test(scripts): complete bootstrap fixture and trust closures
This commit is contained in:
committed by
GitHub
parent
14ff866288
commit
8cf6b9e5ab
@@ -12,6 +12,7 @@ on:
|
||||
- scripts/check-docs-mdx.mts
|
||||
- scripts/lib/mintlify-accordion.mjs
|
||||
- scripts/lib/tsx-cli-shim.mjs
|
||||
- scripts/lib/local-check-runtime.mts
|
||||
- .github/workflows/docs-sync-publish.yml
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ on:
|
||||
- "scripts/lib/plugin-npm-package-manifest.mjs"
|
||||
- "scripts/lib/plugin-npm-package-manifest.mts"
|
||||
- "scripts/lib/tsx-cli-shim.mjs"
|
||||
- "scripts/lib/local-check-runtime.mts"
|
||||
- "scripts/lib/plugin-npm-release.ts"
|
||||
- "scripts/lib/plugin-publication-candidates.ts"
|
||||
- "scripts/lib/plugin-publication-collector.ts"
|
||||
|
||||
@@ -33,6 +33,10 @@ const SYNC_SUPPORT_FILES = [
|
||||
source: path.join(ROOT, "scripts", "lib", "tsx-cli-shim.mjs"),
|
||||
target: path.join(".openclaw-sync", "lib", "tsx-cli-shim.mjs"),
|
||||
},
|
||||
{
|
||||
source: path.join(ROOT, "scripts", "lib", "local-check-runtime.mts"),
|
||||
target: path.join(".openclaw-sync", "lib", "local-check-runtime.mts"),
|
||||
},
|
||||
{
|
||||
source: path.join(ROOT, "scripts", "lib", "mintlify-accordion.mjs"),
|
||||
target: path.join(".openclaw-sync", "lib", "mintlify-accordion.mjs"),
|
||||
|
||||
@@ -24,6 +24,10 @@ type RepoToolOptions = {
|
||||
fileExists?: (candidate: string) => boolean;
|
||||
resolveCommonDir?: (cwd: string) => string | null;
|
||||
};
|
||||
type NodeModulesLinkOptions = Pick<RepoToolOptions, "cwd" | "fileExists"> & {
|
||||
symlink?: typeof fs.symlinkSync;
|
||||
platform?: NodeJS.Platform;
|
||||
};
|
||||
|
||||
/** Return whether local check safeguards are enabled for an environment. */
|
||||
export function isLocalCheckEnabled(env: Env) {
|
||||
@@ -81,10 +85,7 @@ export function ensureRepoToolNodeModulesLink(
|
||||
resolveCommonDir = resolveGitCommonDir,
|
||||
symlink = fs.symlinkSync,
|
||||
platform = process.platform,
|
||||
}: RepoToolOptions & {
|
||||
symlink?: typeof fs.symlinkSync;
|
||||
platform?: NodeJS.Platform;
|
||||
} = {},
|
||||
}: RepoToolOptions & NodeModulesLinkOptions = {},
|
||||
) {
|
||||
const localNodeModules = path.resolve(cwd, "node_modules");
|
||||
if (fileExists(localNodeModules)) {
|
||||
@@ -102,10 +103,30 @@ export function ensureRepoToolNodeModulesLink(
|
||||
return null;
|
||||
}
|
||||
|
||||
return ensureRepoNodeModulesLink(primaryNodeModules, { cwd, fileExists, symlink, platform });
|
||||
}
|
||||
|
||||
/** Make selected toolchain packages resolvable from dependency-less source paths. */
|
||||
export function ensureRepoNodeModulesLink(
|
||||
modulesDir: string,
|
||||
{
|
||||
cwd = process.cwd(),
|
||||
fileExists = fs.existsSync,
|
||||
symlink = fs.symlinkSync,
|
||||
platform = process.platform,
|
||||
}: NodeModulesLinkOptions = {},
|
||||
) {
|
||||
const localNodeModules = path.resolve(cwd, "node_modules");
|
||||
if (fileExists(localNodeModules)) {
|
||||
return localNodeModules;
|
||||
}
|
||||
if (!fileExists(modulesDir)) {
|
||||
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");
|
||||
// Keep existing checkout dependencies locally owned; only absent modules
|
||||
// reuse the selected installed toolchain, without reconciling dependencies.
|
||||
symlink(modulesDir, localNodeModules, platform === "win32" ? "junction" : "dir");
|
||||
} catch (error) {
|
||||
// Another local runner may have installed the same stable link concurrently.
|
||||
if (!fileExists(localNodeModules)) {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const PLUGIN_NPM_RELEASE_AUTHORITY_PATHS = [
|
||||
"scripts/generate-npm-package-lock.mjs",
|
||||
"scripts/generate-npm-package-lock.mts",
|
||||
"scripts/lib/actions-artifact-archive.mjs",
|
||||
"scripts/lib/local-check-runtime.mts",
|
||||
"scripts/lib/npm-json-output.mts",
|
||||
"scripts/lib/plugin-npm-package-manifest.mjs",
|
||||
"scripts/lib/plugin-npm-package-manifest.mts",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
|
||||
import { constants as osConstants } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { ensureRepoNodeModulesLink } from "./local-check-runtime.mts";
|
||||
|
||||
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
||||
const DEFAULT_FORCE_KILL_DELAY_MS = 5_000;
|
||||
@@ -40,7 +41,13 @@ function resolveTsxImport(checkoutRoot) {
|
||||
].filter(Boolean)) {
|
||||
try {
|
||||
const require = createRequire(path.join(candidateRoot, "package.json"));
|
||||
return pathToFileURL(require.resolve("tsx")).href;
|
||||
const importUrl = pathToFileURL(require.resolve("tsx")).href;
|
||||
const selectedModulesDir =
|
||||
candidateRoot === hydratedTsxRoot
|
||||
? path.dirname(candidateRoot)
|
||||
: path.join(candidateRoot, "node_modules");
|
||||
ensureRepoNodeModulesLink(selectedModulesDir, { cwd: checkoutRoot });
|
||||
return importUrl;
|
||||
} catch (error) {
|
||||
resolutionError = error;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ pr_wrapper_components=(
|
||||
scripts/lib/plain-gh.mjs
|
||||
scripts/lib/direct-run.mjs
|
||||
scripts/lib/tsx-cli-shim.mjs
|
||||
scripts/lib/local-check-runtime.mts
|
||||
scripts/verify-pr-hosted-gates.mjs
|
||||
scripts/verify-pr-hosted-gates.mts
|
||||
scripts/watch-pr-ci.mjs
|
||||
|
||||
@@ -100,6 +100,13 @@ function writeTsxFixture(modulesDir: string, marker: string) {
|
||||
path.join(packageDir, "loader.mjs"),
|
||||
`process.env.OPENCLAW_TSX_FIXTURE_LOADER = ${JSON.stringify(marker)};\n`,
|
||||
);
|
||||
const dependencyDir = path.join(modulesDir, "shim-dependency");
|
||||
mkdirSync(dependencyDir, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(dependencyDir, "package.json"),
|
||||
JSON.stringify({ name: "shim-dependency", type: "module", exports: "./index.js" }),
|
||||
);
|
||||
writeFileSync(path.join(dependencyDir, "index.js"), 'export const value = "loaded";\n');
|
||||
}
|
||||
|
||||
function runShimFixture(
|
||||
@@ -121,10 +128,14 @@ function runShimFixture(
|
||||
"scripts/lib/tsx-cli-shim.mjs",
|
||||
path.join(checkoutRoot, "scripts", "lib", "tsx-cli-shim.mjs"),
|
||||
);
|
||||
copyFileSync(
|
||||
"scripts/lib/local-check-runtime.mts",
|
||||
path.join(checkoutRoot, "scripts", "lib", "local-check-runtime.mts"),
|
||||
);
|
||||
writeFileSync(path.join(checkoutRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
|
||||
writeFileSync(
|
||||
implementationPath,
|
||||
"process.stdout.write(JSON.stringify({ loader: process.env.OPENCLAW_TSX_FIXTURE_LOADER, args: process.argv.slice(2) }));\n",
|
||||
'import { value } from "shim-dependency";\nprocess.stdout.write(JSON.stringify({ loader: process.env.OPENCLAW_TSX_FIXTURE_LOADER, dependency: value, args: process.argv.slice(2) }));\n',
|
||||
);
|
||||
writeTsxFixture(path.join(checkoutRoot, "node_modules"), "checkout");
|
||||
const modulesEnv = configureModules({ checkoutRoot, fixtureRoot });
|
||||
@@ -149,7 +160,11 @@ function runShimFixture(
|
||||
function expectShimLoader(result: ReturnType<typeof runShimFixture>, loader: string) {
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toEqual({ loader, args: ["--hydrated-proof"] });
|
||||
expect(JSON.parse(result.stdout)).toEqual({
|
||||
loader,
|
||||
dependency: "loaded",
|
||||
args: ["--hydrated-proof"],
|
||||
});
|
||||
}
|
||||
|
||||
describe("script direct-run entrypoints", () => {
|
||||
@@ -196,6 +211,29 @@ describe("script direct-run entrypoints", () => {
|
||||
expectShimLoader(runShimFixture(TSX_SHIM_WRAPPERS[3]), "checkout");
|
||||
});
|
||||
|
||||
it.each(["hydrated", "primary"] as const)(
|
||||
"resolves implementation dependencies from the %s toolchain without local modules",
|
||||
(source) => {
|
||||
const result = runShimFixture(TSX_SHIM_WRAPPERS[0], ({ checkoutRoot, fixtureRoot }) => {
|
||||
rmSync(path.join(checkoutRoot, "node_modules"), { recursive: true });
|
||||
const primaryRoot = path.join(fixtureRoot, "primary");
|
||||
const modulesDir = path.join(primaryRoot, "node_modules");
|
||||
writeTsxFixture(modulesDir, source);
|
||||
if (source === "hydrated") {
|
||||
return { PNPM_CONFIG_MODULES_DIR: modulesDir };
|
||||
}
|
||||
const initialized = spawnSync(
|
||||
"git",
|
||||
["init", "--quiet", "--separate-git-dir", path.join(primaryRoot, ".git"), checkoutRoot],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
expect(initialized.status, initialized.stderr).toBe(0);
|
||||
return {};
|
||||
});
|
||||
expectShimLoader(result, source);
|
||||
},
|
||||
);
|
||||
|
||||
it("matches Windows drive paths case-insensitively", () => {
|
||||
expect(
|
||||
isDirectRunPath(
|
||||
|
||||
@@ -627,6 +627,7 @@ describe("scripts/test-docker-all scheduler", () => {
|
||||
for (const fileName of [
|
||||
"docker-e2e-plan.mts",
|
||||
"docker-e2e-scenarios.mts",
|
||||
"local-check-runtime.mts",
|
||||
"managed-child-process.mts",
|
||||
"official-external-channel-catalog.json",
|
||||
"release-version.mjs",
|
||||
|
||||
@@ -213,6 +213,7 @@ function installPrCliFixture(repoDir: string) {
|
||||
"scripts/lib/plain-gh.mjs",
|
||||
"scripts/lib/direct-run.mjs",
|
||||
"scripts/lib/tsx-cli-shim.mjs",
|
||||
"scripts/lib/local-check-runtime.mts",
|
||||
"scripts/pr-lib/worktree.sh",
|
||||
"scripts/pr-lib/operation-lock.sh",
|
||||
"scripts/pr-lib/process-group-runner.mjs",
|
||||
|
||||
@@ -87,6 +87,10 @@ function makeMismatchedWrapperRepo() {
|
||||
cpSync("scripts/lib/plain-gh.mjs", join(canonical, "scripts", "lib", "plain-gh.mjs"));
|
||||
cpSync("scripts/lib/direct-run.mjs", join(canonical, "scripts", "lib", "direct-run.mjs"));
|
||||
cpSync("scripts/lib/tsx-cli-shim.mjs", join(canonical, "scripts", "lib", "tsx-cli-shim.mjs"));
|
||||
cpSync(
|
||||
"scripts/lib/local-check-runtime.mts",
|
||||
join(canonical, "scripts", "lib", "local-check-runtime.mts"),
|
||||
);
|
||||
writeFileSync(
|
||||
join(canonical, "scripts", "lib", "plain-gh.sh"),
|
||||
"resolve_plain_gh_bin() { printf '/usr/bin/true\\n'; }\ngh_plain() { :; }\n",
|
||||
@@ -438,6 +442,7 @@ describe("scripts/pr wrappers", () => {
|
||||
writeFileSync(join(repo, "scripts", "lib", "plain-gh.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "lib", "direct-run.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "lib", "tsx-cli-shim.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "lib", "local-check-runtime.mts"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "watch-pr-ci.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "watch-pr-ci.mts"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "verify-pr-hosted-gates.mjs"), "// canonical\n");
|
||||
@@ -454,36 +459,23 @@ describe("scripts/pr wrappers", () => {
|
||||
expect(git(repo, ["commit", "-m", "test: canonical wrapper"]).status).toBe(0);
|
||||
expect(git(repo, ["worktree", "add", "-b", "feature", linked]).status).toBe(0);
|
||||
|
||||
writeFileSync(join(linked, "scripts", "pr-lib", "merge.sh"), "# dirty linked\n");
|
||||
const dirtyLinkedResult = spawnSync(join(linked, "scripts", "pr"), ["ls"], {
|
||||
cwd: linked,
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(dirtyLinkedResult.status).toBe(1);
|
||||
expect(dirtyLinkedResult.stderr).toContain("scripts/pr wrapper files have uncommitted changes");
|
||||
expect(git(linked, ["restore", "scripts/pr-lib/merge.sh"]).status).toBe(0);
|
||||
|
||||
writeFileSync(join(linked, "scripts", "watch-pr-ci.mts"), "// dirty watcher\n");
|
||||
const dirtyWatcherResult = spawnSync(join(linked, "scripts", "pr"), ["ls"], {
|
||||
cwd: linked,
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(dirtyWatcherResult.status).toBe(1);
|
||||
expect(dirtyWatcherResult.stderr).toContain(
|
||||
"scripts/pr wrapper files have uncommitted changes",
|
||||
);
|
||||
expect(git(linked, ["restore", "scripts/watch-pr-ci.mts"]).status).toBe(0);
|
||||
|
||||
writeFileSync(join(linked, "scripts", "verify-pr-hosted-gates.mts"), "// dirty verifier\n");
|
||||
const dirtyVerifierResult = spawnSync(join(linked, "scripts", "pr"), ["ls"], {
|
||||
cwd: linked,
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(dirtyVerifierResult.status).toBe(1);
|
||||
expect(dirtyVerifierResult.stderr).toContain(
|
||||
"scripts/pr wrapper files have uncommitted changes",
|
||||
);
|
||||
expect(git(linked, ["restore", "scripts/verify-pr-hosted-gates.mts"]).status).toBe(0);
|
||||
for (const component of [
|
||||
"scripts/pr-lib/merge.sh",
|
||||
"scripts/watch-pr-ci.mts",
|
||||
"scripts/verify-pr-hosted-gates.mts",
|
||||
"scripts/lib/local-check-runtime.mts",
|
||||
]) {
|
||||
writeFileSync(join(linked, component), "# dirty linked\n");
|
||||
const dirtyResult = spawnSync(join(linked, "scripts", "pr"), ["ls"], {
|
||||
cwd: linked,
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(dirtyResult.status, component).toBe(1);
|
||||
expect(dirtyResult.stderr, component).toContain(
|
||||
"scripts/pr wrapper files have uncommitted changes",
|
||||
);
|
||||
expect(git(linked, ["restore", component]).status).toBe(0);
|
||||
}
|
||||
|
||||
// A dirty canonical checkout no longer blocks a linked worktree whose
|
||||
// committed wrapper matches the origin/main trust anchor; without that
|
||||
@@ -499,8 +491,8 @@ describe("scripts/pr wrappers", () => {
|
||||
);
|
||||
expect(git(repo, ["restore", "scripts/pr-lib/merge.sh"]).status).toBe(0);
|
||||
|
||||
writeFileSync(join(linked, "scripts", "pr-lib", "merge.sh"), "# linked\n");
|
||||
expect(git(linked, ["add", "scripts/pr-lib/merge.sh"]).status).toBe(0);
|
||||
writeFileSync(join(linked, "scripts", "lib", "local-check-runtime.mts"), "// linked\n");
|
||||
expect(git(linked, ["add", "scripts/lib/local-check-runtime.mts"]).status).toBe(0);
|
||||
expect(git(linked, ["commit", "-m", "test: linked wrapper"]).status).toBe(0);
|
||||
|
||||
const result = spawnSync(join(linked, "scripts", "pr"), ["ls"], {
|
||||
@@ -513,6 +505,7 @@ describe("scripts/pr wrappers", () => {
|
||||
expect(result.stderr).toContain(
|
||||
"scripts/pr implementation differs between this worktree and the canonical checkout",
|
||||
);
|
||||
expect(result.stderr).toContain("scripts/lib/local-check-runtime.mts");
|
||||
});
|
||||
|
||||
it("runs the local wrapper when it matches origin/main and the canonical checkout is parked elsewhere", () => {
|
||||
@@ -526,6 +519,7 @@ describe("scripts/pr wrappers", () => {
|
||||
writeFileSync(join(repo, "scripts", "lib", "plain-gh.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "lib", "direct-run.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "lib", "tsx-cli-shim.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "lib", "local-check-runtime.mts"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "watch-pr-ci.mjs"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "watch-pr-ci.mts"), "// canonical\n");
|
||||
writeFileSync(join(repo, "scripts", "verify-pr-hosted-gates.mjs"), "// canonical\n");
|
||||
|
||||
@@ -132,6 +132,7 @@ function makeIsolatedPreflightFixture(params: Parameters<typeof makeReleaseFixtu
|
||||
"scripts/windows-cmd-helpers.mjs",
|
||||
"scripts/lib/error-format.mts",
|
||||
"scripts/lib/failed-trailer.mts",
|
||||
"scripts/lib/local-check-runtime.mts",
|
||||
"scripts/lib/managed-child-process.mts",
|
||||
"scripts/lib/release-version.mjs",
|
||||
"scripts/lib/tsx-cli-shim.mjs",
|
||||
|
||||
Reference in New Issue
Block a user