The catalogue holds the module graph, and the modules that make it build themselves #21
@@ -10,7 +10,7 @@
|
||||
// diamond need no special handling, and nothing holds a plan.
|
||||
|
||||
import { on, emit } from "@novox/mesh-sdk/events";
|
||||
import { Graph, type BuiltAgainst } from "./store.js";
|
||||
import { Graph, type Made } from "./store.js";
|
||||
|
||||
const graph = Graph.fromEnv();
|
||||
|
||||
@@ -27,8 +27,15 @@ interface Built {
|
||||
path?: string;
|
||||
ref?: string;
|
||||
manifest?: unknown;
|
||||
/** Every artifact this was built against, so the edge is derived rather than declared. */
|
||||
against?: { module: string; commit: string }[];
|
||||
/** What this build published, each pinned as anything else would name it. */
|
||||
made?: Made[];
|
||||
/**
|
||||
* Every artifact this was built on top of, so the edge is derived rather than declared.
|
||||
*
|
||||
* References, not module names: the builder sees a pinned image and cannot see which module
|
||||
* produced it. Turning that into an edge between module-versions is this module's job.
|
||||
*/
|
||||
against?: string[];
|
||||
}
|
||||
|
||||
await on("module.builder.built", async (event) => {
|
||||
@@ -40,13 +47,6 @@ await on("module.builder.built", async (event) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const against: BuiltAgainst[] = (body.against ?? []).map((a) => ({
|
||||
module: body.module as string,
|
||||
commit: body.commit as string,
|
||||
againstModule: a.module,
|
||||
againstCommit: a.commit,
|
||||
}));
|
||||
|
||||
const { upgraded, previous } = await graph.register({
|
||||
module: body.module,
|
||||
commit: body.commit,
|
||||
@@ -54,7 +54,7 @@ await on("module.builder.built", async (event) => {
|
||||
path: body.path ?? "",
|
||||
ref: body.ref ?? "",
|
||||
manifest: body.manifest ?? {},
|
||||
}, against);
|
||||
}, body.made ?? [], body.against ?? []);
|
||||
|
||||
await emit("module.mesh-catalog.registered", {
|
||||
module: body.module, commit: body.commit, upgraded,
|
||||
|
||||
@@ -27,15 +27,25 @@ export interface ModuleVersion {
|
||||
manifest: unknown;
|
||||
}
|
||||
|
||||
/** An edge: this module-version was built against that one. Derived, never declared (ADR 0009). */
|
||||
export interface BuiltAgainst {
|
||||
module: string;
|
||||
commit: string;
|
||||
againstModule: string;
|
||||
againstCommit: string;
|
||||
/** An artifact a module-version produced, as the builder published it. */
|
||||
export interface Made {
|
||||
name: string;
|
||||
kind: string;
|
||||
/** How anything else names it — for an image, a registry reference pinned to a digest. */
|
||||
reference: string;
|
||||
}
|
||||
|
||||
/** A module whose artifacts were built against something that is no longer current. */
|
||||
/**
|
||||
* A module whose artifacts were built against something that is no longer current.
|
||||
*
|
||||
* **An edge is an artifact reference, not a module name.** The builder can see exactly what it
|
||||
* built on top of — a pinned image — and cannot see which module produced it: that is a fact about
|
||||
* the graph, and the graph is here. So the reference is what is stored, and resolving it to a
|
||||
* module-version is a join against what each version says it made. An edge to an artifact no
|
||||
* module here produced is kept rather than dropped; it resolves by itself the day that module is
|
||||
* registered, which is the ordinary case while a mesh is still being filled in.
|
||||
*/
|
||||
|
||||
export interface Stale {
|
||||
module: string;
|
||||
commit: string;
|
||||
@@ -64,15 +74,39 @@ CREATE TABLE IF NOT EXISTS module_current (
|
||||
moved timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS built_against (
|
||||
module text NOT NULL,
|
||||
commit_sha text NOT NULL,
|
||||
against_module text NOT NULL,
|
||||
against_commit text NOT NULL,
|
||||
PRIMARY KEY (module, commit_sha, against_module, against_commit)
|
||||
-- What a module-version published. This is what makes a build edge resolvable: an edge names an
|
||||
-- artifact, and this says which version put that artifact there.
|
||||
CREATE TABLE IF NOT EXISTS module_artifact (
|
||||
module text NOT NULL,
|
||||
commit_sha text NOT NULL,
|
||||
name text NOT NULL,
|
||||
kind text NOT NULL DEFAULT '',
|
||||
reference text NOT NULL,
|
||||
PRIMARY KEY (module, commit_sha, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS built_against_target ON built_against (against_module);
|
||||
CREATE INDEX IF NOT EXISTS module_artifact_reference ON module_artifact (reference);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS built_against (
|
||||
module text NOT NULL,
|
||||
commit_sha text NOT NULL,
|
||||
against_reference text NOT NULL,
|
||||
PRIMARY KEY (module, commit_sha, against_reference)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS built_against_target ON built_against (against_reference);
|
||||
`;
|
||||
|
||||
// The graph first keyed its edges on a module and a commit, which the builder does not know and
|
||||
// never sent — so every build that had been built against anything was rejected, and the only
|
||||
// versions that registered were the ones built against nothing. No edge was ever stored, so there
|
||||
// is nothing to carry across: the old columns are dropped and the new one added.
|
||||
const MIGRATE = `
|
||||
ALTER TABLE built_against ADD COLUMN IF NOT EXISTS against_reference text;
|
||||
DELETE FROM built_against WHERE against_reference IS NULL;
|
||||
ALTER TABLE built_against DROP COLUMN IF EXISTS against_module;
|
||||
ALTER TABLE built_against DROP COLUMN IF EXISTS against_commit;
|
||||
ALTER TABLE built_against ALTER COLUMN against_reference SET NOT NULL;
|
||||
`;
|
||||
|
||||
export class Graph {
|
||||
@@ -91,6 +125,7 @@ export class Graph {
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
await this.pool.query(DDL);
|
||||
await this.pool.query(MIGRATE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +135,7 @@ export class Graph {
|
||||
* rebuild producing the commit already current changes nothing, and announcing it as an upgrade
|
||||
* would ripple outward forever through modules that did not change (ADR 0072).
|
||||
*/
|
||||
async register(version: ModuleVersion, against: BuiltAgainst[]): Promise<{ upgraded: boolean; previous: string | null }> {
|
||||
async register(version: ModuleVersion, made: Made[], against: string[]): Promise<{ upgraded: boolean; previous: string | null }> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
@@ -114,15 +149,25 @@ export class Graph {
|
||||
JSON.stringify(version.manifest ?? {})],
|
||||
);
|
||||
|
||||
// The edges are replaced rather than added to: they describe this build, and a previous
|
||||
// build of the same commit that saw different dependencies was wrong about one of them.
|
||||
// Both are replaced rather than added to: they describe this build, and a previous build of
|
||||
// the same commit that saw different artifacts was wrong about one of them.
|
||||
await client.query(`DELETE FROM module_artifact WHERE module = $1 AND commit_sha = $2`,
|
||||
[version.module, version.commit]);
|
||||
for (const a of made) {
|
||||
await client.query(
|
||||
`INSERT INTO module_artifact (module, commit_sha, name, kind, reference)
|
||||
VALUES ($1,$2,$3,$4,$5) ON CONFLICT (module, commit_sha, name) DO UPDATE SET
|
||||
kind = excluded.kind, reference = excluded.reference`,
|
||||
[version.module, version.commit, a.name, a.kind, a.reference]);
|
||||
}
|
||||
|
||||
await client.query(`DELETE FROM built_against WHERE module = $1 AND commit_sha = $2`,
|
||||
[version.module, version.commit]);
|
||||
for (const e of against) {
|
||||
for (const reference of against) {
|
||||
await client.query(
|
||||
`INSERT INTO built_against (module, commit_sha, against_module, against_commit)
|
||||
VALUES ($1,$2,$3,$4) ON CONFLICT DO NOTHING`,
|
||||
[version.module, version.commit, e.againstModule, e.againstCommit]);
|
||||
`INSERT INTO built_against (module, commit_sha, against_reference)
|
||||
VALUES ($1,$2,$3) ON CONFLICT DO NOTHING`,
|
||||
[version.module, version.commit, reference]);
|
||||
}
|
||||
|
||||
const was = await client.query(
|
||||
@@ -175,13 +220,18 @@ export class Graph {
|
||||
};
|
||||
}
|
||||
|
||||
/** What was built against this module — the modules a change to it reaches. */
|
||||
/**
|
||||
* What was built against this module — the modules a change to it reaches.
|
||||
*
|
||||
* Resolved through the artifacts each version published, because that is what an edge names.
|
||||
*/
|
||||
async dependents(module: string): Promise<{ module: string; commit: string; againstCommit: string }[]> {
|
||||
const { rows } = await this.pool.query(
|
||||
`SELECT b.module, b.commit_sha, b.against_commit
|
||||
`SELECT b.module, b.commit_sha, a.commit_sha AS against_commit
|
||||
FROM built_against b
|
||||
JOIN module_current c ON c.module = b.module AND c.commit_sha = b.commit_sha
|
||||
WHERE b.against_module = $1
|
||||
JOIN module_artifact a ON a.reference = b.against_reference
|
||||
WHERE a.module = $1
|
||||
ORDER BY b.module`, [module]);
|
||||
return rows.map((r) => ({
|
||||
module: r.module as string, commit: r.commit_sha as string,
|
||||
@@ -199,11 +249,13 @@ export class Graph {
|
||||
*/
|
||||
async stale(): Promise<Stale[]> {
|
||||
const { rows } = await this.pool.query(
|
||||
`SELECT b.module, b.commit_sha, b.against_module, b.against_commit, c2.commit_sha AS now_at
|
||||
`SELECT b.module, b.commit_sha, a.module AS against_module, a.commit_sha AS against_commit,
|
||||
c2.commit_sha AS now_at
|
||||
FROM built_against b
|
||||
JOIN module_current c1 ON c1.module = b.module AND c1.commit_sha = b.commit_sha
|
||||
JOIN module_current c2 ON c2.module = b.against_module
|
||||
WHERE c2.commit_sha <> b.against_commit
|
||||
JOIN module_artifact a ON a.reference = b.against_reference
|
||||
JOIN module_current c2 ON c2.module = a.module
|
||||
WHERE c2.commit_sha <> a.commit_sha
|
||||
ORDER BY b.module`);
|
||||
const by = new Map<string, Stale>();
|
||||
for (const r of rows) {
|
||||
|
||||
Reference in New Issue
Block a user