openclaw/scripts/check-env-var-count.mts
Peter Steinberger 7cbaa6b36f
refactor(scripts): deslop shared tooling flows (#162824)
Consolidate script lifecycle and helper ownership while preserving CLI, generated-output, cleanup, and guard contracts. Reject inherited object keys in model-matrix option admission with the existing unknown-argument diagnostic.

Validation: 90 full sibling suites; original-fails parser regression; byte-identical native outputs; changed checks; SDK surface and API comparison; both zero-cycle checks; independent isolated review.
2026-10-01 21:30:19 +00:00

175 lines
5.6 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { reportLimitViolations } from "./lib/check-limits.mts";
import {
loadRatchetReference,
loadRatchetSnapshot,
loadRatchetSources,
parseRatchetScalar,
} from "./lib/shrink-ratchet.mts";
const BUDGET_PATH = "config/env-var-count-budget.txt";
const SOURCE_ROOTS = ["src", "packages", "extensions"];
const SOURCE_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
const ENV_VAR_PATTERN = /OPENCLAW_[A-Z0-9_]+/gu;
export function isCountedSourcePath(filePath: string) {
const normalized = filePath.replaceAll("\\", "/");
if (!SOURCE_ROOTS.some((root) => normalized.startsWith(root + "/"))) {
return false;
}
if (!SOURCE_EXTENSIONS.has(path.posix.extname(normalized))) {
return false;
}
if (
/^(?:extensions\/(?:qa-lab|test-support)|.*\/(?:__tests__|test|tests|test-utils|test-support))\//u.test(
normalized,
)
) {
return false;
}
return !/(?:^|[./-])(?:e2e|live-helpers|live-harness|spec|suite|test|test-helpers|test-harness|test-setup|test-support|test-utils)(?:[./-]|$)/u.test(
normalized,
);
}
export type EnvVarNamesByPath = ReadonlyMap<string, ReadonlySet<string>>;
export function addEnvVarNames(source: string, names: Set<string>) {
for (const match of source.matchAll(ENV_VAR_PATTERN)) {
names.add(match[0]);
}
}
export function collectEnvVarNames(
root = process.cwd(),
options: { staged?: boolean; preparedNames?: EnvVarNamesByPath } = {},
) {
const staged = options.staged === true;
const files = execFileSync(
"git",
[
"ls-files",
"-z",
"--cached",
...(staged ? [] : ["--others", "--exclude-standard"]),
"--",
...SOURCE_ROOTS,
],
{ cwd: root, maxBuffer: 256 * 1024 * 1024 },
)
.toString("utf8")
.split("\0")
.filter(isCountedSourcePath)
.filter((file) => staged || fs.existsSync(path.join(root, file)));
const sources = staged ? loadRatchetSources(root, files).values() : files;
const names = new Set<string>();
for (const entry of sources) {
const prepared = staged ? undefined : options.preparedNames?.get(entry);
if (prepared !== undefined) {
for (const name of prepared) {
names.add(name);
}
continue;
}
const source = staged ? entry : fs.readFileSync(path.join(root, entry), "utf8");
addEnvVarNames(source, names);
}
return [...names].toSorted((left, right) => (left < right ? -1 : left > right ? 1 : 0));
}
function parseBudget(source: string) {
return parseRatchetScalar(source, BUDGET_PATH);
}
function readBaseBudget(root: string, ref: string) {
const resolved = spawnSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
cwd: root,
encoding: "utf8",
});
if (resolved.status !== 0) {
throw new Error(`Could not resolve env-var count base ref: ${ref}`);
}
const mergeBase = spawnSync("git", ["merge-base", "HEAD", ref], {
cwd: root,
encoding: "utf8",
});
const baselineRef = mergeBase.stdout.trim();
// Exit 1 with no output is git reporting no shared ancestor; a real failure exits 128.
// Shallow clones and grafted agent checkouts resolve the ref but truncate history, and
// only the growth comparison needs a baseline, so skip it rather than failing the gate.
if (mergeBase.status === 1 && !baselineRef) {
process.stderr.write(
`[env-var-count] ${ref} shares no reachable ancestor here; skipping the base-budget comparison\n`,
);
return null;
}
if (mergeBase.status !== 0 || !baselineRef) {
throw new Error(`Could not resolve env-var count merge base for: ${ref}`);
}
return loadRatchetReference(root, baselineRef, BUDGET_PATH, parseBudget);
}
export function main(
argv: string[] = process.argv.slice(2),
root = process.cwd(),
preparedNames?: EnvVarNamesByPath,
) {
const baseIndex = argv.indexOf("--base");
const baseRef = baseIndex < 0 ? "origin/main" : argv[baseIndex + 1];
const staged = argv.includes("--staged");
const expectedLength = (baseIndex >= 0 ? 2 : 0) + (staged ? 1 : 0);
if (!baseRef || argv.length !== expectedLength) {
throw new Error(
"Usage: node --import tsx scripts/check-env-var-count.mts [--staged] [--base <git-ref>]",
);
}
const budget = loadRatchetSnapshot(root, BUDGET_PATH, staged, parseBudget);
const baseBudget = readBaseBudget(root, baseRef);
const growth =
baseBudget !== null && budget > baseBudget
? [
{
file: BUDGET_PATH,
title: "Environment variable count budget",
message: `OPENCLAW_* budget grew from ${baseBudget} to ${budget}`,
},
]
: [];
if (reportLimitViolations(growth)) {
throw new Error(growth[0]!.message);
}
const names = collectEnvVarNames(root, { staged, preparedNames });
const messages =
names.length === budget
? []
: [
`OPENCLAW_* count ${names.length} ${names.length < budget ? "is below" : "exceeds"} budget ${budget}; update ${BUDGET_PATH}`,
];
if (
reportLimitViolations(
messages.map((message) => ({
file: BUDGET_PATH,
title: "Environment variable count budget",
message,
})),
)
) {
throw new Error(messages.join("\n"));
}
if (messages.length === 0 && growth.length === 0) {
console.log(`OPENCLAW_* count ${names.length}/${budget}`);
}
return names.length;
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}