mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
166 lines
6.5 KiB
JavaScript
166 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const policyPath = path.join(root, "config/production-license-policy.json");
|
|
const noticesPath = path.join(root, "THIRD_PARTY_NOTICES.md");
|
|
|
|
function packageLabels(entries) {
|
|
return entries
|
|
.flatMap((entry) => {
|
|
const versions = Array.isArray(entry.versions)
|
|
? [...entry.versions].map(String).sort().join(",")
|
|
: "unknown";
|
|
return `${String(entry.name ?? "unnamed")}@${versions}`;
|
|
})
|
|
.sort();
|
|
}
|
|
|
|
export function validateInventory(inventory, policy) {
|
|
const allowed = new Set(policy.allowedExpressions ?? []);
|
|
const denied = new Set(policy.deniedExpressions ?? []);
|
|
const violations = [];
|
|
|
|
for (const expression of Object.keys(inventory).sort()) {
|
|
const entries = Array.isArray(inventory[expression]) ? inventory[expression] : [];
|
|
const packages = packageLabels(entries).join(", ");
|
|
const declaredLicenses = new Set(entries.map((entry) => entry.license));
|
|
if (declaredLicenses.size !== 1 || !declaredLicenses.has(expression)) {
|
|
violations.push(`inventory group ${expression} has inconsistent package license metadata`);
|
|
}
|
|
if (/^(unknown|unlicensed)$/i.test(expression)) {
|
|
violations.push(`unknown license expression ${expression}: ${packages}`);
|
|
} else if (denied.has(expression)) {
|
|
violations.push(`denied license expression ${expression}: ${packages}`);
|
|
} else if (!allowed.has(expression)) {
|
|
violations.push(`unapproved license expression ${expression}: ${packages}`);
|
|
}
|
|
}
|
|
|
|
for (const expression of denied) {
|
|
if (allowed.has(expression)) {
|
|
violations.push(`policy expression is both allowed and denied: ${expression}`);
|
|
}
|
|
}
|
|
return violations.sort();
|
|
}
|
|
|
|
// Native bindings ship one package per platform and arch, so `pnpm licenses
|
|
// list` returns whichever set the current machine installed. Rendering those
|
|
// verbatim makes the notices file describe the developer's laptop: it is
|
|
// regenerated on macOS, then the Linux CI runner resolves the x64 bindings
|
|
// instead and fails the very check that produced it.
|
|
//
|
|
// Collapse each family to its base name for the notices. The policy check above
|
|
// still sees every package the current platform installed, so an unacceptable
|
|
// license in a binding is caught wherever it is installed; only the attribution
|
|
// list is normalized, and the upstream project is still credited once.
|
|
// Three shapes in the wild: a suffix on the package name
|
|
// (@img/sharp-darwin-arm64), the libc glued to the OS (@img/sharp-linuxmusl-arm64),
|
|
// and the whole unscoped name (@esbuild/darwin-arm64), which collapses to the
|
|
// scope alone.
|
|
const PLATFORM_SUFFIX =
|
|
/[-/](?:darwin|linux(?:musl)?|win32|freebsd|openbsd|android|sunos)(?:-(?:x64|arm64|arm|ia32|ppc64|s390x|riscv64|loong64))?(?:-(?:musl|gnu|gnueabihf|msvc))?$/;
|
|
|
|
// Packages that only exist on one platform, rather than shipping a build per
|
|
// platform. fsevents is macOS-only by design and simply absent on Linux, so it
|
|
// cannot be normalized into a shared family and has to be named.
|
|
const PLATFORM_ONLY_PACKAGES = new Set(["fsevents"]);
|
|
|
|
function platformFamily(name) {
|
|
return String(name).replace(PLATFORM_SUFFIX, "");
|
|
}
|
|
|
|
export function renderNotices(inventory) {
|
|
const lines = [
|
|
"# Third-Party Production Node Dependency Notices",
|
|
"",
|
|
"This file is generated from the frozen pnpm production dependency graph.",
|
|
"This inventory covers Node packages only; exact release artifacts publish separate full SBOMs.",
|
|
"Run `pnpm check:production-node-licenses -- --write-notices` after an intentional dependency change.",
|
|
"Package authors retain all rights granted by their respective licenses.",
|
|
"",
|
|
];
|
|
|
|
for (const expression of Object.keys(inventory).sort()) {
|
|
lines.push(`## ${expression}`, "");
|
|
const families = new Map();
|
|
for (const entry of inventory[expression]) {
|
|
if (PLATFORM_ONLY_PACKAGES.has(entry.name)) continue;
|
|
const name = platformFamily(entry.name);
|
|
const family = families.get(name) ?? { name, versions: new Set(), homepage: entry.homepage };
|
|
for (const version of entry.versions) family.versions.add(String(version));
|
|
family.homepage ??= entry.homepage;
|
|
families.set(name, family);
|
|
}
|
|
const entries = [...families.values()].sort((left, right) =>
|
|
left.name.localeCompare(right.name),
|
|
);
|
|
for (const entry of entries) {
|
|
const versions = [...entry.versions].sort().join(",");
|
|
const label = `${entry.name}@${versions}`;
|
|
lines.push(entry.homepage ? `- [${label}](${entry.homepage})` : `- ${label}`);
|
|
}
|
|
lines.push("");
|
|
}
|
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
}
|
|
|
|
function readPolicy() {
|
|
const policy = JSON.parse(readFileSync(policyPath, "utf8"));
|
|
if (
|
|
policy.schemaVersion !== 1 ||
|
|
!Array.isArray(policy.allowedExpressions) ||
|
|
!Array.isArray(policy.deniedExpressions)
|
|
) {
|
|
throw new Error("production license policy has an unsupported schema");
|
|
}
|
|
return policy;
|
|
}
|
|
|
|
function readInventory() {
|
|
return JSON.parse(
|
|
execFileSync("pnpm", ["licenses", "list", "--prod", "--json"], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function main() {
|
|
const policy = readPolicy();
|
|
const inventory = readInventory();
|
|
const violations = validateInventory(inventory, policy);
|
|
if (violations.length > 0) {
|
|
for (const violation of violations) console.error(`ERROR: ${violation}`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const expectedNotices = renderNotices(inventory);
|
|
if (process.argv.includes("--write-notices")) {
|
|
writeFileSync(noticesPath, expectedNotices);
|
|
console.log(`Updated ${path.relative(root, noticesPath)}`);
|
|
return;
|
|
}
|
|
|
|
if (!existsSync(noticesPath) || readFileSync(noticesPath, "utf8") !== expectedNotices) {
|
|
console.error(
|
|
"ERROR: THIRD_PARTY_NOTICES.md is stale; review the dependency change, then run pnpm check:production-node-licenses -- --write-notices",
|
|
);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const packageCount = Object.values(inventory).reduce((sum, entries) => sum + entries.length, 0);
|
|
console.log(
|
|
`Production Node license policy passed: ${packageCount} packages across ${Object.keys(inventory).length} expressions`,
|
|
);
|
|
}
|
|
|
|
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) main();
|