mesh/merge-gate head 8d8dcd5ebb86 merged into trunk cd239283207a — pass: builds gitea, lab → novox, shanks; no bus step; every machine composes with the ch…
mesh/repo-check head 8d8dcd5ebb86 merged into trunk cd239283207a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The lab's credential came from the package team, which also writes the @novox packages, and the branches a run clones are not reviewed yet. The lab now contributes {"access": "read"} to its npm-package-registry requirement, and gitea's provisioner puts such a consumer on a team of its own, readers: code read on the org's repositories, package read, no write, and off the package team. The build agent, which publishes, stays on packages. A stop's SIGTERM to the run's process group killed the wrapper before it removed the credential file. The run's script now removes it as soon as its clones are done and traps every way it ends, and a run's start and lab_check remove what a crash or a reboot left.
197 lines
11 KiB
TypeScript
197 lines
11 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { spawnSync } from "node:child_process";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { installPlan, installStep, NPM_REPOSITORIES, REPOSITORIES, script } from "../tools/runs.ts";
|
|
|
|
// Defends novox/hq issue 624: lab_run ran `npm ci` at the root of every checkout it installs, and
|
|
// mesh-tools keeps its package in node-tools/ with no lock file at its root, so every run failed at its
|
|
// setup, in 9 seconds, before anything was built.
|
|
|
|
function aCheckout(files: Record<string, string>): string {
|
|
const root = mkdtempSync(join(tmpdir(), "lab-checkout-"));
|
|
for (const [path, content] of Object.entries(files)) {
|
|
mkdirSync(join(root, path, ".."), { recursive: true });
|
|
writeFileSync(join(root, path), content);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
test("a checkout with a lock file at its root is installed there", () => {
|
|
const root = aCheckout({ "package.json": "{}", "package-lock.json": "{}", "node-tools/package.json": "{}" });
|
|
assert.deepEqual(installPlan(root), { install: [root], unlocked: [] });
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
test("a checkout whose package lives one directory down is installed there, as mesh-tools' node-tools", () => {
|
|
const root = aCheckout({ "node-tools/package.json": "{}", "node-tools/package-lock.json": "{}", "Dockerfile": "" });
|
|
assert.deepEqual(installPlan(root), { install: [join(root, "node-tools")], unlocked: [] });
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
test("a package.json with no lock file is named, never installed", () => {
|
|
const root = aCheckout({ "package.json": "{}" });
|
|
assert.deepEqual(installPlan(root), { install: [], unlocked: [root] });
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
// The shell a run executes follows the same rule: npm is asked where installPlan says, and a checkout
|
|
// with nothing installable fails the run loudly, naming it.
|
|
function runStep(root: string, build: boolean): { ok: boolean; said: string; asked: string[] } {
|
|
const bin = mkdtempSync(join(tmpdir(), "lab-npm-"));
|
|
const record = join(bin, "asked");
|
|
writeFileSync(join(bin, "npm"), `#!/bin/sh\necho "$(pwd) $*" >> ${JSON.stringify(record)}\n`);
|
|
chmodSync(join(bin, "npm"), 0o755);
|
|
const ran = spawnSync("bash", ["-c", `set -euo pipefail\n${installStep(root, build)}`], {
|
|
encoding: "utf8", env: { ...process.env, PATH: `${bin}:${process.env.PATH}` },
|
|
});
|
|
let asked: string[] = [];
|
|
try {
|
|
asked = readFileSync(record, "utf8").trim().split("\n").filter(Boolean);
|
|
} catch { /* nothing asked */ }
|
|
rmSync(bin, { recursive: true, force: true });
|
|
return { ok: ran.status === 0, said: `${ran.stdout}${ran.stderr}`, asked };
|
|
}
|
|
|
|
test("a run installs and builds mesh-tools in node-tools, where its lock file is", () => {
|
|
const root = aCheckout({ "node-tools/package.json": "{}", "node-tools/package-lock.json": "{}" });
|
|
const r = runStep(root, true);
|
|
assert.ok(r.ok, r.said);
|
|
assert.deepEqual(r.asked, [
|
|
`${join(root, "node-tools")} ci --no-audit --no-fund --loglevel=error`,
|
|
`${join(root, "node-tools")} run build --if-present`,
|
|
]);
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
test("a run installs a checkout with its lock file at the root there, and builds only when asked", () => {
|
|
const root = aCheckout({ "package.json": "{}", "package-lock.json": "{}" });
|
|
const r = runStep(root, false);
|
|
assert.ok(r.ok, r.said);
|
|
assert.deepEqual(r.asked, [`${root} ci --no-audit --no-fund --loglevel=error`]);
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
test("a run fails at its setup, naming the checkout, when nothing of it can be installed", () => {
|
|
const layouts: Record<string, string>[] = [{ "package.json": "{}" }, { "Dockerfile": "" }, { "sub/package.json": "{}" }];
|
|
for (const files of layouts) {
|
|
const root = aCheckout(files);
|
|
const r = runStep(root, false);
|
|
assert.equal(r.ok, false, `installed ${JSON.stringify(files)}`);
|
|
assert.match(r.said, /issue 624/);
|
|
assert.deepEqual(r.asked, []);
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// A run checks out mesh-tools-go too (novox/hq issue 616): genesis builds the Go toolchain from the
|
|
// repository beside mesh-tools, and a bed that raises genesis serves it. And it tells the beds where the
|
|
// checkouts are.
|
|
test("a run checks out mesh-tools-go and says where its checkouts are", () => {
|
|
assert.ok(REPOSITORIES.includes("mesh-tools-go"));
|
|
const s = script("/work", "run-x", "http://forge", ["test/integration/genesis-keeps-its-bus.test.ts"], {});
|
|
assert.match(s, /novox\/mesh-tools-go\.git' '\/work\/run-x\/mesh-tools-go'/);
|
|
assert.match(s, /export MESH_LAB_REPOS='\/work\/run-x'/);
|
|
assert.doesNotMatch(s, /cd '\/work\/run-x'\/\$repo && npm ci/);
|
|
for (const repo of NPM_REPOSITORIES) assert.ok(s.includes(`'/work/run-x/${repo}'/package-lock.json`), repo);
|
|
});
|
|
|
|
// Defends novox/hq issue 624, its second half: mesh-tools-go is private, and the lab cloned over HTTP with
|
|
// no credential ("could not read Username"). A run clones with the forge credential the mesh binds and
|
|
// seals to the lab, through git's credential store, and the secret is in no script, URL or argument.
|
|
import { credentialArgs, credentialPath, forgeCredential } from "../tools/runs.ts";
|
|
|
|
function aBinding(as: string, secret: string): { binding: string; secret: string } {
|
|
const dir = mkdtempSync(join(tmpdir(), "lab-binding-"));
|
|
writeFileSync(join(dir, "package-registry.json"), JSON.stringify({ provision: "npm-package-registry", at: "10.42.0.1", as,
|
|
serves: { scheme: "http", port: 3000 } }));
|
|
writeFileSync(join(dir, "package-registry.secret"), `${secret}\n`);
|
|
return { binding: join(dir, "package-registry.json"), secret: join(dir, "package-registry.secret") };
|
|
}
|
|
|
|
test("the forge credential is the bound account and its sealed secret, for the forge's own origin", () => {
|
|
const b = aBinding("lab-shanks", "s3cret-of-forty-characters");
|
|
assert.equal(forgeCredential("http://novox.internal:20000", b.binding, b.secret),
|
|
"http://lab-shanks:s3cret-of-forty-characters@novox.internal:20000");
|
|
assert.equal(forgeCredential("http://novox.internal:20000/", b.binding, ""), undefined);
|
|
assert.equal(forgeCredential("", b.binding, b.secret), undefined);
|
|
assert.equal(forgeCredential("http://novox.internal:20000", "/nowhere", b.secret), undefined);
|
|
});
|
|
|
|
test("a run clones through git's credential store, and its script carries no secret", () => {
|
|
const b = aBinding("lab-shanks", "s3cret-of-forty-characters");
|
|
const credential = forgeCredential("http://novox.internal:20000", b.binding, b.secret);
|
|
const path = credentialPath("/work", "run-x");
|
|
const s = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {}, path);
|
|
assert.ok(credential);
|
|
assert.doesNotMatch(s, /s3cret/);
|
|
assert.match(s, /git -c credential\.helper= -c 'credential\.helper=store --file=\/work\/\.run-x\.git-credentials' clone --quiet --depth 50 --branch 'main' 'http:\/\/novox\.internal:20000\/novox\/mesh-tools-go\.git'/);
|
|
assert.match(s, /export GIT_TERMINAL_PROMPT=0/);
|
|
assert.equal(path.startsWith("/work/run-x/"), false, "the credential is kept out of the run's own directory");
|
|
assert.equal(credentialArgs(undefined), "");
|
|
});
|
|
|
|
// Held against git itself: the credential in the store is offered to the forge's origin when it asks, and
|
|
// a clone without it fails at once rather than prompting.
|
|
test("git presents the stored credential to the forge, and only when one is given", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "lab-git-"));
|
|
const store = join(dir, "git-credentials");
|
|
writeFileSync(store, "http://lab-shanks:pw@forge.invalid:20000\n", { mode: 0o600 });
|
|
const fill = spawnSync("sh", ["-c", `printf 'protocol=http\\nhost=forge.invalid:20000\\n\\n' | git ${credentialArgs(store)}credential fill`],
|
|
{ encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
assert.match(fill.stdout, /username=lab-shanks/);
|
|
assert.match(fill.stdout, /password=pw/);
|
|
const other = spawnSync("sh", ["-c", `printf 'protocol=http\\nhost=elsewhere.invalid\\n\\n' | git ${credentialArgs(store)}credential fill`],
|
|
{ encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
assert.doesNotMatch(other.stdout, /password=pw/);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
// Review 196 on mesh-catalog #272: the credential file outlived a stop, because SIGTERM to the run's
|
|
// process group killed the wrapper before its cleanup. It is now removed right after the clones, on
|
|
// every way the script ends, and at the next run's start and in lab_check for what a crash left.
|
|
import { cleanCredentials, credentialGuard } from "../tools/runs.ts";
|
|
|
|
test("a stopped script removes the credential, whatever ends it", () => {
|
|
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
|
|
const dir = mkdtempSync(join(tmpdir(), "lab-guard-"));
|
|
const file = join(dir, ".run-x.git-credentials");
|
|
writeFileSync(file, "http://u:p@h\n", { mode: 0o600 });
|
|
const ran = spawnSync("bash", ["-c", `${credentialGuard(file)}\nkill -${signal.slice(3)} $$\nsleep 5`], { encoding: "utf8", timeout: 10_000 });
|
|
assert.notEqual(ran.status, 0, `${signal}: the script was not stopped`);
|
|
assert.equal(existsSync(file), false, `${signal} left the credential on disk`);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("the script removes the credential as soon as its clones are done", () => {
|
|
const s = script("/work", "run-x", "http://forge", ["t.test.ts"], {}, "/work/.run-x.git-credentials");
|
|
const lastClone = s.lastIndexOf("clone --quiet");
|
|
const removed = s.indexOf("rm -f '/work/.run-x.git-credentials'", lastClone);
|
|
const building = s.indexOf("npm ci");
|
|
assert.ok(lastClone > 0 && removed > lastClone && removed < building, "not removed between the clones and the build");
|
|
assert.match(s, /trap 'rm -f '\\''\/work\/\.run-x\.git-credentials'\\''' EXIT/);
|
|
});
|
|
|
|
test("credentials a crashed run left are removed, but not the running one's", () => {
|
|
const work = mkdtempSync(join(tmpdir(), "lab-work-"));
|
|
for (const n of [".run-a.git-credentials", ".run-b.git-credentials", "run-a", "other"]) writeFileSync(join(work, n), "x");
|
|
const removed = cleanCredentials(work, join(work, ".run-b.git-credentials"));
|
|
assert.deepEqual(removed, [join(work, ".run-a.git-credentials")]);
|
|
assert.ok(existsSync(join(work, ".run-b.git-credentials")) && existsSync(join(work, "run-a")) && existsSync(join(work, "other")));
|
|
rmSync(work, { recursive: true, force: true });
|
|
});
|
|
|
|
// The lab asks the forge for read only (review 196): its credential is for clones of branches nobody has
|
|
// reviewed yet, and must not be able to publish the mesh's packages.
|
|
test("the lab asks the package registry for read only", () => {
|
|
const m = JSON.parse(readFileSync(join(import.meta.dirname, "..", "module.json"), "utf8")) as {
|
|
requires: unknown[]; contributes?: Record<string, Record<string, unknown>>;
|
|
};
|
|
assert.ok(m.requires.includes("npm-package-registry"));
|
|
assert.equal(m.contributes?.["npm-package-registry"]?.access, "read");
|
|
});
|