Files
OmniRoute/scripts/check-env-doc-sync.mjs
diegosouzapw 4437b0040e docs(docs): add drift detection scripts + minor refinements
Adds three automated drift-detection scripts under scripts/, plus npm
helpers to run them, and applies small follow-ups to TERMUX, I18N, and
CODEBASE docs flagged by the docs audit.

New scripts (scripts/):
- check-env-doc-sync.mjs — cross-checks process.env.X in code vs
  .env.example vs docs/ENVIRONMENT.md. Soft-fails by default; --strict
  exits 1 on drift.
- check-docs-counts-sync.mjs — validates counts (executors, routing
  strategies, OAuth providers, A2A skills, cloud agents) match between
  code and docs.
- check-deprecated-versions.mjs — flags hardcoded stale versions and
  "Last updated" dates older than 60 days. Uses hardcoded regexes to
  satisfy semgrep ReDoS guidance.

package.json:
- New scripts: check:env-doc-sync, check:docs-counts,
  check:deprecated-versions, check:docs-all (umbrella).

Doc refinements:
- TERMUX_GUIDE: spell out the Node version range required by package.json.
- I18N: note that docs/i18n/in/ tree is an orphan duplicate of hi/ that
  the generator no longer writes to; replace hard-coded
  UNTRANSLATABLE_KEYS count with "varies per release".
- CODEBASE_DOCUMENTATION: minor wording.

Pre-commit hook is unchanged — the new checks are heuristic and ship as
on-demand npm scripts to avoid false-positive blocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:47:41 -03:00

132 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
// Validates that env vars referenced in code appear in .env.example AND in docs/ENVIRONMENT.md.
// Exits 0 on success, 1 on missing entries. Designed for use in pre-commit / CI.
//
// Run: node scripts/check-env-doc-sync.mjs
// Strict mode: node scripts/check-env-doc-sync.mjs --strict
// In strict mode, missing entries cause failure. In default mode, only summary is printed.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
const ENV_EXAMPLE = path.join(ROOT, ".env.example");
const ENV_DOC = path.join(ROOT, "docs", "ENVIRONMENT.md");
const STRICT = process.argv.includes("--strict");
// Vars that are intentionally not documented or detected in code via dynamic patterns.
const IGNORE = new Set([
"NODE_ENV",
"PATH",
"HOME",
"USER",
"PWD",
"SHELL",
"TERM",
"TZ",
"LANG",
"LC_ALL",
"CI",
"GITHUB_ACTIONS",
"RUNNER_OS",
// Add false positives here as discovered.
]);
function readEnvExampleVars() {
if (!fs.existsSync(ENV_EXAMPLE)) {
console.error(`${ENV_EXAMPLE} not found`);
process.exit(2);
}
const txt = fs.readFileSync(ENV_EXAMPLE, "utf8");
const vars = new Set();
for (const line of txt.split("\n")) {
// Match both "VAR=value" and "# VAR=value" (commented-out examples are still documented)
const m = line.match(/^#?\s*([A-Z][A-Z0-9_]+)\s*=/);
if (m) vars.add(m[1]);
}
return vars;
}
function readEnvDocVars() {
if (!fs.existsSync(ENV_DOC)) {
console.error(`${ENV_DOC} not found`);
process.exit(2);
}
const txt = fs.readFileSync(ENV_DOC, "utf8");
const vars = new Set();
// Match `VAR_NAME` in inline code or table cells.
for (const m of txt.matchAll(/`([A-Z][A-Z0-9_]{2,})`/g)) {
vars.add(m[1]);
}
return vars;
}
function readCodeVars() {
const vars = new Set();
let stdout;
try {
stdout = execSync(
"grep -rhoE 'process\\.env\\.[A-Z][A-Z0-9_]+' src/ open-sse/ bin/ scripts/ 2>/dev/null || true",
{ cwd: ROOT, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }
);
} catch (e) {
console.error(`✗ grep failed: ${e.message}`);
process.exit(2);
}
for (const line of stdout.split("\n")) {
const m = line.match(/^process\.env\.([A-Z][A-Z0-9_]+)$/);
if (m && !IGNORE.has(m[1])) vars.add(m[1]);
}
return vars;
}
function diff(set, against) {
return [...set].filter((v) => !against.has(v)).sort();
}
function main() {
const codeVars = readCodeVars();
const exampleVars = readEnvExampleVars();
const docVars = readEnvDocVars();
const inCodeMissingExample = diff(codeVars, exampleVars);
const inCodeMissingDoc = diff(codeVars, docVars);
const inExampleMissingDoc = diff(exampleVars, docVars);
const inExampleMissingCode = diff(exampleVars, codeVars);
console.log("Env var sync report");
console.log("===================");
console.log(`Code references: ${codeVars.size} unique vars`);
console.log(`In .env.example: ${exampleVars.size} unique vars`);
console.log(`In docs/ENVIRONMENT.md: ${docVars.size} unique vars (heuristic)`);
console.log();
function printList(label, list) {
if (list.length === 0) {
console.log(`${label}: none`);
} else {
console.log(`${label}: ${list.length}`);
for (const v of list.slice(0, 30)) console.log(` - ${v}`);
if (list.length > 30) console.log(` ... and ${list.length - 30} more`);
}
}
printList("In code but missing from .env.example", inCodeMissingExample);
printList("In code but missing from ENVIRONMENT.md", inCodeMissingDoc);
printList("In .env.example but missing from ENVIRONMENT.md", inExampleMissingDoc);
printList("In .env.example but not referenced in code (dead?)", inExampleMissingCode);
const errors = inCodeMissingExample.length + inExampleMissingDoc.length;
if (STRICT && errors > 0) {
console.error(`\n${errors} drift(s) detected (strict mode)`);
process.exit(1);
}
console.log(`\n${errors === 0 ? "✓" : "⚠"} Done.`);
}
main();