104 lines
5.1 KiB
TypeScript
104 lines
5.1 KiB
TypeScript
// **One writer of a head's mesh/merge-gate** (novox/hq issue 446).
|
|
//
|
|
// A pending check says where it is: "waiting in the build queue since …" when the head is announced. The forge keeps only
|
|
// the newest status of a context, so two writers cannot order their writes; this module is the one writer of
|
|
// mesh/merge-gate, and every write of one head's gate goes through one chain, in the order it was asked. A recheck's
|
|
// pending (novox/hq issue 456) and a trunk's move (ADR 0323 §4) take their turns on the same chain. No verdict is written
|
|
// here any more: the controller's `checked` is no event (ADR 0571 clause 13), and main requires the checker's mesh/check.
|
|
|
|
import { CHECK_CONTEXT, pendingWords } from "./pulls.js";
|
|
import type { CommitStatus, HeadCheck } from "./client.js";
|
|
|
|
/** What the gate writer needs of the forge: a status set, and a head's statuses as the forge holds them. */
|
|
export interface GateForge {
|
|
setCommitStatus(owner: string, repo: string, sha: string, status: CommitStatus): Promise<void>;
|
|
/** The head's statuses by context, the newest of each, with their words (novox/hq ADR 0323). */
|
|
headChecks(owner: string, repo: string, sha: string): Promise<Record<string, HeadCheck>>;
|
|
}
|
|
|
|
/** Whether a repository as the mesh clones it (`http://forge/novox/hq.git`) is owner/repo. */
|
|
export function clonedAs(url: string | undefined, owner: string, repo: string): boolean {
|
|
const u = (url ?? "").trim().toLowerCase().replace(/\.git$/, "");
|
|
const full = `${owner}/${repo}`.toLowerCase();
|
|
return u === full || u.endsWith("/" + full);
|
|
}
|
|
|
|
interface Head {
|
|
/** The chain the head's writes take their turns on. */
|
|
chain: Promise<void>;
|
|
}
|
|
|
|
export class GateWriter {
|
|
private heads = new Map<string, Head>();
|
|
private forge: GateForge;
|
|
private now: () => Date;
|
|
|
|
constructor(forge: GateForge, now: () => Date = () => new Date()) {
|
|
this.forge = forge;
|
|
this.now = now;
|
|
}
|
|
|
|
private head(owner: string, repo: string, sha: string): Head {
|
|
const key = `${owner}/${repo}@${sha}`.toLowerCase();
|
|
let h = this.heads.get(key);
|
|
if (!h) {
|
|
h = { chain: Promise.resolve() };
|
|
this.heads.set(key, h);
|
|
// Kept for the heads still in play; the oldest go first, past what any forge holds open at once.
|
|
if (this.heads.size > 2000) this.heads.delete(this.heads.keys().next().value as string);
|
|
}
|
|
return h;
|
|
}
|
|
|
|
/** One turn on the head's chain; a turn that fails never stops the next. Its failure is the caller's. */
|
|
private turn(h: Head, fn: () => Promise<void>): Promise<void> {
|
|
const run = h.chain.then(fn);
|
|
h.chain = run.catch(() => {});
|
|
return run;
|
|
}
|
|
|
|
/** A turn whose failure is said here: a pending word not written is not worth the event again. */
|
|
private said(h: Head, what: string, fn: () => Promise<void>): Promise<void> {
|
|
return this.turn(h, fn).catch((err) => {
|
|
console.error(`[gitea] ${what} — ${err instanceof Error ? err.message : err}`);
|
|
});
|
|
}
|
|
|
|
/** The head announced: it waits in the build queue from now. A new check of the head, so no verdict yet. */
|
|
pending(owner: string, repo: string, sha: string): Promise<void> {
|
|
const h = this.head(owner, repo, sha);
|
|
const at = this.now();
|
|
return this.said(h, `${owner}/${repo}@${sha.slice(0, 8)}: could not say a check is pending`, () =>
|
|
this.forge.setCommitStatus(owner, repo, sha, { state: "pending", context: CHECK_CONTEXT, description: pendingWords(at) }));
|
|
}
|
|
|
|
/**
|
|
* A recheck's pending, asked through the gitea_commit_status tool (novox/hq issue 456): it takes its turn after every
|
|
* write already asked for, so it lands over them. Any other status of the merge check takes its turn on the same
|
|
* chain. A write that fails fails the caller.
|
|
*/
|
|
rechecked(owner: string, repo: string, sha: string, status: CommitStatus): Promise<void> {
|
|
return this.turn(this.head(owner, repo, sha), () => this.forge.setCommitStatus(owner, repo, sha, status));
|
|
}
|
|
|
|
/**
|
|
* **A trunk's move puts the head's verdict out of date** (novox/hq ADR 0323 §4): its merge check statuses are put back
|
|
* to pending, with the reason, in their turn after every write of the head already asked for — a verdict in flight
|
|
* lands first. What is written is decided in the turn, from the statuses the forge holds then (`decide`, trunk.ts's
|
|
* outOfDateStatuses): a check on its way, or a verdict already judged against the trunk's head, is left as it is. It
|
|
* opens no check — nothing is asked — so a verdict of a check asked before it keeps its weight. Answers what it wrote;
|
|
* a write that fails fails the caller, and the move is weighed again on the next pass.
|
|
*/
|
|
outOfDate(owner: string, repo: string, sha: string,
|
|
decide: (checks: Record<string, HeadCheck>) => CommitStatus[] | Promise<CommitStatus[]>, target?: string): Promise<CommitStatus[]> {
|
|
const h = this.head(owner, repo, sha);
|
|
const written: CommitStatus[] = [];
|
|
return this.turn(h, async () => {
|
|
for (const s of await decide(await this.forge.headChecks(owner, repo, sha))) {
|
|
await this.forge.setCommitStatus(owner, repo, sha, target ? { ...s, target_url: target } : s);
|
|
written.push(s);
|
|
}
|
|
}).then(() => written);
|
|
}
|
|
}
|