Files
mesh-catalog/modules/gitea/delivery.ts
T
jochen b6f0bc309b
mesh/merge-gate fail: a manifest the change touches fails the module check: modules/mesh-delivery/module.json: this manifest cannot be used:
mesh/delivery delivered
mesh/delivery-group group feat/mesh-delivery delivered: every member is delivered
Add mesh-delivery, the owner of deliveries and delivery groups (hq ADR 0239)
One module answers 'did my change go out' for a commit and orders a
cross-repository change: one compiled state table, its state on the bus,
every transition said, noted on the commit and shown on the pull request.
The forge's holder gains the note, view and status tools it asks with, and
says closed pull requests and a merge's head and statuses.
2026-10-06 23:59:54 +02:00

98 lines
5.4 KiB
TypeScript

// What the forge's holder does for a delivery (novox/hq ADR 0239): the commit's note under
// refs/notes/mesh-plan, the delivery's view on its pull request, and its statuses — asked by mesh-delivery,
// the delivery's owner, through this module's tools. The forge is this module's; mesh-delivery never writes
// to it itself.
//
// **The note is written in the forge's own repository, as the forge's own user.** The forge's API reads a
// note and writes none, and a push needs a clone and a credential nobody else should hold; the forge's
// container holds the bare repository and git. So the line is appended there, by `git notes append`, as the
// account the forge runs as — the mesh's own forge, never a person's or an agent's hand. Appending is
// idempotent here: a line the note already has is not added again, so asking twice adds nothing.
//
// Pure functions and an injectable runner, so this is tested without a forge.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
/** How a command is run: docker on the forge's machine, or a test's. */
export type Runner = (file: string, args: string[]) => Promise<{ stdout: string; code: number }>;
const execFileP = promisify(execFile);
export const run: Runner = async (file, args) => {
try {
const { stdout } = await execFileP(file, args, { maxBuffer: 4 << 20 });
return { stdout, code: 0 };
} catch (err) {
const e = err as { code?: number; stdout?: string };
return { stdout: e.stdout ?? "", code: typeof e.code === "number" ? e.code : 1 };
}
};
const name = /^[A-Za-z0-9_.-]+$/;
const sha = /^[0-9a-fA-F]{7,64}$/;
const ref = /^[a-z0-9][a-z0-9-]*$/;
/** A note line as it may be written: one line, bounded, nothing that is not text. */
export function noteLine(line: string): string {
const one = String(line ?? "").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
if (!one) throw new Error("an empty line is no note");
return one.length > 2000 ? one.slice(0, 1999) + "…" : one;
}
/** Where the forge keeps a repository, inside its container. */
export function gitDir(owner: string, repo: string): string {
if (!name.test(owner) || !name.test(repo)) throw new Error(`${owner}/${repo} is not a repository's name`);
return `/data/git/repositories/${owner.toLowerCase()}/${repo.toLowerCase()}.git`;
}
/** The commands the note is read and appended with, in the forge's container, as its user. */
export function noteArgs(container: string, owner: string, repo: string, commit: string, notesRef: string, line?: string): string[] {
if (!name.test(container)) throw new Error(`${container} is not a container's name`);
if (!sha.test(commit)) throw new Error(`${commit} is not a commit`);
if (!ref.test(notesRef)) throw new Error(`${notesRef} is not a notes ref`);
const base = ["exec", "-u", "git", container, "git", "--git-dir", gitDir(owner, repo),
"-c", "user.name=mesh", "-c", "user.email=mesh@mesh.invalid", "notes", `--ref=${notesRef}`];
return line === undefined ? [...base, "show", commit] : [...base, "append", "-m", noteLine(line), commit];
}
/** Append a line to a commit's note, unless the note already holds it. Answers whether it was added. */
export async function appendNote(runner: Runner, container: string, owner: string, repo: string, commit: string,
notesRef: string, line: string): Promise<{ added: boolean; lines: number }> {
const wanted = noteLine(line);
const shown = await runner("docker", noteArgs(container, owner, repo, commit, notesRef));
// No note yet is git's exit 1 with nothing on stdout; anything else unreadable is said.
const lines = shown.code === 0 ? shown.stdout.split("\n").filter((l) => l.trim() !== "") : [];
if (lines.includes(wanted)) return { added: false, lines: lines.length };
const appended = await runner("docker", noteArgs(container, owner, repo, commit, notesRef, wanted));
if (appended.code !== 0) throw new Error(`the note on ${commit.slice(0, 8)} could not be appended (git exited ${appended.code})`);
return { added: true, lines: lines.length + 1 };
}
/** The marker that makes one comment of a pull request the delivery's view. */
export const VIEW_MARKER = "<!-- mesh-delivery:view -->";
/** The view's body, marked: mesh-delivery writes it, this keeps exactly one of them per pull request. */
export function viewBody(body: string): string {
const b = String(body ?? "");
return b.includes(VIEW_MARKER) ? b : `${VIEW_MARKER}\n${b}`;
}
/** Which comment is the view: the first that carries the marker, or none yet. */
export function viewComment<T extends { id: number; body: string }>(comments: T[]): T | undefined {
return comments.find((c) => c.body.includes(VIEW_MARKER));
}
const states = new Set(["pending", "success", "error", "failure", "warning"]);
/** A status mesh-delivery asks for, checked: one of the forge's states, a context of the mesh's own, a
* bounded description. */
export function deliveryStatus(context: string, state: string, description: string, target?: string) {
if (!/^mesh\/[a-z-]+$/.test(context)) throw new Error(`${context} is not a status of the mesh's own`);
if (!states.has(state)) throw new Error(`${state} is not a status the forge keeps`);
let d = String(description ?? "").replace(/\s+/g, " ").trim();
if (d.length > 140) d = d.slice(0, 139) + "…";
return { state: state as "pending" | "success" | "error" | "failure" | "warning", context, description: d,
...(target && /^https?:\/\//.test(target) ? { target_url: target } : {}) };
}