Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 9d11f5aefeede6e615c3313f6a0d4e035550600d
parent 046047adaaa165d9771ffe2db48e9bc75abf4ea6
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 13:41:57 -0400

docker: build-site.sh syncs the tracked assets into a site's public/ every run — a changed one ships, a removed one stops (review L1, L2)

/site/public persists between container builds, and the no-clobber copy
never refreshed a changed svg and never dropped a removed one: an icon taken
out of the repo (the Ko-fi mark was one, dropped for anonymity) would keep
shipping from every container-built site. sync_public_assets copies each
tracked .svg over whatever is there and removes a name the previous run copied
that the repo no longer has — the names are listed in /site/.tracked-public-
assets, outside public/ so the list never ships, and only those names are ever
removed, so nothing compose wrote is touched.

common/publish/buildImage.test.ts: the function read out of build-site.sh and
run with bash over temp dirs (a changed asset ships, a removed one goes, a
composed file and an unlisted svg stay, a non-svg is never copied); and every
file git tracks in export/public is a top-level .svg — the ignore file and the
sync both assume it, and the failure says to update both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Acommon/publish/buildImage.test.ts | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocker/build-site.sh | 42+++++++++++++++++++++++++++++++-----------
2 files changed, 118 insertions(+), 11 deletions(-)

diff --git a/common/publish/buildImage.test.ts b/common/publish/buildImage.test.ts @@ -0,0 +1,87 @@ +// 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`. No docker here — the invariant is read from git, +// 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"; + +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"), "<svg>v1</svg>"); + writeFileSync(path.join(src, "kofi-symbol.svg"), "<svg>kofi</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"), "<svg>compose</svg>"); + + sync(); + assert.equal(read("globe.svg"), "<svg>v1</svg>"); + assert.equal(read("kofi-symbol.svg"), "<svg>kofi</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. + writeFileSync(path.join(src, "globe.svg"), "<svg>v2</svg>"); + rmSync(path.join(src, "kofi-symbol.svg")); + sync(); + assert.equal(read("globe.svg"), "<svg>v2</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"), "<svg>compose</svg>", "only a name the last run copied is removed"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/docker/build-site.sh b/docker/build-site.sh @@ -40,17 +40,37 @@ rm -rf export/.next # `next build` publishes export/public into out/, so export/public must BE the # public/ compose writes for this site (EXPORT_PUBLIC_DIR=/site/public). It used # to be the copy baked into the image, which shipped as the site's data: none at -# all, or whatever site the image's build context had composed last. The image's -# public/ holds only the repo's tracked assets, all .svg -# (Dockerfile.build.dockerignore); those are copied across first, never over a -# file already there, and nothing else is — whatever else an image's public/ -# held would be data, and not this site's. The export dir was made writable in -# the image so this link (and Next's next-env.d.ts) can be created as an -# arbitrary runtime uid. -for f in export/public/*.svg; do - [ -e "$f" ] || continue - [ -e "/site/public/${f##*/}" ] || cp -a "$f" /site/public/ -done +# all, or whatever site the image's build context had composed last. The export +# dir was made writable in the image so this link (and Next's next-env.d.ts) can +# be created as an arbitrary runtime uid. +# +# The repo's tracked assets in public/ — all .svg (Dockerfile.build.dockerignore; +# common/publish/buildImage.test.ts fails if one is not) — are synced into the +# site's public/ first, on EVERY run, because /site/public persists between runs: +# - each is copied over whatever copy is there, so a changed asset is shipped; +# - one the repo no longer has is removed, so a dropped asset stops shipping. +# Only a name the previous run copied is ever removed — they are listed in +# <list>, outside public/ so the list never ships — so nothing compose wrote +# is touched. +# Nothing but the .svg assets is copied: anything else in an image's public/ +# would be data, and not this site's. +sync_public_assets() { # <image public dir> <site public dir> <list file> + local src="$1" dest="$2" list="$3" f name + if [ -f "$list" ]; then + while IFS= read -r name; do + case "$name" in "" | */* | . | ..) continue ;; esac + if [ ! -e "$src/$name" ] && [ -f "$dest/$name" ]; then rm -f "$dest/$name"; fi + done < "$list" + fi + : > "$list.new" + for f in "$src"/*.svg; do + [ -f "$f" ] || continue + cp -a "$f" "$dest/" + printf '%s\n' "${f##*/}" >> "$list.new" + done + mv "$list.new" "$list" +} +sync_public_assets export/public /site/public /site/.tracked-public-assets rm -rf export/public ln -s /site/public export/public