/** * Rebuild what the lab runs, from source, before it runs. * * **A stale artifact reporting success against old rules is the fault this project keeps writing * down** (novox/hq 04-ISSUES/005). The lab consumes a handful of artifacts from two repositories, * and they * were rebuilt by hand, one at a time, from memory. A rename in the control plane's catalogue needs * both the control-plane image *and* the builder binary, because both parse manifests; rebuilding * one left a binary eleven hours old refusing a field the mesh had just renamed, and cost a full * run to find out. * * In the repository rather than in a shell script beside it, for the reason 005 is about: a step * that lives in somebody's terminal history runs when they remember, and remembering is not a * mechanism. */ import { spawnSync } from "node:child_process"; import { repositories } from "./repos.ts"; export interface Build { /** What it produces, for the log. */ what: string; /** The repository root to run in. */ in: string; argv: string[]; env?: NodeJS.ProcessEnv; } /** * planned is what must be built, given where this run has been pointed. * * Derived from the same environment the suite is configured by, so there is one place that says * where a repository is. A repository this run was not pointed at is not built — and, per * {@link whatWasTested}, is not claimed in the receipt either. */ export function planned(env: NodeJS.ProcessEnv = process.env): Build[] { const builds: Build[] = []; const where = repositories(env); const host = env["MESH_LAB_HOST_BINARY"]; if (host && where["mesh-host"]) { builds.push({ what: "host", in: where["mesh-host"], argv: ["go", "build", "-ldflags=-s -w -X main.builtFor=arch", "-o", host, "./cmd/mesh-host"], env: { CGO_ENABLED: "0" }, }); } const control = where["mesh-controller"]; if (control) { // **Every image the lab runs, not only the control plane's.** // // On 2026-09-01 a suite ran with a control-plane image built that minute and a provisioner // image built the day before. The rotation test failed against a real database, and the // failure looked exactly like the change under test being wrong — the provisioner was // creating logins by a naming rule that had been replaced. // // This is the same fault the builder line below was added for, one target along. A rebuild // that covers most of what a run uses is worse than one that covers none, because the run // that follows it is believed. builds.push({ what: "images", in: control, argv: ["make", "image", "builder-image", "provisioner-image", "objectstore-image", "redis-provisioner-image", "proxy-image"], }); const builder = env["MESH_LAB_BUILDER"]; if (builder) { // Both of these parse manifests. Building one and not the other is the eleven-hour-old // binary above, so they are one step and not two. builds.push({ what: "builder", in: control, argv: ["go", "build", "-o", builder, "./cmd/mesh-builder"], }); } } // **The installer, carrying the control plane's image.** // // Last, and that is an ordering rather than a preference: `make bootstrap` embeds the output of // `docker save `, so the image has to have been built by the step above or the installer // carries whatever was lying around — the eleven-hour-old artifact again, this time inside a // binary where nothing would ever notice. // // It is built here at all because the bed now bootstraps THROUGH it (novox/hq ADR 0067): the // anchor is brought into existence by running the same program a bare machine runs, rather than // by the bed applying a foundation bundle by hand and calling that an install. An installer that // was stale would be a bed proving something about last week's procedure. const installer = env["MESH_LAB_BOOTSTRAP_BINARY"]; if (installer && where["mesh-host"]) { builds.push({ what: "installer", in: where["mesh-host"], argv: ["make", "bootstrap", `IMAGE=${carriedImage(env)}`, `BOOTSTRAP_OUT=${installer}`], }); } return builds; } /** * The image the installer carries. * * **It is the builder, not the control plane** (novox/hq ADR 0073). The installer used to carry the * thing it was going to run and now carries the thing that makes it, so a raised mesh holds a * control plane it built from a repository and a commit rather than one it was handed. * * `mesh-builder:development` is what mesh-controller's `make builder-image` tags — one tag, said in * one place. Overridable because a release installer carries a release image, and nothing about * that is the lab's business. */ export function carriedImage(env: NodeJS.ProcessEnv = process.env): string { return env["MESH_LAB_CARRIED_IMAGE"] ?? "mesh-builder:development"; } /** rebuild runs the plan, and throws on the first failure rather than testing a stale artifact. */ export function rebuild(env: NodeJS.ProcessEnv = process.env): string[] { const built: string[] = []; for (const build of planned(env)) { const [command, ...args] = build.argv; const ran = spawnSync(command!, args, { cwd: build.in, env: { ...env, ...build.env }, encoding: "utf8", }); if (ran.status !== 0) { // Loudly, and stopping. A suite that runs anyway is a suite reporting on code that is not // the code in front of you, which is the whole of 005. throw new Error( `could not build the ${build.what}: ${build.argv.join(" ")} in ${build.in}\n\n` + `${(ran.stderr || ran.stdout || String(ran.error)).trim()}`, ); } built.push(build.what); } return built; }