Files
mesh-lab/src/runtimes.ts
T
jschoubben d7959001dd The run rebuilds the module runtimes its beds stock
A per-module bed stocks mesh-runtime-<module>:development from the workstation's
image store, built by hand by a script the suite never called; six were two weeks
older than the manifests they served. For the beds named, every runtime their
scenarios stock is compared against the module's source and the tool runtime and
SDK it is built on, and rebuilt where older, missing or uncommitted; a failed
build stops the suite (novox/hq 04-ISSUES/075).
2026-09-21 19:26:59 +02:00

145 lines
6.2 KiB
TypeScript

/**
* The module runtimes a run stocks are rebuilt by the run, like everything else it tests.
*
* A per-module bed stocks the module's runtime image — the tool runtime carrying that module's
* code — from the workstation's image store, by tag. It was built by hand, by a script the suite
* never called, and on the day this was written the images for six modules about to run dated
* from two weeks before the manifests they were installed with (novox/hq 04-ISSUES/075). The
* suite's own rule, *the run rebuilds what it tests*, was held for the host and the control plane
* and not for these.
*
* So: for the beds about to run, every `mesh-runtime-<module>:development` their scenarios stock
* is compared against the source it is built from — the module in the catalogue, and the tool
* runtime and SDK it is built on — and rebuilt where the image is older, missing, or the source is
* uncommitted. A rebuild that fails stops the suite, the way a stale host binary would.
*/
import { readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { parse } from "yaml";
import type { Build } from "./rebuild.ts";
import { catalogueRoot } from "./repos.ts";
/** A runtime image a scenario stocks by tag, and the module it is built from. */
export interface StockedRuntime {
tag: string;
module: string;
}
/**
* Tags whose name is not the module's. The audit logger's runtime was the first, built before the
* script was generalised, and the scenario still stocks it under the module's slug.
*/
const NAMED_OTHERWISE: Record<string, string> = { "mesh-runtime-audit": "audit-logger" };
const RUNTIME_TAG = /^(mesh-runtime-[a-z0-9-]+):development$/;
/** The runtimes the scenarios of these beds stock, each named once. */
export function runtimesStockedBy(testFiles: string[], root: string = process.cwd()): StockedRuntime[] {
const seen = new Map<string, StockedRuntime>();
for (const file of testFiles) {
const text = readFileSync(resolve(root, file), "utf8");
const named = /const SCENARIO = "([^"]+)"/.exec(text);
if (!named) continue;
const scenario = parse(readFileSync(join(root, "scenarios", `${named[1]}.yml`), "utf8")) as { images?: unknown };
for (const image of Array.isArray(scenario.images) ? scenario.images : []) {
const m = RUNTIME_TAG.exec(String(image));
if (!m) continue;
const repository = m[1]!;
seen.set(repository, { tag: String(image), module: NAMED_OTHERWISE[repository] ?? repository.slice("mesh-runtime-".length) });
}
}
return [...seen.values()];
}
/** When an image was made and when its source last changed, in seconds; null for no image. */
export interface Ages {
image: number | null;
/** Infinity where the source has uncommitted changes: what is on disk is newer than any commit. */
source: number;
}
/** stale is whether the image predates its source, or is not there at all. */
export function isStale(a: Ages): boolean {
return a.image === null || a.image < a.source;
}
/** A command runner, for the two questions asked below; injected so the rules can be tested. */
export type Ask = (command: string, args: string[]) => { status: number | null; stdout: string };
const ask: Ask = (command, args) => {
const ran = spawnSync(command, args, { encoding: "utf8" });
return { status: ran.status, stdout: ran.stdout ?? "" };
};
/** ageOfImage is when the local image store made this tag, or null when it holds no such image. */
export function ageOfImage(tag: string, run: Ask = ask): number | null {
const ran = run("docker", ["image", "inspect", "--format", "{{.Created}}", tag]);
if (ran.status !== 0) return null;
const at = Date.parse(ran.stdout.trim());
return Number.isNaN(at) ? null : Math.floor(at / 1000);
}
/**
* ageOfSource is the newest commit touching what the runtime is built from: the module's directory
* in the catalogue, and the whole of each repository it is built on top of. Uncommitted changes in
* any of them are newer than every commit.
*/
export function ageOfSource(catalogue: string, module: string, builtOn: string[], run: Ask = ask): number {
let newest = 0;
const at = (dir: string, path?: string): number => {
const args = ["-C", dir, "log", "-1", "--format=%ct"];
if (path) args.push("--", path);
const ran = run("git", args);
const t = Number.parseInt(ran.stdout.trim(), 10);
return ran.status === 0 && Number.isFinite(t) ? t : 0;
};
const dirty = (dir: string, path?: string): boolean => {
const args = ["-C", dir, "status", "--porcelain"];
if (path) args.push("--", path);
const ran = run("git", args);
return ran.status === 0 && ran.stdout.trim() !== "";
};
if (dirty(catalogue, `modules/${module}`)) return Infinity;
newest = Math.max(newest, at(catalogue, `modules/${module}`));
for (const dir of builtOn) {
if (dirty(dir)) return Infinity;
newest = Math.max(newest, at(dir));
}
return newest;
}
/**
* plannedRuntimes is the runtime builds these beds need before they run, given where the run was
* pointed: nothing where no catalogue was named (the beds skip), one build per stale image
* otherwise. The repositories the runtime is built on are the siblings the build script itself
* reads (`MESH_TOOLS`, `MESH_SDK`, or the checkouts beside this one).
*/
export function plannedRuntimes(testFiles: string[], env: NodeJS.ProcessEnv = process.env,
root: string = process.cwd(), run: Ask = ask): Build[] {
const named = env["MESH_LAB_CATALOG"];
if (!named) return [];
const catalogue = catalogueRoot(named);
const builtOn = [
env["MESH_TOOLS"] ?? resolve(root, "..", "mesh-tools"),
env["MESH_SDK"] ?? resolve(root, "..", "mesh-sdk"),
];
const builds: Build[] = [];
for (const runtime of runtimesStockedBy(testFiles, root)) {
const ages: Ages = {
image: ageOfImage(runtime.tag, run),
source: ageOfSource(catalogue, runtime.module, builtOn, run),
};
if (!isStale(ages)) continue;
builds.push({
what: `runtime ${runtime.tag}`,
in: root,
argv: ["scripts/build-module-runtime.sh", runtime.module, join(tmpdir(), `mesh-lab-${runtime.module}.tar`)],
env: { MESH_CATALOG: catalogue, RUNTIME_TAG: runtime.tag },
});
}
return builds;
}