Files
mesh-catalog/modules/gitea/announce.ts
T
jschoubben 32655b95f6 GIT1, GIT2, GIT3, GIT10: the forge module serves the seat git's verbs and says its events, retires its acting tools, follows the catalog half
The four share files (index.ts, module.json, guard.test.ts), so they land as one commit.
GIT1: seat.ts serves pull, open-pulls, tags, trees, file, contains, statuses, status, comment, view, note, merge-pull
as tools on the seat, each answering the contract's shape and an error of kind refused, not-found or failed; a comment,
view or note whose act id is held is answered held (note matched by id). merge-pull merges the head the caller names
(its from-G judgements are GIT6, GIT7, GIT9).
GIT2: seatevents.ts says pull-updated, pull-merged, pull-closed, forge-pull, forge-status, forge-delete and repo-writable on
mesh.seat.git.event.<event>; the module's emits drop them.
GIT3: merge.ts, check.ts, the acting tools, the owed watch, the gate's source read and check-gates-merge are gone.
GIT10: catalog.ts follows the seat mesh-catalog (revision, catalog-revision); no write is granted before the first view.

Builds: design 47 G §310, §311, §312, §313, §314, §163, §317, §278, §321, §196

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 11:31:24 +01:00

72 lines
3.6 KiB
TypeScript

// **The forge's announcements carry a message id** (novox/hq ADR 0406 §6 and §7, issue 976). A merge, a new head and a
// close are announced by more than one path — the poll, the forge's own event, the merge tool's ask of a check — and
// were kept from being announced twice by files of what had been announced. They are now announced with a message id
// that is the same for the same announcement, whichever path makes it: the runtime makes an event's `x-event-id` header
// the bus's message id (mesh-tools node-tools/internal/bus/bus.go, `nats.MsgId`), and the bus stores a message it has
// seen the id of, within its de-duplication window, once.
//
// They are the seat git's events (design 47 *The seat git*), said through the runtime's seat publish with that id.
//
// The ids: a merge by its merge commit, a head by the head and the trunk commit it was announced against (the check asked
// again against a new trunk commit is another announcement), a close by its head.
import { sayOnSeat } from "./seatevents.js";
import { state as keptState } from "@novox/mesh-sdk/state";
type Said = { owner?: string; repo?: string; number?: number; head_sha?: string; base_sha?: string; merge_commit_sha?: string };
/** The message id of an announcement, or undefined for an event that is not one of these three. */
export function announcementId(type: string, body: unknown): string | undefined {
const b = (body ?? {}) as Said;
const pull = `${b.owner}/${b.repo}#${b.number}`;
switch (type) {
case "pull.merged":
return `merged:${pull}@${b.merge_commit_sha ?? ""}`;
case "pull.updated":
return `updated:${pull}@${b.head_sha ?? ""}:${b.base_sha ?? ""}`;
case "pull.closed":
return `closed:${pull}@${b.head_sha ?? ""}`;
default:
return undefined;
}
}
/** Announce a merge, a head or a close: said on the seat git (seatevents.ts) with its message id. */
export async function announce(type: string, body: unknown): Promise<void> {
const id = announcementId(type, body);
if (!id) throw new Error(`${type} is not an announcement with a message id`);
if (!(await sayOnSeat(type, (body ?? {}) as Record<string, unknown>, id))) throw new Error(`the seat git says no ${type}`);
}
// --- the watermark: the one value of the old files that is not the bus's to remember --------------------------------
/** What the module keeps of how far its poll of merges has looked: a time, in its own state. */
interface Kept {
get(key: string): Promise<{ value: unknown } | null>;
put(key: string, value: unknown): Promise<number>;
}
const WATERMARK = "merges-since";
/** The time the poll of merges last looked from, or "" when none is kept or the state cannot be reached: a merge made
* before it is history, one after it is announced. */
export async function readWatermark(state: Kept = keptState("announced")): Promise<string> {
try {
const entry = await state.get(WATERMARK);
const since = (entry?.value as { since?: string } | undefined)?.since;
return typeof since === "string" ? since : "";
} catch (err) {
console.error(`[gitea] where the poll of merges last looked from could not be read, so none is kept — ${err instanceof Error ? err.message : err}`);
return "";
}
}
/** Keep the time the poll of merges looked from; a state that cannot be reached is said, and the next pass keeps it. */
export async function keepWatermark(since: string, state: Kept = keptState("announced")): Promise<void> {
try {
await state.put(WATERMARK, { since });
} catch (err) {
console.error(`[gitea] where the poll of merges looked from could not be kept — ${err instanceof Error ? err.message : err}`);
}
}