// 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"), "v1"); writeFileSync(path.join(src, "kofi-symbol.svg"), "kofi"); 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"), "compose"); sync(); assert.equal(read("globe.svg"), "v1"); assert.equal(read("kofi-symbol.svg"), "kofi"); 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"), "v2"); rmSync(path.join(src, "kofi-symbol.svg")); writeFileSync(list, "globe.svg\nkofi-symbol.svg\ncorpus.json\n"); sync(); assert.equal(read("globe.svg"), "v2", "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"), "compose", "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`, ); });