feat(catalog): verify deterministic release manifests before Git export
CI / check (push) Failing after 22s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped

This commit is contained in:
Simo committed 2026-09-13 17:48:23 +02:00
1 parent 422be3ce2d
commit a320e47c29
9 files changed
+491 -9

No files matched your search

+26 -7
View File
@@ -1,6 +1,14 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import type { RowDataPacket } from "mysql2";
import {
digestFile,
sourceFingerprint,
} from "@/features/catalog/releases/files";
import {
CATALOG_RELEASE_MANIFEST_PATH,
createReleaseManifest,
} from "@/features/catalog/releases/manifest";
import { db } from "@/lib/db";
import { getEffectMapPath } from "@/lib/services/effectmap";
import { getFigureDataPath } from "@/lib/services/figuredata";
@@ -133,16 +141,13 @@ export async function createCatalogSnapshot(
}
const files: CatalogFile[] = [];
const fingerprints = new Map<string, string>();
const fingerprint = async (source: string) => {
const stat = await fs.lstat(source);
if (!stat.isFile() || stat.isSymbolicLink())
throw new Error("Invalid catalog source");
return `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
};
const copiedHashes = new Map<string, string>();
const fingerprint = sourceFingerprint;
for (const [target, source] of sources) {
fingerprints.set(source, await fingerprint(source));
const copy = path.join(directory, String(files.length));
await fs.copyFile(source, copy);
copiedHashes.set(source, (await digestFile(copy)).sha256);
if (target.endsWith(".json")) JSON.parse(await fs.readFile(copy, "utf8"));
files.push({ source: copy, target });
}
@@ -210,8 +215,22 @@ export async function createCatalogSnapshot(
connection.release();
}
for (const [source, stamp] of fingerprints) {
if ((await fingerprint(source)) !== stamp)
if (
(await fingerprint(source)) !== stamp ||
(await digestFile(source)).sha256 !== copiedHashes.get(source)
)
throw new Error("Assets changed during export; retry required");
}
const entries = [];
for (const file of files) {
const { bytes, sha256 } = await digestFile(file.source);
entries.push({ path: file.target, bytes, sha256 });
}
const manifest = createReleaseManifest(entries);
const manifestSource = path.join(directory, "catalog-release.json");
await fs.writeFile(manifestSource, `${JSON.stringify(manifest, null, 2)}\n`, {
flag: "wx",
});
files.push({ source: manifestSource, target: CATALOG_RELEASE_MANIFEST_PATH });
return files;
}