release: move publication orchestration into tools/release.zig, pin rotated subkey
Gates / package (push) Successful in 8m51s
Release / guard (push) Successful in 1m33s
Gates / test-aarch64 (push) Successful in 4m24s
Gates / frontend (push) Successful in 44s
Gates / package (push) Successful in 33s
Gates / container (push) Successful in 35s
Release / gates (push) Successful in 8m24s
Gates / test-aarch64 (push) Successful in 7m56s
Gates / frontend (push) Successful in 1m31s
Gates / test (push) Failing after 16m8s
Gates / container (push) Successful in 5m11s
CI / gates (push) Failing after 30m3s
Gates / test (push) Successful in 2m7s
Release / publish (push) Failing after 10m20s

This commit is contained in:
2026-08-08 20:22:04 +02:00
parent 32cd9b8e3e
commit 266dde7396
9 changed files with 3042 additions and 1202 deletions
+77
View File
@@ -0,0 +1,77 @@
// The decisions behind `npm run assert-bundled`, kept separate from the script
// that does the I/O so they can be unit-tested (milestone-14 deviation 24).
//
// The licence inventory has to cover every package whose bytes ship, and the
// lockfile does not answer that question: it lists what could be reached, not
// what rollup kept. Several packages of the non-dev closure are recorded as
// tree-shaken away, and if application code starts importing one of them, no
// lockfile, no version and no dependency set changes — only the bundle does. So
// the bundle is what this reads.
const sectionHeading = "[npm packages bundled into web/dist]";
// A sourcemap `sources` entry for a dependency ends in
// `node_modules/<name>/<file>` or `node_modules/@<scope>/<name>/<file>`. Only
// the last `node_modules/` matters: a nested dependency's path carries two.
export function packageFromSource(source) {
const marker = "node_modules/";
const at = source.lastIndexOf(marker);
if (at === -1) return null;
const rest = source.slice(at + marker.length);
const parts = rest.split("/");
if (parts.length === 0 || parts[0] === "") return null;
if (parts[0].startsWith("@")) {
if (parts.length < 2 || parts[1] === "") return null;
return `${parts[0]}/${parts[1]}`;
}
return parts[0];
}
/// The sorted, deduplicated package set of a list of sourcemap `sources` arrays.
export function bundledPackages(sourceLists) {
const found = new Set();
for (const sources of sourceLists) {
for (const source of sources) {
const name = packageFromSource(source);
if (name !== null) found.add(name);
}
}
return [...found].sort();
}
/// The recorded section of `licenses/dependency-identity.txt`: every non-blank
/// line after the heading, up to the next `[section]`.
export function recordedPackages(text) {
const recorded = new Set();
let grabbing = false;
for (const raw of text.split("\n")) {
const line = raw.trim();
if (!grabbing) {
if (line === sectionHeading) grabbing = true;
continue;
}
if (line.startsWith("[")) break;
if (line !== "") recorded.add(line);
}
return grabbing ? [...recorded].sort() : null;
}
/// What changed, in the two directions that mean different things: a package
/// that started shipping needs a licence decision, and one that stopped needs
/// the record corrected.
export function comparePackages(recorded, bundled) {
const inBundle = new Set(bundled);
const inRecord = new Set(recorded);
return {
added: bundled.filter((name) => !inRecord.has(name)),
removed: recorded.filter((name) => !inBundle.has(name)),
};
}
export function formatDiff(recorded, bundled) {
const { added, removed } = comparePackages(recorded, bundled);
const lines = [];
for (const name of removed) lines.push(`-${name}`);
for (const name of added) lines.push(`+${name}`);
return lines.join("\n");
}