151 lines
6.6 KiB
TypeScript
151 lines
6.6 KiB
TypeScript
// What the catalogue makes of a build outcome it hears (novox/hq issue 712).
|
|
//
|
|
// Kept apart from index.ts, which subscribes at its top level, so the decision can be tested without a bus.
|
|
|
|
import type { Made } from "./store.js";
|
|
|
|
/** What the build seat says when it has built something, and what the control plane replays. */
|
|
export interface Built {
|
|
module?: string;
|
|
commit?: string;
|
|
repository?: string;
|
|
path?: string;
|
|
ref?: string;
|
|
manifest?: unknown;
|
|
/** 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[];
|
|
/**
|
|
* This is history, not news — the mesh re-announcing a build this catalogue was not there for.
|
|
*
|
|
* Registered exactly as any other, and announced as nothing. A module that moved months ago is
|
|
* not something anything should act on now: emitting `upgraded` would have the control plane
|
|
* decide about a rollout, and `rebuild-needed` would ask for builds of things that are already
|
|
* current.
|
|
*/
|
|
replay?: boolean;
|
|
/** Why the build failed; a failed build made no module-version. */
|
|
failed?: string;
|
|
/** A dry run is looked at, never taken in (novox/hq issue 240). */
|
|
"dry-run"?: boolean;
|
|
/** A pull request's merge check or a branch tried before one (to-be 45 §9, issue 686): nothing merged. */
|
|
"check-outcome"?: unknown;
|
|
check?: unknown;
|
|
/** The repository's default branch as the build seat read the forge at the build (ADR 0238); empty from a seat older than the rule. */
|
|
trunk?: string;
|
|
/** Whether the commit is on `trunk`. */
|
|
"on-trunk"?: boolean;
|
|
/** The branches the commit is on. */
|
|
branches?: string[];
|
|
}
|
|
|
|
/**
|
|
* Where a build outcome comes from now: the builder's log (novox/hq ADR 0571 clauses 1 and 9). The module asks the
|
|
* builder's tool for the builds that ended since a time, and again whenever the builder's state of done keys changes.
|
|
*/
|
|
export const BUILDER = "mesh-delivery-builder";
|
|
export const BUILDS_SINCE_TOOL = "builder_builds_since";
|
|
export const BUILDS_STATE = "mesh-delivery-builder.builds";
|
|
|
|
/** One entry of the builder's log, an ended build (mesh-sdk delivery.BuildLogEntry). */
|
|
export interface LogEntry {
|
|
event: string;
|
|
module: string;
|
|
fp: string;
|
|
repository: string;
|
|
directory: string;
|
|
commit: string;
|
|
ref?: string;
|
|
node?: string;
|
|
at: string;
|
|
/** done, failed or queued. */
|
|
outcome?: string;
|
|
/** The digest of the manifest built, read from the indexer's state of manifests. */
|
|
manifest?: string;
|
|
against?: string[];
|
|
made?: Made[];
|
|
}
|
|
|
|
/** The answer of the builder's tool: every ended build since a time, oldest first, and whether the log held more. */
|
|
export interface BuildsSince {
|
|
since: string;
|
|
builds: LogEntry[];
|
|
more?: boolean;
|
|
}
|
|
|
|
/**
|
|
* What a tool answers, whether bare or wrapped as the runtime wraps a tool's reply ({output} or {content:[{text}]}).
|
|
*/
|
|
export function answerOf<T>(raw: unknown): T {
|
|
const r = raw as { output?: unknown; content?: { text?: string }[] } | null;
|
|
if (r && typeof r.output === "string") return JSON.parse(r.output) as T;
|
|
if (r && Array.isArray(r.content) && typeof r.content[0]?.text === "string") return JSON.parse(r.content[0].text) as T;
|
|
if (typeof raw === "string") return JSON.parse(raw) as T;
|
|
return raw as T;
|
|
}
|
|
|
|
/**
|
|
* A log entry as the build outcome the catalogue places: a build that is done is placed with its manifest, what it
|
|
* made and what it was built against; one that failed is passed over (placeable says so); one still queued has not
|
|
* ended and is not taken in. `replay` is set by the caller for history.
|
|
*/
|
|
export function builtFrom(e: LogEntry, manifest: unknown, replay: boolean): Built | undefined {
|
|
if (e.outcome === "queued") return undefined;
|
|
return {
|
|
module: e.module,
|
|
commit: e.commit,
|
|
repository: e.repository,
|
|
path: e.directory,
|
|
ref: e.ref ?? "",
|
|
manifest,
|
|
made: e.made ?? [],
|
|
against: e.against ?? [],
|
|
replay,
|
|
...(e.outcome === "failed" ? { failed: "the builder's log says the build failed" } : {}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Whether a build outcome is a module-version to place in the graph, and why not when it is not.
|
|
*
|
|
* It mirrors two of the rules the controller keeps before it registers a build (mesh-controller
|
|
* cmd/mesh-controller/main.go `builds.Built`, build.go `publishable`): a merge check, a try and a dry run are
|
|
* never published, and **only a commit on its repository's trunk is** (ADR 0238) — a branch built through
|
|
* the `build` verb is checked, never a module's version. Where the seat names no trunk (one older than the
|
|
* rule) it is placed, as the controller lets it through. A failure made nothing and is passed over too.
|
|
*
|
|
* Not mirrored, because the catalogue cannot see what they read: the branch a module follows when it is not
|
|
* the default one (the controller's `follows`; such a module's builds off the default branch are passed over
|
|
* here and reach the graph by the replay), ADR 0266's rule that a module registers only from the repository
|
|
* it is built from, a superseded request, a build in the scratch namespace, one that failed its gate, and a
|
|
* build whose source is unchanged. Each needs the controller's records. This is safe for what the mesh
|
|
* sends, which is chosen from the controller's own module table and never from this graph; that the graph
|
|
* can still hold a build the controller refused under those rules is a follow-up (hq issue 712).
|
|
*/
|
|
export function placeable(body: Built):
|
|
| { placed: "yes"; module: string; commit: string }
|
|
| { placed: "no"; why: string; fault: boolean } {
|
|
if (body["check-outcome"] != null || body.check != null) {
|
|
return { placed: "no", why: "a merge check or a try: nothing of it merged", fault: false };
|
|
}
|
|
if (body["dry-run"]) {
|
|
return { placed: "no", why: "a dry run is looked at, never taken in", fault: false };
|
|
}
|
|
if (body.failed) {
|
|
return { placed: "no", why: "a failed build made no module-version", fault: false };
|
|
}
|
|
if (body.trunk && !body["on-trunk"] && !(body.branches ?? []).includes(body.trunk)) {
|
|
return { placed: "no", why: `the commit is not on its repository's trunk ${body.trunk}: checked, never published (ADR 0238)`, fault: false };
|
|
}
|
|
if (!body.module || !body.commit) {
|
|
return { placed: "no", why: "it named no module or no commit", fault: true };
|
|
}
|
|
return { placed: "yes", module: body.module, commit: body.commit };
|
|
}
|