Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/compatibility.ts

Raw
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { ROOT } from "./build/build.ts";

/**
 * Enforce one source of truth for the minimum supported Pi version:
 * `pi-ext.minimumPi` in the root manifest must equal the lowest Pi version in
 * the release compatibility matrix in `.ci/release.kdl`.
 * Root Pi dependencies stay unpinned; the lockfile records verified versions.
 */
const PI_SCOPE = "@earendil-works/";

function piRanges(deps: Record<string, string> | undefined): string[] {
	return Object.entries(deps ?? {})
		.filter(([name]) => name.startsWith(PI_SCOPE))
		.map(([, range]) => range);
}

function parseMatrixPiAxis(kdl: string): string[] {
	const matrix = /matrix\s*\{([\s\S]*?)\}/.exec(kdl)?.[1] ?? "";
	const line = /\bpi\b([^\n]*)/.exec(matrix)?.[1] ?? "";
	return [...line.matchAll(/"([^"]+)"/g)].map((match) => match[1] as string);
}

function compareSemver(a: string, b: string): number {
	const pa = a.split(".").map(Number);
	const pb = b.split(".").map(Number);
	for (let i = 0; i < 3; i++)
		if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
	return 0;
}

function main(): void {
	const errors: string[] = [];
	const rootPkg = JSON.parse(
		readFileSync(join(ROOT, "package.json"), "utf8"),
	) as {
		"pi-ext"?: { minimumPi?: string };
		devDependencies?: Record<string, string>;
	};
	const minimumPi = rootPkg["pi-ext"]?.minimumPi;
	if (!minimumPi) {
		console.error("root package.json is missing pi-ext.minimumPi");
		process.exit(1);
	}
	for (const range of piRanges(rootPkg.devDependencies))
		if (range !== "*")
			errors.push(`root devDependency Pi range ${range} must be unpinned (*)`);

	const releaseKdl = join(ROOT, ".ci", "release.kdl");
	if (!existsSync(releaseKdl))
		errors.push(".ci/release.kdl is missing (release compatibility matrix)");
	else {
		const axis = parseMatrixPiAxis(readFileSync(releaseKdl, "utf8"));
		if (axis.length === 0)
			errors.push(".ci/release.kdl matrix declares no pi axis");
		else {
			const lowest = [...axis].sort(compareSemver)[0];
			if (lowest !== minimumPi)
				errors.push(
					`release matrix lowest Pi ${lowest} disagrees with declared minimum ${minimumPi}`,
				);
		}
	}

	if (errors.length) {
		for (const error of errors) console.error(`ERROR compatibility: ${error}`);
		process.exit(1);
	}
	console.log(`OK compatibility: minimum Pi ${minimumPi} consistent`);
}

main();