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
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:
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
// The set of npm packages whose bytes reach web/dist must be exactly the set
|
||||
// recorded in licenses/dependency-identity.txt (milestone-14 ruling 3).
|
||||
//
|
||||
// The shipped build carries no sourcemaps, so this makes a second build with
|
||||
// them into its own directory: the `sources` list of each chunk names the
|
||||
// modules that went into it, and the artifact `npm run build` produced stays
|
||||
// untouched. Runs from web/ as `npm run assert-bundled`, on a laptop exactly as
|
||||
// on the runner.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { bundledPackages, comparePackages, formatDiff, recordedPackages } from "./bundledPackages.mjs";
|
||||
|
||||
const webRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const outDir = "dist-sourcemap";
|
||||
const identityFile = join(webRoot, "..", "licenses", "dependency-identity.txt");
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function mapFiles(relativeDir) {
|
||||
const absolute = join(webRoot, relativeDir);
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(absolute, { withFileTypes: true });
|
||||
} catch (err) {
|
||||
fail(`assert-bundled: cannot read ${relativeDir}: ${err.message}`);
|
||||
}
|
||||
const found = [];
|
||||
for (const entry of entries) {
|
||||
const child = `${relativeDir}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
found.push(...mapFiles(child));
|
||||
} else if (entry.isFile() && entry.name.endsWith(".map")) {
|
||||
found.push(child);
|
||||
}
|
||||
}
|
||||
return found.sort();
|
||||
}
|
||||
|
||||
// The binary npm ci installed, never `npx`: npx silently downloads a package it
|
||||
// cannot find locally, so a wrong working directory would turn a licence check
|
||||
// into an unpinned fetch from the network.
|
||||
try {
|
||||
execFileSync(
|
||||
join(webRoot, "node_modules", ".bin", "vite"),
|
||||
["build", "--sourcemap", "--outDir", outDir, "--emptyOutDir"],
|
||||
{
|
||||
cwd: webRoot,
|
||||
stdio: ["ignore", "ignore", "inherit"],
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
fail(`assert-bundled: the sourcemap build failed: ${err.message}`);
|
||||
}
|
||||
|
||||
const maps = mapFiles(outDir);
|
||||
if (maps.length === 0) fail("assert-bundled: the sourcemap build produced no .map files; this check cannot run blind");
|
||||
|
||||
const sourceLists = maps.map((path) => {
|
||||
const raw = readFileSync(join(webRoot, path), "utf8");
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
fail(`assert-bundled: ${path} is not JSON: ${err.message}`);
|
||||
}
|
||||
return Array.isArray(parsed.sources) ? parsed.sources : [];
|
||||
});
|
||||
|
||||
const bundled = bundledPackages(sourceLists);
|
||||
|
||||
let identity;
|
||||
try {
|
||||
identity = readFileSync(identityFile, "utf8");
|
||||
} catch (err) {
|
||||
fail(`assert-bundled: cannot read licenses/dependency-identity.txt: ${err.message}`);
|
||||
}
|
||||
|
||||
const recorded = recordedPackages(identity);
|
||||
if (recorded === null) {
|
||||
fail("assert-bundled: licenses/dependency-identity.txt has no '[npm packages bundled into web/dist]' section");
|
||||
}
|
||||
if (recorded.length === 0) {
|
||||
fail("assert-bundled: the '[npm packages bundled into web/dist]' section is empty");
|
||||
}
|
||||
|
||||
const { added, removed } = comparePackages(recorded, bundled);
|
||||
if (added.length !== 0 || removed.length !== 0) {
|
||||
process.stderr.write(`${formatDiff(recorded, bundled)}\n\n`);
|
||||
fail(
|
||||
[
|
||||
"the set of npm packages in web/dist has changed (-recorded +current).",
|
||||
"Work out what the change means for licenses/inventory.zon first, then record",
|
||||
"the new list in that section of licenses/dependency-identity.txt.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
process.stdout.write(`web/dist bundles exactly the ${bundled.length} recorded packages:\n`);
|
||||
for (const name of bundled) process.stdout.write(`${name}\n`);
|
||||
Reference in New Issue
Block a user