morphit/apps/ops-cli/scripts/workspace-deps-pin-check.ts

225 lines
6.8 KiB
TypeScript

#!/usr/bin/env tsx
/**
* workspace-deps-pin-check — generalized deps-pin-check
* covering ALL workspaces.
*
* The cp13 deps-pin-check covered only apps/matrix-bot because
* that's where the cp11 lesson came from (matrix-bot-sdk API
* changes between minors). But the pattern applies to every
* workspace. This smoke checks ALL of them.
*
* For each workspace's package.json:
* 1. Read its `dependencies` map.
* 2. For each dep, locate the installed version in node_modules
* (workspace-local node_modules first, then root).
* 3. Verify the installed version satisfies the declared range.
*
* Hard-fails on drift. Soft-skips if node_modules isn't
* populated (CI runner doing static analysis only).
*
* Different from matrix-bot's deps-pin-check.ts (which is
* matrix-bot-specific, narrower in scope, kept for backward
* compatibility + as a more focused per-workspace check).
*/
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
const REPO_ROOT = join(import.meta.dirname, '..', '..', '..');
const ROOT_NODE_MODULES = join(REPO_ROOT, 'node_modules');
function parseVersion(v: string): [number, number, number] | null {
const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
if (!m) return null;
return [Number(m[1]), Number(m[2]), Number(m[3])];
}
function satisfies(range: string, version: string): boolean {
const v = parseVersion(version);
if (!v) return false;
// Strip a leading "v" if any.
const cleaned = range.replace(/^v/, '');
if (cleaned.startsWith('^')) {
const r = parseVersion(cleaned.slice(1));
if (!r) return false;
if (v[0] !== r[0]) return false;
// Pre-1.0 rule: ^0.Y.Z requires same minor.
if (r[0] === 0 && v[1] !== r[1]) return false;
if (v[0] > r[0]) return true;
if (v[1] > r[1]) return true;
if (v[1] === r[1] && v[2] >= r[2]) return true;
return false;
}
if (cleaned.startsWith('~')) {
const r = parseVersion(cleaned.slice(1));
if (!r) return false;
return v[0] === r[0] && v[1] === r[1] && v[2] >= r[2];
}
if (cleaned.startsWith('>=')) {
const r = parseVersion(cleaned.slice(2));
if (!r) return false;
if (v[0] > r[0]) return true;
if (v[0] < r[0]) return false;
if (v[1] > r[1]) return true;
if (v[1] < r[1]) return false;
return v[2] >= r[2];
}
// Wildcards / catch-alls accepted as-is.
if (cleaned === '*' || cleaned === 'latest') return true;
// Workspace protocol — common in monorepos.
if (cleaned.startsWith('workspace:')) return true;
if (cleaned.startsWith('file:') || cleaned.startsWith('link:')) return true;
// Exact version match.
return cleaned === version;
}
interface DepCheck {
readonly workspace: string;
readonly dep: string;
readonly declared: string;
readonly installed: string | null;
readonly ok: boolean;
readonly reason?: string;
}
function findInstalledVersion(depName: string, workspaceDir: string): string | null {
// Check workspace-local node_modules first, then root.
const candidates = [
join(workspaceDir, 'node_modules', depName, 'package.json'),
join(ROOT_NODE_MODULES, depName, 'package.json')
];
for (const p of candidates) {
if (existsSync(p)) {
try {
const pkg = JSON.parse(readFileSync(p, 'utf-8'));
if (typeof pkg.version === 'string') return pkg.version;
} catch {
// fall through
}
}
}
return null;
}
function findWorkspaces(): string[] {
const dirs: string[] = [];
for (const top of ['apps', 'packages']) {
const topDir = join(REPO_ROOT, top);
if (!existsSync(topDir)) continue;
for (const entry of readdirSync(topDir)) {
const wsDir = join(topDir, entry);
if (!statSync(wsDir).isDirectory()) continue;
if (existsSync(join(wsDir, 'package.json'))) dirs.push(wsDir);
}
}
return dirs;
}
const workspaces = findWorkspaces();
// Soft-skip if root node_modules is empty AND no workspace-local
// node_modules exist.
const anyInstalled = existsSync(ROOT_NODE_MODULES) ||
workspaces.some((w) => existsSync(join(w, 'node_modules')));
if (!anyInstalled) {
console.log(
'workspace deps-pin-check: SKIP (no node_modules populated)'
);
console.log(' run `npm ci` to populate, then re-run.');
console.log('');
console.log(
'✓ all 1 workspace-deps-pin scenarios hold (skipped due to env)'
);
process.exit(0);
}
const checks: DepCheck[] = [];
for (const wsDir of workspaces) {
const wsName = wsDir.replace(REPO_ROOT + '/', '');
const pkg = JSON.parse(readFileSync(join(wsDir, 'package.json'), 'utf-8'));
const declaredDeps = (pkg.dependencies ?? {}) as Record<string, string>;
// Internal workspace dependencies (with `workspace:` protocol
// or `file:`/`link:` paths) are exempt — they resolve to a
// local checkout and are always "the version on disk".
for (const [dep, range] of Object.entries(declaredDeps)) {
if (range.startsWith('workspace:') || range.startsWith('file:') ||
range.startsWith('link:')) {
checks.push({
workspace: wsName,
dep,
declared: range,
installed: '(workspace local)',
ok: true
});
continue;
}
const installed = findInstalledVersion(dep, wsDir);
if (installed === null) {
// Dep is declared but not installed. This can be
// legitimate if the workspace hasn't been `npm install`-ed
// individually. Mark as a SOFT failure — emit a warning
// per missing dep but don't count toward hard fail unless
// there's actual drift.
checks.push({
workspace: wsName,
dep,
declared: range,
installed: null,
ok: true, // soft pass
reason: 'not installed; nothing to compare'
});
continue;
}
const ok = satisfies(range, installed);
checks.push({
workspace: wsName,
dep,
declared: range,
installed,
ok,
reason: ok ? undefined : `installed ${installed} does NOT satisfy ${range}`
});
}
}
// ─── Report ─────────────────────────────────────────────────────
// Group by workspace for readability.
const byWorkspace = new Map<string, DepCheck[]>();
for (const c of checks) {
if (!byWorkspace.has(c.workspace)) byWorkspace.set(c.workspace, []);
byWorkspace.get(c.workspace)!.push(c);
}
let total = 0;
let failed = 0;
console.log(
`workspace deps-pin-check: ${workspaces.length} workspaces, ${checks.length} deps\n`
);
for (const [ws, wsChecks] of byWorkspace) {
console.log(` ${ws}:`);
for (const c of wsChecks) {
total++;
if (c.ok) {
const shown = c.installed === null
? `${c.dep}@${c.declared} (not installed)`
: `${c.dep}: declared ${c.declared}, installed ${c.installed}`;
console.log(`${shown}`);
} else {
console.log(`${c.dep}: ${c.reason}`);
failed++;
}
}
}
console.log('');
if (failed === 0) {
console.log(`✓ all ${total} workspace-deps-pin checks hold`);
process.exit(0);
}
console.error(`${failed} drifted across workspaces`);
process.exit(1);