// The container build's contract with the repo: what Dockerfile.build's
// image bakes into export/public and how docker/build-site.sh puts it in front
// of each site's `next build`; and what the runtime image (the root Dockerfile)
// must agree with the code about. No docker here — the invariant is read from
// git or the Dockerfile's text, and the shell function is read out of
// build-site.sh and run with bash.
//
// Run with:
// pnpm --filter yt-dlp-transcript-common test
import { test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { FILTER_REPO_PIPX_SPEC } from "./source";
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
// The site build image bakes only export/public/*.svg (Dockerfile.build.
// dockerignore) and docker/build-site.sh copies only those into each site's
// public/. A tracked asset of any other kind, or in a subdirectory, would be in
// every host build and silently missing from every container build.
test("every tracked file in export/public is a top-level .svg, as the container build assumes", (t) => {
let listed: string;
try {
listed = execFileSync("git", ["-C", REPO, "ls-files", "export/public"], { encoding: "utf8" });
} catch {
t.skip("not a git checkout");
return;
}
const odd = listed.split("\n").filter((f) => f && !/^export\/public\/[^/]+\.svg$/.test(f));
assert.deepEqual(
odd,
[],
`export/public tracks ${odd.join(", ")} — not a top-level .svg. The container build ships only ` +
`those: admit the new asset in Dockerfile.build.dockerignore and copy it in docker/build-site.sh ` +
`(sync_public_assets), or every container-built site goes without it.`,
);
});
// /site/public persists between container builds, so the tracked assets are
// synced into it every run: a changed one must ship, a removed one must stop
// shipping (an icon dropped from the repo — the Ko-fi mark was one — must not
// live on in a site), and nothing compose wrote may be touched. The function is
// read out of build-site.sh itself and run with bash over temp dirs.
test("build-site.sh's asset sync ships a changed svg, drops a removed one, and touches nothing compose wrote", () => {
const script = readFileSync(path.join(REPO, "docker", "build-site.sh"), "utf8");
const fn = /^sync_public_assets\(\) \{[\s\S]*?^\}$/m.exec(script)?.[0];
assert.ok(fn, "docker/build-site.sh defines sync_public_assets()");
const root = mkdtempSync(path.join(tmpdir(), "asset-sync-"));
const src = path.join(root, "image-public");
const dest = path.join(root, "site-public");
const list = path.join(root, ".tracked-public-assets");
const sync = () =>
execFileSync("bash", ["-euo", "pipefail", "-c", `${fn}\nsync_public_assets "$1" "$2" "$3"`, "sync", src, dest, list]);
const read = (p: string) => readFileSync(path.join(dest, p), "utf8");
try {
mkdirSync(src);
mkdirSync(path.join(dest, "transcripts"), { recursive: true });
writeFileSync(path.join(src, "globe.svg"), "");
writeFileSync(path.join(src, "kofi-symbol.svg"), "");
writeFileSync(path.join(src, "stale.json"), "{}"); // not an asset: never copied
// What compose wrote (or any file not copied from the image): never touched.
writeFileSync(path.join(dest, "corpus.json"), '{"site":{"id":"anilyzer"}}');
writeFileSync(path.join(dest, "transcripts", "page-0000.json"), "[]");
writeFileSync(path.join(dest, "composed.svg"), "");
sync();
assert.equal(read("globe.svg"), "");
assert.equal(read("kofi-symbol.svg"), "");
assert.equal(existsSync(path.join(dest, "stale.json")), false);
assert.equal(readFileSync(list, "utf8"), "globe.svg\nkofi-symbol.svg\n");
// The repo changes one asset and drops another. And a list naming a file
// compose writes (a hand-edited or damaged list) must not cost the site it:
// only an .svg is ever removed.
writeFileSync(path.join(src, "globe.svg"), "");
rmSync(path.join(src, "kofi-symbol.svg"));
writeFileSync(list, "globe.svg\nkofi-symbol.svg\ncorpus.json\n");
sync();
assert.equal(read("globe.svg"), "", "a changed asset is shipped");
assert.equal(existsSync(path.join(dest, "kofi-symbol.svg")), false, "a removed asset stops shipping");
assert.equal(readFileSync(list, "utf8"), "globe.svg\n");
assert.equal(read("corpus.json"), '{"site":{"id":"anilyzer"}}');
assert.equal(read("transcripts/page-0000.json"), "[]");
assert.equal(read("composed.svg"), "", "only a name the last run copied is removed");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
// The runtime image (the root Dockerfile) installs git-filter-repo with pipx so
// a container can publish the homepage's /source mirror without a network fetch.
// The version it bakes must be the one `source publish` would fetch with
// `pipx run --spec` on a host (FILTER_REPO_PIPX_SPEC): the filter-repo version
// is part of the publish's skip key, so a drift between the two is a rebuild
// nobody asked for, or a mirror rewritten by a version nobody tested.
test("the runtime image installs the git-filter-repo that source publish names, in every target", () => {
const dockerfile = readFileSync(path.join(REPO, "Dockerfile"), "utf8");
const installs = [...dockerfile.matchAll(/pipx install (\S+)/g)].map((m) => m[1]);
// runtime-base (runtime + runtime-vulkan) and runtime-cuda, which repeats it.
assert.equal(installs.length, 2, `expected two \`pipx install\` lines, found ${installs.length}`);
for (const spec of installs) {
assert.equal(
spec,
FILTER_REPO_PIPX_SPEC,
`the Dockerfile installs ${spec}, source.ts names ${FILTER_REPO_PIPX_SPEC} — move both together`,
);
}
});
// The yt-dlp substitution hook: the image ships its own yt-dlp as
// ARCHILYZER_IMAGE_YTDLP beside YTDLP_BIN (the entrypoint and the doctor tell an
// override by the difference), and links the from-source wrapper — in every
// target.
test("every runtime target names its own yt-dlp and links the from-source wrapper", () => {
const dockerfile = readFileSync(path.join(REPO, "Dockerfile"), "utf8");
const image = [...dockerfile.matchAll(/^\s+ARCHILYZER_IMAGE_YTDLP=(\S+) \\$/gm)].map((m) => m[1]);
const bin = [...dockerfile.matchAll(/^\s+YTDLP_BIN=(\S+) \\$/gm)].map((m) => m[1]);
assert.equal(image.length, 2);
assert.deepEqual(image, bin, "ARCHILYZER_IMAGE_YTDLP and YTDLP_BIN start equal in each target");
const links = dockerfile.match(/ln -s \/repo\/docker\/yt-dlp-from-source\.sh \/usr\/local\/bin\/yt-dlp-from-source/g) ?? [];
assert.equal(links.length, 2);
assert.ok(existsSync(path.join(REPO, "docker", "yt-dlp-from-source.sh")));
});
// Node's major, everywhere the image is built or run: the build stage
// (NODE_IMAGE), the default runtime (RUNTIME_IMAGE), the Vulkan overlay's
// runtime and runtime-cuda's nodesource major. The native modules are compiled
// once against the build stage's ABI, so all of them must agree.
function imageNodeMajors(): Record {
const dockerfile = readFileSync(path.join(REPO, "Dockerfile"), "utf8");
const vulkan = readFileSync(path.join(REPO, "docker-compose.vulkan.yml"), "utf8");
const one = (re: RegExp, text: string, what: string) => {
const m = re.exec(text);
assert.ok(m, `${what} not found`);
return Number(m[1]);
};
const out: Record = {
NODE_IMAGE: one(/^ARG NODE_IMAGE=node:(\d+)-/m, dockerfile, "ARG NODE_IMAGE=node:-…"),
RUNTIME_IMAGE: one(/^ARG RUNTIME_IMAGE=node:(\d+)-/m, dockerfile, "ARG RUNTIME_IMAGE=node:-…"),
NODE_MAJOR: one(/^ARG NODE_MAJOR=(\d+)$/m, dockerfile, "ARG NODE_MAJOR= (runtime-cuda)"),
};
const vk = [...vulkan.matchAll(/RUNTIME_IMAGE: node:(\d+)-/g)].map((m) => Number(m[1]));
assert.ok(vk.length > 0, "docker-compose.vulkan.yml names a RUNTIME_IMAGE");
vk.forEach((v, i) => (out[`vulkan RUNTIME_IMAGE #${i + 1}`] = v));
return out;
}
test("every image target runs the Node major the build stage compiled the native modules for", () => {
const majors = imageNodeMajors();
assert.equal(new Set(Object.values(majors)).size, 1, `one Node major everywhere, found ${JSON.stringify(majors)}`);
});
// Every deploy runs the wrangler pinned in common's devDependencies from this
// image, and wrangler refuses to start below its engines floor (4.x: >=22).
// Skipped where wrangler is not installed.
test("the image's Node major meets the pinned wrangler's engines floor", (t) => {
const pkg = path.join(REPO, "common", "node_modules", "wrangler", "package.json");
if (!existsSync(pkg)) {
t.skip("wrangler is not installed in common/node_modules");
return;
}
const engines = (JSON.parse(readFileSync(pkg, "utf8")) as { engines?: { node?: string } }).engines?.node ?? "";
const floor = /(\d+)/.exec(engines);
assert.ok(floor, `wrangler's engines.node (${JSON.stringify(engines)}) names a major`);
const major = imageNodeMajors().NODE_IMAGE;
assert.ok(
major >= Number(floor[1]),
`the image runs Node ${major}, and wrangler needs ${engines} — every deploy from the container would exit 1`,
);
});