From 975f1aa8e83dcb467b8c8691c4900dbd3dfddb59 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 11:32:33 +0100 Subject: [PATCH] GIT5: the forge's push webhook of a branch says branch-moved with the new head on the seat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §11, §111, §331 branch-moved Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- modules/gitea/seatevents.ts | 21 ++++++++++++++++----- modules/gitea/test/announce.test.ts | 8 +++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/modules/gitea/seatevents.ts b/modules/gitea/seatevents.ts index 8b859f846..4d49da269 100644 --- a/modules/gitea/seatevents.ts +++ b/modules/gitea/seatevents.ts @@ -10,9 +10,9 @@ import { seatPublish } from "@novox/mesh-sdk/runtime"; import { forgeDeleteSubject, forgePullSubject, forgeStatusSubject, pullClosedSubject, pullMergedSubject, pullUpdatedSubject, - repoWritableSubject, + repoWritableSubject, branchMovedSubject, type Change, type ForgeDeleteEvent, type ForgePullEvent, type ForgeStatusEvent, type PullClosedEvent, type PullMergedEvent, - type PullUpdatedEvent, type RepoWritableEvent, + 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). */ @@ -36,8 +36,18 @@ function changeOf(b: Body): Change | 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): { subject: string; body: unknown } | undefined { +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)), @@ -82,7 +92,8 @@ export function seatEventOf(type: string, b: Body): { subject: string; body: unk return undefined; } -/** The module's own events the seat says instead: never said as the module's (its `emits` names none of them). */ +/** 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; @@ -91,6 +102,6 @@ export type Say = (subject: string, body: unknown, id?: string) => Promise export async function sayOnSeat(type: string, body: Body, id = "", say: Say = seatPublish): Promise { const e = seatEventOf(type, body); if (!e) return false; - await say(e.subject, e.body, id); + await say(e.subject, e.body, id || e.id); return true; } diff --git a/modules/gitea/test/announce.test.ts b/modules/gitea/test/announce.test.ts index 7a3c95eb6..a40ff4a3d 100644 --- a/modules/gitea/test/announce.test.ts +++ b/modules/gitea/test/announce.test.ts @@ -45,13 +45,19 @@ test("an announcement is said on the seat git with its id, in the contract's bod ["repo.writable", { owner: "novox", repo: "app", account: "mesh-tools" }, "mesh.seat.git.event.repo-writable"], ["forge.pull-request.synchronized", { full_name: "novox/app", number: 7, head_sha: "aaa111" }, "mesh.seat.git.event.forge-pull"], ["forge.pull-request.labeled", { full_name: "novox/app", number: 7 }, undefined], - ["forge.push", { full_name: "novox/app" }, undefined], + ["forge.push", { full_name: "novox/app", ref: "refs/tags/v1", after: "a".repeat(40) }, undefined], + ["forge.push", { full_name: "novox/app", ref: "refs/heads/gone", after: "0".repeat(40) }, undefined], + ["forge.push", { full_name: "novox/app", ref: "refs/heads/feat/x", after: "a".repeat(40) }, "mesh.seat.git.event.branch-moved"], ]; for (const [type, body, subject] of rows) assert.equal(seatEventOf(type, body)?.subject, subject, type); assert.deepEqual(seatEventOf("pull.updated", updated)?.body, { repository: "novox/app", number: 7, base: "main", branch: "", head: "aaa111", "base-head": "ttt333" }); assert.equal(seatEventOf("forge.pull-request.edited", { full_name: "novox/app", number: 7, body: "x" })?.body && "body" in (seatEventOf("forge.pull-request.edited", { full_name: "novox/app", number: 7, body: "x" })!.body as object), false); + // A push of a branch (a merge and an admitted push alike) says branch-moved with the new head, one id per move. + const moved = seatEventOf("forge.push", { full_name: "novox/app", ref: "refs/heads/main", after: "b".repeat(40) }, () => new Date("2026-11-11T10:00:00Z")); + assert.deepEqual(moved?.body, { repository: "novox/app", branch: "main", head: "b".repeat(40), at: "2026-11-11T10:00:00.000Z" }); + assert.equal(moved?.id, `moved:novox/app:main@${"b".repeat(40)}`); assert.deepEqual([...SEAT_ONLY].sort(), ["forge.delete", "forge.status", "pull.closed", "pull.merged", "pull.updated", "repo.writable"]); });