lab: name the package registry by the address it names itself by, so npm 12 installs locked @novox packages (hq issue 624)
mesh/merge-gate head bb60de6d616a merged into trunk a84d0094a0a3 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head bb60de6d616a merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head bb60de6d616a merged into trunk a84d0094a0a3 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head bb60de6d616a merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
npm 12 installs a locked tarball only from its configured registry; the forge writes every tarball URL, and so every lock file, at its public address, and the lab named the registry by its forge setting's address. Every run stopped at mesh-tools' install with EALLOWREMOTE.
This commit is contained in:
@@ -251,3 +251,53 @@ test("the lab's tools bundle names its bound credential and binding in its env",
|
||||
assert.equal(env.MESH_LAB_FORGE_SECRET, m.secrets["npm-package-registry"]);
|
||||
assert.equal(env.MESH_LAB_FORGE_BINDING, m.binds["npm-package-registry"]);
|
||||
});
|
||||
|
||||
// The next setup failure of novox/hq issue 624 (run-2026-10-13T13-15-26-634Z on shanks): npm 12 refused
|
||||
// mesh-tools' locked @novox/mesh-sdk with EALLOWREMOTE. The lab named the registry by its forge setting's
|
||||
// address (http://novox.internal:20000/…), the lock file holds the address the forge writes every tarball
|
||||
// URL at (https://git.novox.be/…), and npm 12 installs a locked tarball only from its configured registry.
|
||||
// A run names the registry by the address the registry names itself by.
|
||||
import { npmRegistry } from "../tools/runs.ts";
|
||||
|
||||
function aRegistry(answer: () => Response | Promise<Response>): { get: typeof fetch; asked: string[] } {
|
||||
const asked: string[] = [];
|
||||
const get = (async (url: string | URL | Request) => {
|
||||
asked.push(String(url));
|
||||
return answer();
|
||||
}) as typeof fetch;
|
||||
return { get, asked };
|
||||
}
|
||||
|
||||
const packument = (tarball: string) => new Response(JSON.stringify({
|
||||
name: "@novox/mesh-sdk", "dist-tags": { latest: "0.1.14" },
|
||||
versions: { "0.1.1": { dist: { tarball: "http://old.example/api/packages/novox/npm/x.tgz" } }, "0.1.14": { dist: { tarball } } },
|
||||
}), { status: 200, headers: { "content-type": "application/json" } });
|
||||
|
||||
test("a run names the package registry by the address the registry gives its tarballs", async () => {
|
||||
const r = aRegistry(() => packument("https://forge.example.org/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz"));
|
||||
const got = await npmRegistry("http://novox.internal:20000/", r.get);
|
||||
assert.deepEqual(got, { registry: "https://forge.example.org/api/packages/novox/npm/" });
|
||||
assert.deepEqual(r.asked, ["http://novox.internal:20000/api/packages/novox/npm/@novox%2fmesh-sdk"]);
|
||||
});
|
||||
|
||||
test("a registry that does not say its address leaves the forge's, and the reason is said", async () => {
|
||||
for (const answer of [
|
||||
() => new Response("not found", { status: 404 }),
|
||||
() => packument("https://forge.example.org/elsewhere/mesh-sdk-0.1.14.tgz"),
|
||||
() => new Response("{}", { status: 200 }),
|
||||
() => { throw new Error("connect ECONNREFUSED"); },
|
||||
]) {
|
||||
const got = await npmRegistry("http://novox.internal:20000", aRegistry(answer).get);
|
||||
assert.equal(got.registry, "http://novox.internal:20000/api/packages/novox/npm/");
|
||||
assert.match(got.problem ?? "", /did not say its own address/);
|
||||
}
|
||||
});
|
||||
|
||||
test("a run's npm configuration names the registry it was given, and the forge's own address only without one", () => {
|
||||
const given = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {}, undefined,
|
||||
"https://forge.example.org/api/packages/novox/npm/");
|
||||
assert.match(given, / '@novox:registry=https:\/\/forge\.example\.org\/api\/packages\/novox\/npm\/' > '\/work\/run-x\/\.npmrc'\n/);
|
||||
assert.doesNotMatch(given, /@novox:registry=http:\/\/novox\.internal/);
|
||||
const none = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {});
|
||||
assert.match(none, /'@novox:registry=http:\/\/novox\.internal:20000\/api\/packages\/novox\/npm\/'/);
|
||||
});
|
||||
|
||||
@@ -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, NPM_REPOSITORIES, readStatus, REPOSITORIES, running, start,
|
||||
checkDir, cleanCredentials, clearChecks, credentialArgs, forgeCredential, installPlan, listRuns, npmRegistry, NPM_REPOSITORIES, readStatus, REPOSITORIES, running, start,
|
||||
stop, tail, writeCredential,
|
||||
} from "./runs.js";
|
||||
|
||||
@@ -42,6 +42,9 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
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));
|
||||
// And the address a run names the package registry by (npmRegistry): said when the registry does not say it.
|
||||
const registry = await npmRegistry(forge);
|
||||
if (registry.problem) problems.push(registry.problem);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -87,7 +90,9 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
|
||||
if (stranger.length > 0) return { started: false, reason: `the lab does not build ${stranger.join(", ")}` };
|
||||
const busy = running(work);
|
||||
if (busy) return { started: false, reason: `${busy.id} is still ${busy.state}; one run at a time`, running: busy };
|
||||
return { started: true, run: start(work, forge, tests, refs, credentialOf(forge)) };
|
||||
const registry = await npmRegistry(forge);
|
||||
return { started: true, run: start(work, forge, tests, refs, credentialOf(forge), registry.registry),
|
||||
registry: registry.registry, ...(registry.problem ? { problem: registry.problem } : {}) };
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,6 +60,44 @@ 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/";
|
||||
|
||||
/**
|
||||
* The address a run names the @novox registry by: the one the registry names itself by (novox/hq issue 624).
|
||||
*
|
||||
* **npm 12 installs a locked tarball only from the registry it is configured with.** Its `allow-remote`
|
||||
* defaults to `none`, and a lock file's `resolved` URL counts as the registry's only when its origin and
|
||||
* path are the configured scope registry's; any other is a "remote" tarball and refused (EALLOWREMOTE).
|
||||
* The forge writes every tarball URL — in what it answers, and so in every lock file made against it — at
|
||||
* its own public address, whichever address it was asked at; the lab reaches it at the forge setting's,
|
||||
* another one. So the lab asks the registry, at the forge, for the SDK's newest tarball and names the
|
||||
* registry by the address in it, as the lock files do. Answered with the forge's own address and the
|
||||
* reason when the registry does not say, so a run still starts and lab_check names it.
|
||||
*/
|
||||
export async function npmRegistry(forge: string, get: typeof fetch = fetch): Promise<{ registry: string; problem?: string }> {
|
||||
const asked = `${forge.replace(/\/+$/, "")}${NPM_PATH}`;
|
||||
const url = `${asked}@novox%2fmesh-sdk`;
|
||||
const fallback = (why: string) => ({
|
||||
registry: asked,
|
||||
problem: `the package registry at ${url} did not say its own address (${why}), so a run names it ${asked}, and npm 12 refuses a locked @novox tarball at any other`,
|
||||
});
|
||||
try {
|
||||
const res = await get(url, { signal: AbortSignal.timeout(15_000) });
|
||||
if (!res.ok) return fallback(`it answered ${res.status}`);
|
||||
const doc = await res.json() as { "dist-tags"?: { latest?: string }; versions?: Record<string, { dist?: { tarball?: string } }> };
|
||||
const latest = doc["dist-tags"]?.latest ?? "";
|
||||
const tarball = doc.versions?.[latest]?.dist?.tarball ?? "";
|
||||
const at = tarball.indexOf(NPM_PATH);
|
||||
if (at < 0) return fallback(tarball ? `its tarball ${tarball} is not under ${NPM_PATH}` : "no tarball for its latest version");
|
||||
const registry = tarball.slice(0, at + NPM_PATH.length);
|
||||
new URL(registry);
|
||||
return { registry };
|
||||
} catch (e) {
|
||||
return fallback(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The forge credential a run's clones may present, as a git credential-store line —
|
||||
* scheme://user:password@host[:port] — for the forge's own origin, or undefined when the lab holds none.
|
||||
@@ -240,7 +278,7 @@ const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
|
||||
* 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): string {
|
||||
credential?: string, registry = `${forge}${NPM_PATH}`): string {
|
||||
const dir = runDir(work, id);
|
||||
const setState = (state: string) =>
|
||||
`node -e ${shellQuote(
|
||||
@@ -266,9 +304,9 @@ node -e ${shellQuote(
|
||||
`const fs=require("fs");const f=${JSON.stringify(join(dir, "status.json"))};const s=JSON.parse(fs.readFileSync(f,"utf8"));s.commits=Object.fromEntries(fs.readFileSync(${JSON.stringify(join(dir, "commits.txt"))},"utf8").trim().split("\\n").map(l=>l.split(" ")));fs.writeFileSync(f,JSON.stringify(s,null,2))`,
|
||||
)}
|
||||
${setState("building")}
|
||||
# The @novox scope resolves from the mesh's own package registry on the forge, as the builder
|
||||
# resolves it; nothing else is asked of it.
|
||||
printf '%s\n' ${shellQuote(`@novox:registry=${forge}/api/packages/novox/npm/`)} > ${shellQuote(join(dir, ".npmrc"))}
|
||||
# The @novox scope resolves from the mesh's own package registry on the forge, named by the address it
|
||||
# names itself by (npmRegistry), which is the address its lock files hold; nothing else is asked of it.
|
||||
printf '%s\n' ${shellQuote(`@novox:registry=${registry}`)} > ${shellQuote(join(dir, ".npmrc"))}
|
||||
export NPM_CONFIG_USERCONFIG=${shellQuote(join(dir, ".npmrc"))}
|
||||
${NPM_REPOSITORIES.map((repo) => installStep(join(dir, repo), repo !== "mesh-lab")).join("\n")}
|
||||
mkdir -p ${shellQuote(bin)}
|
||||
@@ -294,7 +332,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>,
|
||||
credential?: string): RunStatus {
|
||||
credential?: string, registry?: string): RunStatus {
|
||||
const id = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
||||
const dir = runDir(work, id);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
@@ -303,7 +341,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), { mode: 0o700 });
|
||||
writeFileSync(join(dir, "run.sh"), script(work, id, forge, 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).
|
||||
|
||||
Reference in New Issue
Block a user