mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 03:01:35 +00:00
* fix(scripts): bound dependency pin git lookups * fix(scripts): address dependency pin review * Update dependency pin helper types * fix(scripts): terminate stalled dependency pin Git commands Bound both dependency-pin Git subprocesses and terminate SIGTERM-resistant children. Preserve sparse-index reads, Windows-native Git, exact timeout errors, and script declaration contracts. Co-authored-by: RileyJJY <0668000974@xydigit.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
193 lines
6.1 KiB
JavaScript
193 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Audits patched dependency pins for exact versions and drift.
|
|
import { execFileSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import YAML from "yaml";
|
|
|
|
const PACKAGE_DEPENDENCY_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
const WORKSPACE_DEPENDENCY_SECTIONS = ["overrides"];
|
|
const EXACT_SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
const EXACT_NPM_ALIAS_PATTERN =
|
|
/^npm:(?:@[^/\s]+\/)?[^@\s]+@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
const PINNED_GIT_PATTERN = /(?:#|\/commit\/)[0-9a-f]{40}$/iu;
|
|
const PINNED_GITHUB_TARBALL_PATTERN =
|
|
/^https:\/\/codeload\.github\.com\/[^/\s]+\/[^/\s]+\/tar\.gz\/[0-9a-f]{40}$/iu;
|
|
const DEFAULT_GIT_TIMEOUT_MS = 60_000;
|
|
|
|
function runGit(cwd, args, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
|
|
try {
|
|
return execFileSync("git", args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
timeout: timeoutMs,
|
|
// A synchronous child that ignores SIGTERM otherwise keeps its parent blocked.
|
|
killSignal: "SIGKILL",
|
|
});
|
|
} catch (error) {
|
|
if (error?.code === "ETIMEDOUT") {
|
|
throw new Error(
|
|
`dependency pin guard: git ${args.join(" ")} timed out after ${timeoutMs}ms.`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function listTrackedPackageJsonFiles(cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
|
|
return runGit(cwd, ["ls-files", "-z", "--", "*package.json"], timeoutMs)
|
|
.split("\0")
|
|
.filter(Boolean)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
}
|
|
|
|
function readTrackedJson(cwd, relativePath, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
|
|
const filePath = path.join(cwd, relativePath);
|
|
if (fs.existsSync(filePath)) {
|
|
return readJson(filePath);
|
|
}
|
|
return JSON.parse(runGit(cwd, ["show", `:${relativePath}`], timeoutMs));
|
|
}
|
|
|
|
function isAllowedPinnedSpec(spec) {
|
|
if (typeof spec !== "string") {
|
|
return false;
|
|
}
|
|
if (EXACT_SEMVER_PATTERN.test(spec) || EXACT_NPM_ALIAS_PATTERN.test(spec)) {
|
|
return true;
|
|
}
|
|
if (spec === "workspace:*" || spec.startsWith("file:") || spec.startsWith("link:")) {
|
|
return true;
|
|
}
|
|
if (/^(?:git\+|github:|gitlab:|bitbucket:)/u.test(spec)) {
|
|
return PINNED_GIT_PATTERN.test(spec);
|
|
}
|
|
if (PINNED_GITHUB_TARBALL_PATTERN.test(spec)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function collectPackageJsonViolations(cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
|
|
const violations = [];
|
|
for (const relativePath of listTrackedPackageJsonFiles(cwd, timeoutMs)) {
|
|
const packageJson = readTrackedJson(cwd, relativePath, timeoutMs);
|
|
for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
|
|
for (const [name, spec] of Object.entries(packageJson[section] ?? {})) {
|
|
if (!isAllowedPinnedSpec(spec)) {
|
|
violations.push({ file: relativePath, section, name, spec });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function collectDependencyMapViolations(file, section, dependencyMap, violations) {
|
|
for (const [name, spec] of Object.entries(dependencyMap ?? {})) {
|
|
if (!isAllowedPinnedSpec(spec)) {
|
|
violations.push({ file, section, name, spec });
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectWorkspaceViolations(cwd) {
|
|
const file = "pnpm-workspace.yaml";
|
|
const workspacePath = path.join(cwd, file);
|
|
if (!fs.existsSync(workspacePath)) {
|
|
return [];
|
|
}
|
|
const workspace = YAML.parse(fs.readFileSync(workspacePath, "utf8"));
|
|
const violations = [];
|
|
for (const section of WORKSPACE_DEPENDENCY_SECTIONS) {
|
|
collectDependencyMapViolations(file, section, workspace?.[section], violations);
|
|
}
|
|
for (const [packageName, extension] of Object.entries(workspace?.packageExtensions ?? {})) {
|
|
collectDependencyMapViolations(
|
|
file,
|
|
`packageExtensions.${packageName}.dependencies`,
|
|
extension?.dependencies,
|
|
violations,
|
|
);
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
/**
|
|
* Collects dependency pin violations for the current workspace.
|
|
*
|
|
* @param {string} [cwd]
|
|
* @param {{ gitTimeoutMs?: number }} [options]
|
|
*/
|
|
export function collectDependencyPinViolations(
|
|
cwd = process.cwd(),
|
|
{ gitTimeoutMs = DEFAULT_GIT_TIMEOUT_MS } = {},
|
|
) {
|
|
return [...collectPackageJsonViolations(cwd, gitTimeoutMs), ...collectWorkspaceViolations(cwd)];
|
|
}
|
|
|
|
/**
|
|
* Builds the full dependency pin audit payload.
|
|
*/
|
|
function collectDependencyPinAudit(cwd = process.cwd()) {
|
|
const packageJsonFiles = listTrackedPackageJsonFiles(cwd);
|
|
let packageSpecCount = 0;
|
|
for (const relativePath of packageJsonFiles) {
|
|
const packageJson = readTrackedJson(cwd, relativePath);
|
|
for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
|
|
packageSpecCount += Object.keys(packageJson[section] ?? {}).length;
|
|
}
|
|
}
|
|
const workspaceViolations = collectWorkspaceViolations(cwd);
|
|
const violations = [...collectPackageJsonViolations(cwd), ...workspaceViolations];
|
|
return {
|
|
packageManifestCount: packageJsonFiles.length,
|
|
packageSpecCount,
|
|
violations,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Runs the dependency pin check.
|
|
*/
|
|
export async function main() {
|
|
const audit = collectDependencyPinAudit();
|
|
const { violations } = audit;
|
|
if (violations.length === 0) {
|
|
process.stdout.write(
|
|
`PASS direct dependency pin guard: checked ${audit.packageSpecCount} directly declared ` +
|
|
`dependency specs across ${audit.packageManifestCount} tracked package manifests; ` +
|
|
"0 violations.\n",
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.error(
|
|
`FAIL direct dependency pin guard: ${violations.length} unpinned directly declared ` +
|
|
"dependency specs found. Direct dependency specs must be pinned exactly outside peer " +
|
|
"dependency contracts:",
|
|
);
|
|
for (const violation of violations) {
|
|
console.error(
|
|
`- ${violation.file}:${violation.section}:${violation.name} -> ${JSON.stringify(violation.spec)}`,
|
|
);
|
|
}
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
main().catch(
|
|
/** @param {unknown} error */ (error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
},
|
|
);
|
|
}
|