lab: take the forge's owner as a required config value, and name no node in its fixtures (issue 1838)
The lab cloned and installed from the owner this mesh's repositories are under, written in its code; another mesh's forge holds them under its own (1838/AC4). Unset, lab_check, lab_run and a rebuild say so. Fixtures name neutral machines and an example owner.
This commit is contained in:
+23
-13
@@ -8,7 +8,7 @@ import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
checkDir, cleanCredentials, clearChecks, credentialArgs, forgeCredential, installPlan, listRuns, npmRegistry, NPM_REPOSITORIES, publicRootOf, readStatus, REPOSITORIES, running, start,
|
||||
checkDir, repositoryURL, cleanCredentials, clearChecks, credentialArgs, forgeCredential, installPlan, listRuns, npmRegistry, NPM_REPOSITORIES, publicRootOf, readStatus, REPOSITORIES, running, start,
|
||||
stop, tail, writeCredential, baseBuildLog, buildingBase, listBaseBuilds, startBaseBuild, credentialPath, launchRun,
|
||||
launchRebuild, BASE_MAX_AGE_DAYS, describeBase, readBaseImage, baseProblem,
|
||||
} from "./runs.js";
|
||||
@@ -19,13 +19,17 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
// ADR 0192): read from the env-file the mesh fills, at each call, so a changed setting is used
|
||||
// without restarting the runtime. MESH_LAB_FORGE itself still wins, for a hand-run instance.
|
||||
const forgeOf = (): string => (env.MESH_LAB_FORGE ?? wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE")).replace(/\/+$/, "");
|
||||
// The owner on the forge the mesh's repositories and packages are under, the operator's too (novox/hq ADR 0507
|
||||
// §9), read the same way.
|
||||
const ownerOf = (): string => (env.MESH_LAB_FORGE_OWNER ?? wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE_OWNER")).trim();
|
||||
const noOwner = "the lab's forge owner is not set: config for lab, {\"forge-owner\": \"<owner>\"}";
|
||||
// The forge credential the mesh binds and seals to the lab (novox/hq issue 624), read at each call.
|
||||
// The registry's public root the forge serves in the lab's binding (novox/hq ADR 0346), read at each call.
|
||||
const bindingOf = (): string | undefined =>
|
||||
env.MESH_LAB_FORGE_BINDING ?? (wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE_BINDING") || undefined);
|
||||
const registryOf = async (forge: string) => {
|
||||
const root = publicRootOf(bindingOf());
|
||||
const registry = await npmRegistry(forge, fetch, root.root);
|
||||
const registryOf = async (forge: string, owner: string) => {
|
||||
const root = publicRootOf(bindingOf(), owner);
|
||||
const registry = await npmRegistry(forge, owner, fetch, root.root);
|
||||
return registry.problem && root.problem ? { ...registry, problem: `${registry.problem}; ${root.problem}` } : registry;
|
||||
};
|
||||
const credentialOf = (forge: string): string | undefined => forgeCredential(forge,
|
||||
@@ -39,6 +43,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
run: async () => {
|
||||
const forge = forgeOf();
|
||||
if (!forge) return { ok: false, output: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
|
||||
const owner = ownerOf();
|
||||
if (!owner) return { ok: false, output: noOwner };
|
||||
// What earlier checks left is cleared, and what could not be is said; this check works in a fresh
|
||||
// directory of its own either way, and removes it when it is done.
|
||||
const leftovers = clearChecks(work);
|
||||
@@ -46,13 +52,13 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
let check: ReturnType<typeof spawnSync>;
|
||||
let problems: string[];
|
||||
try {
|
||||
const clone = spawnSync("git", ["clone", "--quiet", "--depth", "1", `${forge}/novox/mesh-lab.git`, dir], { encoding: "utf8" });
|
||||
const clone = spawnSync("git", ["clone", "--quiet", "--depth", "1", repositoryURL(forge, owner, "mesh-lab"), dir], { encoding: "utf8" });
|
||||
if (clone.status !== 0) return { ok: false, output: [String(clone.stderr).trim(), ...leftovers].join("\n") };
|
||||
spawnSync("npm", ["ci", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir, encoding: "utf8" });
|
||||
check = spawnSync("node", ["--experimental-strip-types", "src/cli.ts", "check"], { cwd: dir, encoding: "utf8" });
|
||||
problems = setupProblems(forge, `${dir}/setup`, credentialOf(forge));
|
||||
problems = setupProblems(forge, owner, `${dir}/setup`, credentialOf(forge));
|
||||
// And the address a run names the package registry by (npmRegistry): said when the registry does not say it.
|
||||
const registry = await registryOf(forge);
|
||||
const registry = await registryOf(forge, owner);
|
||||
if (registry.problem) problems.push(registry.problem);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
@@ -91,6 +97,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
run: async (args) => {
|
||||
const forge = forgeOf();
|
||||
if (!forge) return { started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
|
||||
const owner = ownerOf();
|
||||
if (!owner) return { started: false, reason: noOwner };
|
||||
const tests = String(args.tests ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||
if (tests.length === 0) return { started: false, reason: "name at least one bed test file" };
|
||||
let refs: Record<string, string> = {};
|
||||
@@ -105,8 +113,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
if (stranger.length > 0) return { started: false, reason: `the lab does not build ${stranger.join(", ")}` };
|
||||
// **One job at a time, not on a base image past its bound, nor while it is rebuilt** (novox/hq issue 698,
|
||||
// review 256): the registry asked first, then the check and the start with nothing awaited between.
|
||||
const launched = await launchRun(work, { registry: () => registryOf(forge), base: () => readBaseImage() },
|
||||
(registry) => start(work, forge, tests, refs, credentialOf(forge), registry));
|
||||
const launched = await launchRun(work, { registry: () => registryOf(forge, owner), base: () => readBaseImage() },
|
||||
(registry) => start(work, forge, owner, tests, refs, credentialOf(forge), registry));
|
||||
if (!launched.started) return launched;
|
||||
return { started: true, run: launched.job, base: describeBase(launched.base), registry: launched.registry,
|
||||
...(launched.problem ? { problem: launched.problem } : {}) };
|
||||
@@ -134,10 +142,12 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
if (args.rebuild !== true) return reading;
|
||||
const forge = forgeOf();
|
||||
if (!forge) return { ...reading, started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
|
||||
const owner = ownerOf();
|
||||
if (!owner) return { ...reading, started: false, reason: noOwner };
|
||||
// Not under a run, nor beside another rebuild: the registry asked first, then the check and the start with
|
||||
// nothing awaited between (review 256).
|
||||
const launched = await launchRebuild(work, { registry: () => registryOf(forge), base: () => before },
|
||||
(registry) => startBaseBuild(work, forge, credentialOf(forge), registry));
|
||||
const launched = await launchRebuild(work, { registry: () => registryOf(forge, owner), base: () => before },
|
||||
(registry) => startBaseBuild(work, forge, owner, credentialOf(forge), registry));
|
||||
if (launched.started === false) return { ...reading, started: false, reason: launched.reason };
|
||||
return { ...reading, started: true, build: launched.job };
|
||||
},
|
||||
@@ -174,7 +184,7 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
* answer for, and a checkout it installs with nothing installable (installPlan). Empty when a run's setup
|
||||
* would go through.
|
||||
*/
|
||||
export function setupProblems(forge: string, dir: string, credential?: string): string[] {
|
||||
export function setupProblems(forge: string, owner: string, dir: string, credential?: string): string[] {
|
||||
const problems: string[] = [];
|
||||
spawnSync("rm", ["-rf", dir]);
|
||||
// The credential a run would offer, in a file of its own outside the check's clones, removed after.
|
||||
@@ -186,7 +196,7 @@ export function setupProblems(forge: string, dir: string, credential?: string):
|
||||
});
|
||||
try {
|
||||
for (const repo of REPOSITORIES) {
|
||||
const url = `${forge}/novox/${repo}.git`;
|
||||
const url = repositoryURL(forge, owner, repo);
|
||||
const remote = git(["ls-remote", "--heads", url, "main"]);
|
||||
if (remote.status !== 0 || !remote.stdout.includes("refs/heads/main")) {
|
||||
const said = remote.stderr.trim().split("\n")[0] ?? "";
|
||||
|
||||
+22
-13
@@ -60,8 +60,11 @@ export function installStep(root: string, build: boolean): string {
|
||||
fi`;
|
||||
}
|
||||
|
||||
/** Where the forge serves the mesh's npm packages, under its own address (gitea's npm registry for the novox org). */
|
||||
export const NPM_PATH = "/api/packages/novox/npm/";
|
||||
/**
|
||||
* Where the forge serves the mesh's npm packages, under its own address: gitea's npm registry for the owner the
|
||||
* mesh's repositories are under, the operator's config value (novox/hq ADR 0507 §9).
|
||||
*/
|
||||
export const npmPath = (owner: string): string => `/api/packages/${encodeURIComponent(owner)}/npm/`;
|
||||
|
||||
/**
|
||||
* The registry's public root, as the forge serves it in the lab's npm-package-registry binding
|
||||
@@ -70,7 +73,8 @@ export const NPM_PATH = "/api/packages/novox/npm/";
|
||||
* nothing else. Undefined, with why, when the binding has none or it is not that shape — a forge on a
|
||||
* machine with no public domain serves none (ADR 0346 §3).
|
||||
*/
|
||||
export function publicRootOf(bindingFile: string | undefined): { root?: string; problem?: string } {
|
||||
export function publicRootOf(bindingFile: string | undefined, owner: string): { root?: string; problem?: string } {
|
||||
const NPM_PATH = npmPath(owner);
|
||||
if (!bindingFile) return { problem: "the lab has no npm-package-registry binding" };
|
||||
let said: unknown;
|
||||
try {
|
||||
@@ -112,8 +116,9 @@ export function publicRootOf(bindingFile: string | undefined): { root?: string;
|
||||
* reaches the run's npm configuration but a scheme, a host and a port. Anything else falls back to the
|
||||
* forge's own address and says why, so a run still starts and lab_check names it.
|
||||
*/
|
||||
export async function npmRegistry(forge: string, get: typeof fetch = fetch, publicRoot?: string):
|
||||
export async function npmRegistry(forge: string, owner: string, get: typeof fetch = fetch, publicRoot?: string):
|
||||
Promise<{ registry: string; problem?: string }> {
|
||||
const NPM_PATH = npmPath(owner);
|
||||
const asked = `${forge.replace(/\/+$/, "")}${NPM_PATH}`;
|
||||
const url = `${asked}@novox%2fmesh-sdk`;
|
||||
const fallback = (why: string) => ({
|
||||
@@ -453,6 +458,10 @@ export function running(work: string): RunStatus | undefined {
|
||||
return listRuns(work).find((s) => !["passed", "failed", "stopped", "lost"].includes(s.state));
|
||||
}
|
||||
|
||||
/** A repository's clone address on the forge: under the owner the mesh's repositories are under (ADR 0507 §9). */
|
||||
export const repositoryURL = (forge: string, owner: string, repo: string): string =>
|
||||
`${forge}/${encodeURIComponent(owner)}/${repo}.git`;
|
||||
|
||||
const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
|
||||
|
||||
/**
|
||||
@@ -461,8 +470,8 @@ const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
|
||||
* The environment is the one the lab's README describes for a run against sibling checkouts, pointed
|
||||
* at this run's own tree, so what is built and claimed is exactly what was checked out.
|
||||
*/
|
||||
export function script(work: string, id: string, forge: string, tests: string[], refs: Record<string, string>,
|
||||
credential?: string, registry = `${forge}${NPM_PATH}`): string {
|
||||
export function script(work: string, id: string, forge: string, owner: string, tests: string[], refs: Record<string, string>,
|
||||
credential?: string, registry = `${forge}${npmPath(owner)}`): string {
|
||||
const dir = runDir(work, id);
|
||||
const setState = (state: string) =>
|
||||
`node -e ${shellQuote(
|
||||
@@ -471,7 +480,7 @@ export function script(work: string, id: string, forge: string, tests: string[],
|
||||
const clones = REPOSITORIES.map((repo) => {
|
||||
const ref = refs[repo] ?? "main";
|
||||
return [
|
||||
`git ${credentialArgs(credential)}clone --quiet --depth 50 --branch ${shellQuote(ref)} ${shellQuote(`${forge}/novox/${repo}.git`)} ${shellQuote(join(dir, repo))}`,
|
||||
`git ${credentialArgs(credential)}clone --quiet --depth 50 --branch ${shellQuote(ref)} ${shellQuote(repositoryURL(forge, owner, repo))} ${shellQuote(join(dir, repo))}`,
|
||||
`echo "${repo} $(git -C ${shellQuote(join(dir, repo))} rev-parse HEAD)" >> ${shellQuote(join(dir, "commits.txt"))}`,
|
||||
].join("\n");
|
||||
}).join("\n");
|
||||
@@ -515,7 +524,7 @@ node --experimental-strip-types src/cli.ts suite ${tests.map(shellQuote).join("
|
||||
}
|
||||
|
||||
/** start begins a run and returns at once with its status. */
|
||||
export function start(work: string, forge: string, tests: string[], refs: Record<string, string>,
|
||||
export function start(work: string, forge: string, owner: string, tests: string[], refs: Record<string, string>,
|
||||
credential?: string, registry?: string): RunStatus {
|
||||
const id = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
||||
const dir = runDir(work, id);
|
||||
@@ -525,7 +534,7 @@ export function start(work: string, forge: string, tests: string[], refs: Record
|
||||
// What a crashed or rebooted run left behind goes before this run writes its own.
|
||||
cleanCredentials(work);
|
||||
const credentials = writeCredential(credentialPath(work, id), credential);
|
||||
writeFileSync(join(dir, "run.sh"), script(work, id, forge, tests, refs, credentials, registry), { mode: 0o700 });
|
||||
writeFileSync(join(dir, "run.sh"), script(work, id, forge, owner, tests, refs, credentials, registry), { mode: 0o700 });
|
||||
|
||||
// The wrapper records how the run ended, then removes the checkouts and keeps the log and status: a
|
||||
// run's tree is its own, and the next run starts from fresh ones (novox/hq ADR 0172).
|
||||
@@ -715,13 +724,13 @@ export function baseBuildLog(work: string, id: string, lines = 40): string {
|
||||
* baseScript is what one rebuild runs: mesh-lab's main from the forge, installed the way a run installs it,
|
||||
* then its `base build`, which replaces the image and reads back that it works before publishing it.
|
||||
*/
|
||||
export function baseScript(dir: string, forge: string, credential?: string, registry = `${forge}${NPM_PATH}`): string {
|
||||
export function baseScript(dir: string, forge: string, owner: string, credential?: string, registry = `${forge}${npmPath(owner)}`): string {
|
||||
const lab = join(dir, "mesh-lab");
|
||||
return `set -euo pipefail
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
${credentialGuard(credential)}
|
||||
cd ${shellQuote(dir)}
|
||||
git ${credentialArgs(credential)}clone --quiet --depth 1 ${shellQuote(`${forge}/novox/mesh-lab.git`)} ${shellQuote(lab)}
|
||||
git ${credentialArgs(credential)}clone --quiet --depth 1 ${shellQuote(repositoryURL(forge, owner, "mesh-lab"))} ${shellQuote(lab)}
|
||||
${credential ? `rm -f ${shellQuote(credential)}` : ""}
|
||||
echo "mesh-lab $(git -C ${shellQuote(lab)} rev-parse HEAD)"
|
||||
printf '%s\\n' ${shellQuote(`@novox:registry=${registry}`)} > ${shellQuote(join(dir, ".npmrc"))}
|
||||
@@ -733,7 +742,7 @@ node --experimental-strip-types src/cli.ts base build
|
||||
}
|
||||
|
||||
/** startBaseBuild rebuilds the base image in the background and answers at once with the build. */
|
||||
export function startBaseBuild(work: string, forge: string, credential?: string, registry?: string): BaseBuild {
|
||||
export function startBaseBuild(work: string, forge: string, owner: string, credential?: string, registry?: string): BaseBuild {
|
||||
const id = `base-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
||||
const dir = join(work, id);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
@@ -742,7 +751,7 @@ export function startBaseBuild(work: string, forge: string, credential?: string,
|
||||
// What a crashed or rebooted run or rebuild left behind goes before this one writes its own.
|
||||
cleanCredentials(work);
|
||||
const credentials = writeCredential(credentialPath(work, id), credential);
|
||||
writeFileSync(join(dir, "build.sh"), baseScript(dir, forge, credentials, registry), { mode: 0o700 });
|
||||
writeFileSync(join(dir, "build.sh"), baseScript(dir, forge, owner, credentials, registry), { mode: 0o700 });
|
||||
const wrapper = `bash ${shellQuote(join(dir, "build.sh"))} > ${shellQuote(join(dir, "build.log"))} 2>&1; code=$?
|
||||
node -e ${shellQuote(
|
||||
`const f=${JSON.stringify(buildStatusPath(work, id))};const s=JSON.parse(require("fs").readFileSync(f,"utf8"));s.state=process.argv[1]==="0"?"built":"failed";s.exit=Number(process.argv[1]);s.ended=new Date().toISOString();require("fs").writeFileSync(f,JSON.stringify(s,null,2))`,
|
||||
|
||||
Reference in New Issue
Block a user