mesh/delivery merged: source.main moved to novox/mesh-catalog@774fcdfa93ad
mesh/check being carried to 470687ea4cce on 774fcdfa93ad | from 2a03c106f4b9
An edited pull request is announced into its new base at once; a close without a merge is announced by its event; the 30 s/5 min polls and the 10 min sweep are gone.
88 lines
4.5 KiB
TypeScript
88 lines
4.5 KiB
TypeScript
// 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<R, P> {
|
|
getRepo(owner: string, repo: string): Promise<R>;
|
|
getPull(owner: string, repo: string, number: number): Promise<P & { state?: string; merged?: boolean }>;
|
|
announceHead(repo: R, pull: P): Promise<unknown>;
|
|
announceMerge(repo: R, pull: P): Promise<unknown>;
|
|
announceClosed(repo: R, pull: P): Promise<unknown>;
|
|
siblingMoves(full: string, number: number): Promise<unknown>;
|
|
}
|
|
|
|
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<R, P>(event: ForgeEvent, d: ActDeps<R, P>): Promise<void> {
|
|
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<void>, opts: { retryMs: number; log?: (l: string) => void }): Promise<void> {
|
|
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<void>((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<boolean>;
|
|
/** Announce it; true when it was this call that announced it (not an event before it). */
|
|
announceHead(pull: GiteaPull): Promise<boolean>;
|
|
/** 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<void> {
|
|
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 };
|