openclaw/scripts/check-dependency-pins.mts
Peter Steinberger 3cb68020b6
refactor(scripts): deslop shared tooling
## What Problem This Solves

Repository scripts retain private copies of shared tooling helpers and an unused translation subprocess runner, increasing maintenance work across sibling scripts.

## User Impact

Internal tooling cleanup: existing flags, diagnostics, generated bytes, JSON formats, exit codes, and cleanup policies stay the same. The build wrapper additionally detects forbidden dynamic-import warnings when compiler output splits the warning marker across chunks.

## Why This Change Was Made

- Remove the retired i18n process runner. Its final production caller moved to the shared formatter in #95534; retain real CLI/privacy/provider tests and run their subprocesses with the existing dependency.
- Share Docker resource, signal-trap, platform, workspace-staging, and plugin-selection helpers while retaining each caller's environment precedence and lifecycle decisions.
- Reuse existing E2E fixture JSON/assertion and package-path helpers; retain distinct write modes, recovery ordering, and mounted/frozen harness contracts.
- Share macOS guest desktop-user/home resolution through its existing owner while preserving per-caller timeout policy.
- Route release/mobile flags through the existing version flag specifications, preserving split-only values, duplicate/mode rules, help timing, and error text. Reuse existing retry sleep and comparator owners and remove an unreachable iOS output branch.
- Reuse existing guard entrypoint, failure-trailer, diagnostic-line, and metadata-normalization owners.

Measured reduction: **732 net production lines**, with test changes counted separately. PR tooling and its protected import closure, CI planners/shards, baselines, and generated artifacts are unchanged.

## Fixes Found Along the Way

The tsdown scanner checked each raw output chunk for its warning marker. It now checks the existing combined-line buffer. The regression exercises every split inside the marker and fails on the original source for the intended missing-warning assertion.

## Evidence

- Blacksmith Testbox `tbx_01m3tp4sabwd0807kj9jkqqmbr`: frozen dependency install and candidate source-byte verification; all nine changed test files and 59 selected sibling files passed, including real CLI, package fixture, recovery, Docker harness, and Parallels transport contracts.
- Generated channel metadata is byte-identical; differential metadata normalization cases passed.
- SDK surface check passed. Independent isolated Codex review completed with no P0–P2 findings.
- Initial changed checks caught an invalid direct source import in a proposed snapshot-distance consolidation; that independent change was withdrawn. No boundary exception was added. The final changed checks pass, including script/test lint and Docker shell/scheduler checks; madge reports 0 cycles and the runtime import check reports 0 cycles.
- Per-file single-worker wall time for changed suites: translation 9.92s; Docker helper 38.88s; live Docker auth 1.71s; mobile ref 2.36s; mobile release 6.23s; release preparation 2.42s; version 2.04s; installer 15.01s; tsdown 2.70s. The existing Docker helper suite executes shell/process cleanup and container-command boundary fixtures; new assertions reuse those fixtures. New parser/scanner cases use no sleeps or polling.

Hosted CI will be verified against this PR's exact pushed head. No live deployment or release was performed.
2026-10-01 05:14:07 +00:00

154 lines
5.1 KiB
TypeScript

#!/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 { asRecord } from "@openclaw/normalization-core/record-coerce";
import YAML from "yaml";
import { classifyDependencySpec } from "./lib/dependency-spec-policy.mts";
import { runAsScript } from "./lib/ts-guard-utils.mts";
const PACKAGE_DEPENDENCY_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
const DEFAULT_GIT_TIMEOUT_MS = 60_000;
type DependencyPinViolation = {
file: string;
section: string;
name: string;
spec: unknown;
};
function runGit(cwd: string, args: string[], timeoutMs = DEFAULT_GIT_TIMEOUT_MS): string {
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 instanceof Error && "code" in error && error.code === "ETIMEDOUT") {
throw new Error(
`dependency pin guard: git ${args.join(" ")} timed out after ${timeoutMs}ms.`,
{ cause: error },
);
}
throw error;
}
}
function listTrackedPackageJsonFiles(cwd: string, timeoutMs = DEFAULT_GIT_TIMEOUT_MS): string[] {
return runGit(cwd, ["ls-files", "-z", "--", "*package.json"], timeoutMs)
.split("\0")
.filter(Boolean)
.toSorted((left, right) => left.localeCompare(right));
}
function readJson(filePath: string): Record<string, unknown> {
return asRecord(JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown);
}
function readTrackedJson(
cwd: string,
relativePath: string,
timeoutMs = DEFAULT_GIT_TIMEOUT_MS,
): Record<string, unknown> {
const filePath = path.join(cwd, relativePath);
if (fs.existsSync(filePath)) {
return readJson(filePath);
}
return asRecord(JSON.parse(runGit(cwd, ["show", `:${relativePath}`], timeoutMs)) as unknown);
}
function collectPackageJsonAudit(cwd: string, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
const packageJsonFiles = listTrackedPackageJsonFiles(cwd, timeoutMs);
let packageSpecCount = 0;
const violations: DependencyPinViolation[] = [];
for (const relativePath of packageJsonFiles) {
const packageJson = readTrackedJson(cwd, relativePath, timeoutMs);
for (const section of PACKAGE_DEPENDENCY_SECTIONS) {
for (const [name, spec] of Object.entries(asRecord(packageJson[section]))) {
packageSpecCount += 1;
if (!classifyDependencySpec(spec).allowedPinned) {
violations.push({ file: relativePath, section, name, spec });
}
}
}
}
return { packageManifestCount: packageJsonFiles.length, packageSpecCount, violations };
}
function collectDependencyMapViolations(
file: string,
section: string,
dependencyMap: unknown,
violations: DependencyPinViolation[],
): void {
for (const [name, spec] of Object.entries(asRecord(dependencyMap))) {
if (section === "overrides" && spec === "-") {
continue;
}
if (!classifyDependencySpec(spec).allowedPinned) {
violations.push({ file, section, name, spec });
}
}
}
function collectWorkspaceViolations(cwd: string): DependencyPinViolation[] {
const file = "pnpm-workspace.yaml";
const workspacePath = path.join(cwd, file);
if (!fs.existsSync(workspacePath)) {
return [];
}
const workspace = asRecord(YAML.parse(fs.readFileSync(workspacePath, "utf8")) as unknown);
const violations: DependencyPinViolation[] = [];
collectDependencyMapViolations(file, "overrides", workspace.overrides, violations);
for (const [packageName, extension] of Object.entries(asRecord(workspace.packageExtensions))) {
collectDependencyMapViolations(
file,
`packageExtensions.${packageName}.dependencies`,
asRecord(extension).dependencies,
violations,
);
}
return violations;
}
export function collectDependencyPinViolations(
cwd = process.cwd(),
{ gitTimeoutMs = DEFAULT_GIT_TIMEOUT_MS } = {},
): DependencyPinViolation[] {
return [
...collectPackageJsonAudit(cwd, gitTimeoutMs).violations,
...collectWorkspaceViolations(cwd),
];
}
export async function main() {
const audit = collectPackageJsonAudit(process.cwd());
const violations = [...audit.violations, ...collectWorkspaceViolations(process.cwd())];
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;
}
runAsScript(import.meta.url, main);