Run the forge, on a database the mesh gave it

Everything up to now stopped at composing a declaration. That proves the
control plane and the host agree, and proves nothing about whether the
thing described works — which is how five modules sat pinned to images
that did not exist while parsing and resolving perfectly.

The forge is the right one to run first. It needs a database from
another module, a password it did not choose, and a connection string it
could not have written itself: the address and port come from what the
database serves, the user name from what the mesh decided both ends
would call it. If any of that is wrong it cannot start, and nothing else
in this suite would notice.

The test checks the chain in the order it has to happen — the login
exists, the database it owns exists, the forge answers, and its log does
not say authentication failed. That last one matters: a forge that
started and could not reach its database would still answer on its port.

Rewriting image references is now shared rather than copied from the
bundle, which had the same problem first. A digest is not knowable until
something is built, and when it is, it belongs to whichever registry
served it — so the text says which image and the scenario says which
copy. Matching is on the repository, with a test that a repository
ending in another one is not half-replaced.

Also makes the planning test put the machine back. Tests here share one
mesh, so the five modules it assigned were inherited by whatever ran
next; harmless while nothing pushed, and not harmless now.
This commit is contained in:
2026-09-01 15:39:30 +02:00
parent bb14ecb7e0
commit 55022edc1e
4 changed files with 205 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
/**
* Rewriting an image reference to the one a scenario's own registry serves.
*
* **A digest is not knowable until something is built** (novox/hq 04-ISSUES/025). A manifest in a
* repository can pin a third-party image, because somebody can ask a registry what a tag points
* at. It cannot pin an image the mesh builds itself: that image does not exist yet, and when it
* does its digest belongs to whichever registry served it.
*
* The bundle has always had this problem and solves it by rewriting references once the scenario's
* registry is up and its digests are known. Modules have exactly the same problem and were solving
* it by shipping sixty-four zeros, which parses, resolves, composes — and stops on the machine.
*
* So the rewriting is shared rather than copied, and matches on the **repository**, because that
* is the part a person writes and the only part that survives being served somewhere else.
*/
/** `192.0.2.250:5000/ghcr.io/mailu/admin@sha256:…` → `ghcr.io/mailu/admin` */
export function repositoryOf(pinned: string): string {
const at = pinned.indexOf("@");
const body = at === -1 ? pinned : pinned.slice(0, at);
const slash = body.indexOf("/");
// Everything after the registry. A reference with no slash at all is its own repository.
return slash === -1 ? body : body.slice(slash + 1);
}
/**
* Replace every reference to a stocked repository with the reference this scenario serves.
*
* Matching is on the repository and ignores whatever registry and digest were written down —
* a file may name `postgres@sha256:7456…` or `mesh-provision-postgres@sha256:0000…` and both mean
* *the postgres this scenario has*. That is the whole point: the text says which image, the
* scenario says which copy.
*
* A repository the scenario did not stock is left alone rather than blanked. It may be reachable
* some other way, and silently emptying a reference would produce the exact failure this exists to
* prevent.
*/
export function pinnedInto(text: string, served: string[]): string {
let out = text;
for (const pinned of served) {
const repository = repositoryOf(pinned);
const escaped = repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Optionally a registry, then the repository, then any digest. Anchored on a quote or
// whitespace so a longer repository ending in a shorter one is not half-replaced.
out = out.replaceAll(
new RegExp(`(?<=^|["\\s])(?:[A-Za-z0-9_.:-]+\\/)*${escaped}@sha256:[0-9a-f]{64}`, "g"),
pinned,
);
}
return out;
}
/** Whether anything is still pinned to a placeholder, which would fail on the machine. */
export function stillUnpinned(text: string): string[] {
return [...text.matchAll(/([A-Za-z0-9_.:/-]+)@sha256:0{64}/g)].map((m) => m[1]!);
}