// What the forge's events ask of the announcer, and the one pass at a start (issue 1422, notes 10 and 11). Nothing polls the // forge: a head or merge is announced when its event is heard, and a pass over every repository runs once when the module // starts and again when the receiver is listening, so what happened while it was down is announced too. // // Apart from index.ts, which has side effects at import, so these are tested with the announcers injected. import type { GiteaPull, GiteaRepo } from "./client.js"; import type { Announced } from "./pulls.js"; import { headKey, headsToAnnounce } from "./pulls.js"; import type { ForgeEvent } from "./webhook.js"; export interface ActDeps { getRepo(owner: string, repo: string): Promise; getPull(owner: string, repo: string, number: number): Promise

; announceHead(repo: R, pull: P): Promise; announceMerge(repo: R, pull: P): Promise; announceClosed(repo: R, pull: P): Promise; siblingMoves(full: string, number: number): Promise; } const HEAD_EVENTS = ["forge.pull-request.opened", "forge.pull-request.synchronized", "forge.pull-request.reopened"]; /** * What the forge's event asks of the announcer at once: a new head is announced, a merge, a close without a merge, and a * pull request retargeted (an edit: the head is announced into the base it now has — the base is in the head's key, so an * edit that changed nothing about the base announces nothing). */ export async function actOn(event: ForgeEvent, d: ActDeps): Promise { const b = event.body as { owner?: string; repo?: string; number?: number; merged?: boolean }; if (!b.owner || !b.repo || !b.number) return; const edited = event.name === "forge.pull-request.edited"; const closed = event.name === "forge.pull-request.closed"; const head = HEAD_EVENTS.includes(event.name) || edited; const merged = event.name === "forge.pull-request.merged"; if (!head && !merged && !closed) return; if (closed && b.merged) return; // a merge is weighed with its own event (ADR 0382 §5) const [repo, pull] = await Promise.all([d.getRepo(b.owner, b.repo), d.getPull(b.owner, b.repo, b.number)]); if (closed) { await d.announceClosed(repo, pull); await d.siblingMoves(`${b.owner}/${b.repo}`, b.number); return; } if (head && pull.state === "open") await d.announceHead(repo, pull); if (merged) await d.announceMerge(repo, pull); } /** * The one pass at a start, and its retry: run now; if it fails, say so and run again after `retryMs`, until one pass has * ended. A retry of a failed pass, never a poll: after a pass has ended nothing is scheduled. */ export function startPass(run: () => Promise, opts: { retryMs: number; log?: (l: string) => void }): Promise { const log = opts.log ?? ((l) => console.error(`[gitea] ${l}`)); return run().catch((err) => { log(`the pass over every repository failed, asked again in ${opts.retryMs / 1000} s — ${err instanceof Error ? err.message : err}`); return new Promise((resolve) => setTimeout(() => resolve(startPass(run, opts)), opts.retryMs)); }); } export interface HeadDeps { /** Whether the forge shows a merge check of the head: only an announced head has one. */ alreadyChecked(pull: GiteaPull): Promise; /** Announce it; true when it was this call that announced it (not an event before it). */ announceHead(pull: GiteaPull): Promise; /** Told of a head this pass announced that no event had. */ missed(what: string): void; } /** * The heads of one repository's open pull requests a pass announces. A head is announced when it is new and fresh (moved in * the last day); at the first pass after a start (`primed` false) one the forge shows a merge check of was announced * already, and is only remembered. */ export async function announceNewHeads(full: string, open: GiteaPull[], heads: Announced, primed: boolean, dayAgo: string, d: HeadDeps): Promise { for (const pull of headsToAnnounce(full, open, heads)) { const key = headKey(full, pull.number, pull.base); const fresh = primed || (!!pull.updated_at && pull.updated_at > dayAgo); if (fresh && !primed && await d.alreadyChecked(pull)) { heads[key] = String(pull.head_sha); continue; } if (fresh) { if (await d.announceHead(pull)) d.missed(`${key} at ${String(pull.head_sha).slice(0, 8)}`); } else heads[key] = String(pull.head_sha); } } export type { GiteaRepo };