Files
mesh-catalog/modules/gitea/seatevents.ts
T

108 lines
6.7 KiB
TypeScript

// The seat git's events, said by its holder (novox/hq design 47 *The seat git*): a pull request's new head, its merge and
// its close (pull.updated, pull.merged, pull.closed), the forge's own webhook of a pull request, a status and a deleted
// branch (forge.pull, forge.status, forge.delete), and a repository the forge can write again (repo.writable). Each is
// said on the seat's subject through the runtime (mesh-sdk `runtime.seatPublish`, `mesh/seat.publish`), in the body the
// seat's contract types, never as this module's own event: its hearers consume the seat, not the forge module.
//
// What the module's paths make (the poll, the webhook, the account's writes) is read here once and turned into the
// contract's body; a pull request's description is never carried (ADR 0659 §1).
import { seatPublish } from "@novox/mesh-sdk/runtime";
import {
forgeDeleteSubject, forgePullSubject, forgeStatusSubject, pullClosedSubject, pullMergedSubject, pullUpdatedSubject,
repoWritableSubject, branchMovedSubject,
type Change, type ForgeDeleteEvent, type ForgePullEvent, type ForgeStatusEvent, type PullClosedEvent, type PullMergedEvent,
type PullUpdatedEvent, type RepoWritableEvent, type BranchMovedEvent,
} from "@novox/mesh-delivery-contracts-git";
/** The forge's pull request actions the seat's forge.pull says (the contract's word list). */
export const PULL_ACTIONS = ["opened", "reopened", "synchronized", "edited", "closed", "merged"];
type Body = Record<string, any>;
const s = (v: unknown): string | undefined => (typeof v === "string" && v !== "" ? v : undefined);
const opt = <K extends string>(k: K, v: unknown) => (v === undefined || v === null || v === "" ? {} : { [k]: v } as Record<K, unknown>);
function repositoryOf(b: Body): string {
const full = s(b.full_name) ?? (s(b.owner) && s(b.repo) ? `${b.owner}/${b.repo}` : undefined);
if (!full) throw new Error("the event names no repository");
return full;
}
function changeOf(b: Body): Change | undefined {
if (!Array.isArray(b.paths)) return undefined;
return { paths: b.paths.map(String), ...opt("paths-truncated", b.paths_truncated === true ? true : undefined),
...opt("removed", Array.isArray(b.removed) && b.removed.length ? b.removed.map(String) : undefined),
...opt("module-dirs", Array.isArray(b.module_dirs) ? b.module_dirs.map(String) : undefined) };
}
/** The seat's subject and body for one of the module's announcements, or undefined when the seat says no such event. */
export function seatEventOf(type: string, b: Body, now: () => Date = () => new Date()): { subject: string; body: unknown; id?: string } | undefined {
switch (type) {
case "forge.push": {
// A branch moved, by a merge or an admitted push alike (design 47 §11): the forge's push webhook of a branch with a
// head after it. A tag's push, and a branch's deletion (the head is the zero hash), move no branch.
const ref = s(b.ref) ?? "";
const head = s(b.after) ?? "";
if (!ref.startsWith("refs/heads/") || !/^[0-9a-f]{40,64}$/.test(head) || /^0+$/.test(head)) return undefined;
const branch = ref.slice("refs/heads/".length);
const body: BranchMovedEvent = { repository: repositoryOf(b), branch, head, at: now().toISOString() };
return { subject: branchMovedSubject, body, id: `moved:${body.repository}:${branch}@${head}` };
}
case "pull.updated": {
const body: PullUpdatedEvent = { repository: repositoryOf(b), number: Number(b.number), base: String(b.base ?? ""),
branch: String(b.head ?? ""), head: String(b.head_sha ?? ""), ...opt("base-head", s(b.base_sha)),
...opt("author", s(b.author)), ...opt("title", s(b.title)), ...opt("url", s(b.html_url)), ...opt("change", changeOf(b)) };
return { subject: pullUpdatedSubject, body };
}
case "pull.merged": {
const body: PullMergedEvent = { repository: repositoryOf(b), number: Number(b.number), base: String(b.base ?? ""),
branch: String(b.head ?? ""), head: String(b.head_sha ?? ""), "merge-commit": String(b.merge_commit_sha ?? ""),
...opt("merged-at", s(b.merged_at)), ...opt("head-checks", b.head_checks), ...opt("author", s(b.author)),
...opt("title", s(b.title)), ...opt("url", s(b.html_url)), ...opt("change", changeOf(b)) };
return { subject: pullMergedSubject, body };
}
case "pull.closed": {
const body: PullClosedEvent = { repository: repositoryOf(b), number: Number(b.number), base: String(b.base ?? ""),
branch: String(b.head ?? ""), head: String(b.head_sha ?? ""), ...opt("author", s(b.author)), ...opt("title", s(b.title)),
...opt("url", s(b.html_url)) };
return { subject: pullClosedSubject, body };
}
case "forge.status": {
const body: ForgeStatusEvent = { repository: repositoryOf(b), commit: String(b.sha ?? ""), context: String(b.context ?? ""),
state: String(b.state ?? ""), ...opt("description", s(b.description)), ...opt("sender", s(b.sender)) };
return { subject: forgeStatusSubject, body };
}
case "forge.delete": {
const body: ForgeDeleteEvent = { repository: repositoryOf(b), ref: String(b.ref ?? ""), ...opt("ref-type", s(b.ref_type)),
...opt("sender", s(b.sender)) };
return { subject: forgeDeleteSubject, body };
}
case "repo.writable": {
const body: RepoWritableEvent = { repository: repositoryOf(b), account: String(b.account ?? "") };
return { subject: repoWritableSubject, body };
}
}
const action = type.startsWith("forge.pull-request.") ? type.slice("forge.pull-request.".length) : "";
if (PULL_ACTIONS.includes(action)) {
const body: ForgePullEvent = { repository: repositoryOf(b), number: Number(b.number), action,
...opt("base", s(b.base)), ...opt("branch", s(b.head)), ...opt("head", s(b.head_sha)), ...opt("author", s(b.author)),
...opt("merge-commit", s(b.merge_commit_sha)), ...(b.merged === true ? { merged: true } : {}), ...opt("sender", s(b.sender)) };
return { subject: forgePullSubject, body };
}
return undefined;
}
/** The module's own events the seat says instead: never said as the module's (its `emits` names none of them). `forge.push`
* stays the module's own too: the seat's branch-moved is said beside it. */
export const SEAT_ONLY = new Set(["pull.updated", "pull.merged", "pull.closed", "forge.status", "forge.delete", "repo.writable"]);
export type Say = (subject: string, body: unknown, id?: string) => Promise<void>;
/** Say an announcement on the seat, de-duplicated by `id` (the same announcement by two paths is one message). */
export async function sayOnSeat(type: string, body: Body, id = "", say: Say = seatPublish): Promise<boolean> {
const e = seatEventOf(type, body);
if (!e) return false;
await say(e.subject, e.body, id || e.id);
return true;
}