Files
openclaw/scripts/check-dependency-pins.mjs
RileyJJY 34b72d1fce fix(scripts): bound dependency pin git lookups (#110570)
* 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>
2026-07-29 13:11:23 -04:00

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);
},
);
}