Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157// Fetches a candidate Node release, verifies it, and runs the existing compat checks// against it without moving the pin: surface.mjs record + delta, then golden.mjs// --check. Needs the demo apps' node_modules first (`npm run build && npm run// fixtures`), or golden.mjs crashes on `Cannot find module 'express'`.//// node scripts/node-release-check.mjs [version] [--index=<file>] [--extra=22,26]// [--report=<path>] [--cache=<dir>]// With no version, auto-detects via node-releases.mjs: every tracked major (the// pin's, plus --extra) whose newest LTS release beats the pin. With a version,// checks exactly that one, skipping detection (useful to re-run a known candidate).// Downloads the official tarball for this platform/arch from nodejs.org/dist,// checks it against that release's SHASUMS256.txt, and extracts it under --cache// (default: os.tmpdir()/nib-node-release/<version>).//// Writes a markdown report to --report (default: node-release-report.md next to this// script) and also prints it. Exit code: 1 if any checked release has a non-empty// surface delta or golden diff, 0 if every checked release is clean, 0 ("up to date")// if nothing was newer than the pin.import { createHash } from "node:crypto";import { spawnSync } from "node:child_process";import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join, resolve } from "node:path";import { fileURLToPath } from "node:url";import { PINNED } from "./pinned-node.mjs";import { selectReleases, trackedMajors } from "./node-releases.mjs";
const HERE = fileURLToPath(new URL(".", import.meta.url));const SURFACE = resolve(HERE, "surface.mjs");const GOLDEN = resolve(HERE, "golden.mjs");const DIST = "https://nodejs.org/dist";
function parseArgs(argv) { const flag = (name) => argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3); const version = argv.find((a) => !a.startsWith("--")); return { version, index: flag("index"), extra: flag("extra")?.split(",").filter(Boolean) ?? [], report: flag("report") ?? resolve(HERE, "node-release-report.md"), cache: flag("cache") ?? join(tmpdir(), "nib-node-release"), };}
async function fetchText(url) { const res = await fetch(url); if (!res.ok) throw new Error(`GET ${url}: ${res.status}`); return res.text();}
async function fetchBuffer(url) { const res = await fetch(url); if (!res.ok) throw new Error(`GET ${url}: ${res.status}`); return Buffer.from(await res.arrayBuffer());}
/** Downloads node-v<version>-<platform>-<arch>.tar.gz, checks it against that * release's SHASUMS256.txt, extracts it under cacheDir, and returns the node binary's * path. Skips the download if that binary already exists (the cache is content-addressed * by version). */async function fetchCandidate(version, cacheDir) { const plat = `${process.platform}-${process.arch}`; const name = `node-v${version}-${plat}`; const dest = resolve(cacheDir, version); const bin = resolve(dest, name, "bin", "node"); if (existsSync(bin)) return bin;
mkdirSync(dest, { recursive: true }); const shasums = await fetchText(`${DIST}/v${version}/SHASUMS256.txt`); const file = `${name}.tar.gz`; const want = shasums.split("\n").find((l) => l.endsWith(` ${file}`))?.split(/\s+/)[0]; if (!want) throw new Error(`${file} not listed in v${version}'s SHASUMS256.txt (platform not shipped for this release?)`);
const tarball = await fetchBuffer(`${DIST}/v${version}/${file}`); const got = createHash("sha256").update(tarball).digest("hex"); if (got !== want) throw new Error(`${file}: checksum mismatch (want ${want}, got ${got})`);
const tarPath = resolve(dest, file); writeFileSync(tarPath, tarball); const r = spawnSync("tar", ["xf", tarPath, "-C", dest], { encoding: "utf8" }); if (r.status !== 0) throw new Error(`tar xf ${tarPath}: ${r.stderr}`); if (!existsSync(bin)) throw new Error(`expected ${bin} after extracting ${file}`); return bin;}
function run(cmd, args) { const r = spawnSync(process.execPath, [cmd, ...args], { encoding: "utf8", maxBuffer: 64 << 20 }); return { ...r, output: `${r.stdout ?? ""}${r.stderr ?? ""}` };}
/** Fetches, records, deltas and checks goldens for one candidate. Returns a report * section and whether it found any change. */async function checkRelease(version, cacheDir) { const bin = await fetchCandidate(version, cacheDir); const record = run(SURFACE, ["record", `--node=${bin}`]); if (record.status !== 0) throw new Error(`surface.mjs record --node=${bin}: ${record.output}`); const delta = run(SURFACE, ["delta", PINNED, version]); const golden = run(GOLDEN, ["--check", `--node=${bin}`]);
const deltaEmpty = /^0 added or changed, 0 removed/m.test(delta.output.trim()); const goldenClean = golden.status === 0; const changed = !deltaEmpty || !goldenClean;
const section = [ `## Node ${version}`, "", "### Surface delta", "```", delta.output.trim(), "```", "", "### Golden check", "```", golden.output.trim(), "```", "", changed ? "**Result: changed — triage needed.**" : "**Result: clean.**", ].join("\n"); return { changed, section };}
async function main() { const { version, index, extra, report, cache } = parseArgs(process.argv.slice(2));
const targets = version ? [version] : selectReleases( index ? JSON.parse(readFileSync(index, "utf8")) : await (await fetch("https://nodejs.org/dist/index.json")).json(), PINNED, trackedMajors(PINNED, extra), ) .filter((r) => r.newer && r.latest) .map((r) => r.latest.version);
if (!targets.length) { const text = `# Node release check\n\nPin: ${PINNED}. Nothing newer on a tracked line — up to date.\n`; writeFileSync(report, text); console.log(text); process.exit(0); }
const sections = []; let anyChanged = false; for (const version of targets) { const { changed, section } = await checkRelease(version, cache); anyChanged ||= changed; sections.push(section); }
const text = `# Node release check\n\nPin: ${PINNED}.\n\n${sections.join("\n\n")}\n`; writeFileSync(report, text); console.log(text); process.exit(anyChanged ? 1 : 0);}
if (import.meta.url === `file://${process.argv[1]}`) main();