mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* fix(git-hooks): use qualified tooling for missing formatter * test(git-hooks): isolate missing formatter shell from Git wrappers * fix(git-hooks): accept inactive formatter WASI flags * test(git-hooks): brace formatter rejection fixtures
151 lines
7.5 KiB
Bash
Executable file
151 lines
7.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "usage: run-node-tool.sh <tool> [args...]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
tool="$1"
|
|
shift
|
|
|
|
local_tool="$ROOT_DIR/node_modules/.bin/$tool"
|
|
if [[ -x "$local_tool" ]]; then
|
|
exec "$local_tool" "$@"
|
|
fi
|
|
|
|
# Code-only worktrees can use the existing PR tooling owner without linking or
|
|
# installing dependencies. Keep stdin available for index-only formatting.
|
|
if [[ "$tool" == "oxfmt" && -f "$ROOT_DIR/package.json" ]]; then
|
|
exec node --input-type=module -e '
|
|
import { spawnSync } from "node:child_process";
|
|
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
|
const [checkout, ...args] = process.argv.slice(1);
|
|
const gitEnv = { ...process.env };
|
|
for (const key of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE", "GIT_PREFIX", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES"]) delete gitEnv[key];
|
|
function git(root, args, missing = false) {
|
|
const result = spawnSync(process.env.OPENCLAW_PR_GIT || "git", ["-C", root, ...args], {
|
|
env: gitEnv, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
if (missing && result.status === 1) return "";
|
|
if (result.status !== 0) throw new Error("Cannot resolve the repository tooling owner.");
|
|
return result.stdout.trim();
|
|
}
|
|
function repository(url, root) {
|
|
if (/^(?:\.?\.?\/|\/)/.test(url)) return realpathSync(resolve(root, url));
|
|
const parsed = new URL(url.replace(/^git@([^:]+):/, "ssh://git@$1/"));
|
|
if (parsed.protocol === "file:") return realpathSync(decodeURIComponent(parsed.pathname));
|
|
return `${parsed.hostname.toLowerCase()}/${parsed.pathname.replace(/^\/|\/$/g, "").replace(/\.git$/, "").toLowerCase()}`;
|
|
}
|
|
function contained(root, path) {
|
|
const target = realpathSync(path);
|
|
const rel = relative(root, target);
|
|
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) {
|
|
throw new Error("Formatter package escapes its tooling owner.");
|
|
}
|
|
return target;
|
|
}
|
|
try {
|
|
const canonical = realpathSync(dirname(git(checkout, ["rev-parse", "--path-format=absolute", "--git-common-dir"])));
|
|
const requested = process.env.OPENCLAW_PR_TOOLING_ROOT || git(canonical, ["config", "--path", "--get", "openclaw.pr.toolingRoot"], true);
|
|
const root = realpathSync(resolve(canonical, requested || "."));
|
|
if (realpathSync(git(root, ["rev-parse", "--show-toplevel"])) !== root ||
|
|
git(root, ["config", "--bool", "--get", "core.sparseCheckout"], true) === "true" ||
|
|
repository(git(root, ["remote", "get-url", "origin"]), root) !== repository(git(canonical, ["remote", "get-url", "origin"]), canonical)) {
|
|
throw new Error("Formatter tooling owner must be a full checkout of this repository.");
|
|
}
|
|
const taskManifest = readFileSync(join(checkout, "package.json"), "utf8");
|
|
const required = JSON.parse(taskManifest).devDependencies?.oxfmt;
|
|
const modules = contained(root, join(root, "node_modules"));
|
|
const packagePath = contained(modules, join(modules, "oxfmt"));
|
|
const manifestPath = join(packagePath, "package.json");
|
|
const packageManifest = readFileSync(manifestPath, "utf8");
|
|
const installed = JSON.parse(packageManifest);
|
|
if (installed.name !== "oxfmt" || !required || installed.version !== required) {
|
|
throw new Error(`Installed oxfmt does not match this checkout requirement: ${required ?? "undeclared"}.`);
|
|
}
|
|
const bin = contained(packagePath, join(packagePath, typeof installed.bin === "string" ? installed.bin : installed.bin.oxfmt));
|
|
if (!statSync(bin).isFile()) throw new Error("Formatter executable is not a file.");
|
|
const require = createRequire(manifestPath);
|
|
for (const [name, version] of Object.entries(installed.dependencies ?? {})) {
|
|
const dependencyPath = contained(modules, require.resolve(`${name}/package.json`));
|
|
const dependency = JSON.parse(readFileSync(dependencyPath, "utf8"));
|
|
contained(dirname(dependencyPath), require.resolve(name));
|
|
if (dependency.name !== name || dependency.version !== version) {
|
|
throw new Error("Formatter dependency does not match its declared version.");
|
|
}
|
|
}
|
|
let platformFound = false;
|
|
for (const [name, version] of Object.entries(installed.optionalDependencies ?? {})) {
|
|
if (!name.startsWith("@oxfmt/binding-")) continue;
|
|
let nativeManifest;
|
|
try { nativeManifest = require.resolve(`${name}/package.json`); }
|
|
catch (error) { if (error.code === "MODULE_NOT_FOUND") continue; throw error; }
|
|
const native = JSON.parse(readFileSync(nativeManifest, "utf8"));
|
|
if (!native.os?.includes(process.platform) || !native.cpu?.includes(process.arch)) continue;
|
|
const nativeRoot = dirname(contained(modules, nativeManifest));
|
|
const nativeEntry = contained(nativeRoot, join(nativeRoot, native.main));
|
|
if (native.name !== name || native.version !== version || version !== required) {
|
|
throw new Error("Formatter platform package does not match this checkout requirement.");
|
|
}
|
|
// Prove native loading, not a successful version from an unqualified WASI fallback.
|
|
try { require(nativeEntry); platformFound = true; } catch {}
|
|
}
|
|
if (!platformFound) throw new Error("Missing formatter package for this platform.");
|
|
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH ||
|
|
process.env.NAPI_RS_FORCE_WASI === "true" || process.env.NAPI_RS_FORCE_WASI === "error" ||
|
|
process.env.NAPI_RS_WASI_FLAVOR) {
|
|
throw new Error("Cannot qualify an overridden formatter platform binding.");
|
|
}
|
|
const env = { ...process.env, NAPI_RS_ENFORCE_VERSION_CHECK: "1" };
|
|
const version = spawnSync(process.execPath, [bin, "--version"], { env, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
if (version.status !== 0 || version.stdout.trim() !== `Version: ${required}`) {
|
|
throw new Error("Formatter could not verify its version on this platform.");
|
|
}
|
|
// Pin physical package paths and reject drift between admission and execution.
|
|
if (readFileSync(join(checkout, "package.json"), "utf8") !== taskManifest ||
|
|
readFileSync(manifestPath, "utf8") !== packageManifest ||
|
|
realpathSync(join(modules, "oxfmt")) !== packagePath) {
|
|
throw new Error("Formatter inputs changed during qualification; retry with a stable tooling owner.");
|
|
}
|
|
const result = spawnSync(process.execPath, [bin, ...args], { env, stdio: "inherit" });
|
|
if (result.error || result.signal || result.status === null) throw new Error("Formatter did not complete.");
|
|
process.exitCode = result.status;
|
|
} catch (error) {
|
|
console.error(`[pre-commit] Cannot use tooling-owner oxfmt: ${error.message}`);
|
|
console.error("Restore the selected tooling owner through its native dependency workflow; no dependencies were installed.");
|
|
process.exitCode = 1;
|
|
}
|
|
' "$ROOT_DIR" "$@"
|
|
fi
|
|
|
|
if [[ -f "$ROOT_DIR/pnpm-lock.yaml" ]] && command -v pnpm >/dev/null 2>&1; then
|
|
if [[ ! -e "$ROOT_DIR/node_modules" ]]; then
|
|
echo "Missing repo dependencies: cannot run $tool without node_modules." >&2
|
|
echo "Run pnpm install in a normal checkout, or bypass the hook only after separate formatting proof." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Missing local tool: $local_tool" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if { [[ -f "$ROOT_DIR/bun.lockb" ]] || [[ -f "$ROOT_DIR/bun.lock" ]]; } && command -v bun >/dev/null 2>&1; then
|
|
exec bunx --bun "$tool" "$@"
|
|
fi
|
|
|
|
if command -v npm >/dev/null 2>&1; then
|
|
exec npm exec -- "$tool" "$@"
|
|
fi
|
|
|
|
if command -v npx >/dev/null 2>&1; then
|
|
exec npx "$tool" "$@"
|
|
fi
|
|
|
|
echo "Missing package manager: pnpm, bun, or npm required." >&2
|
|
exit 1
|