Announce a pull request opened after its push, and say one nobody checks (hq issue 347)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
The announcer read only the repositories whose update time moved since its last pass, less a minute. Opening, reopening or retargeting a pull request does not move a repository's update time, so a pull request opened more than a minute after its push was never announced, never checked, and nothing said so (mesh-sdk #14, 2026-10-09). - Each pass also looks at every repository holding a pull request updated since the floor, by the forge's own search; every ten minutes, or when the search fails, at every repository. - Every five minutes the forge is asked for every open pull request; one on a base requiring mesh/merge-gate whose head has had none for ten minutes is said as the forge failing that pull request's merge check (a provider standing, ADR 0224), which the controller keeps as a condition, and is recovered when a check arrives or it closes. - The client refuses a pull request or issue number that is not one before asking the forge (a misnamed tool argument sent pulls/NaN).
This commit is contained in:
+47
-1
@@ -9,7 +9,8 @@
|
||||
//
|
||||
// Pure functions here, so they are tested without a forge; index.ts does the asking and the setting.
|
||||
|
||||
import type { CommitStatus, GiteaPull } from "./client.js";
|
||||
import type { CommitStatus, GiteaPull, GiteaRepo } from "./client.js";
|
||||
import { movedSince } from "./client.js";
|
||||
|
||||
/** The status context a merge check's gate is kept under: one per commit, the newest replacing the last. */
|
||||
export const CHECK_CONTEXT = "mesh/merge-gate";
|
||||
@@ -92,6 +93,51 @@ export function headsToAnnounce(full: string, pulls: GiteaPull[], announced: Ann
|
||||
return pulls.filter((p) => p.state === "open" && !!p.head_sha && announced[`${full}#${p.number}`] !== p.head_sha);
|
||||
}
|
||||
|
||||
/** A pull request the forge found updated, by its repository: what `searchOpenPulls` answers. */
|
||||
export interface PullSeen {
|
||||
owner: string;
|
||||
name: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/** **The repositories a pass looks at** (novox/hq issue 347): every one that moved since the floor, every one
|
||||
* holding a pull request updated since the floor — opening, reopening or retargeting a pull request moves its own
|
||||
* update time and not its repository's — and every one on a sweep or the first pass. */
|
||||
export function reposToLook(repos: GiteaRepo[], floor: string, pulls: PullSeen[], sweep: boolean): GiteaRepo[] {
|
||||
if (sweep || !floor) return repos;
|
||||
const moved = new Set(movedSince(repos, floor).map((r) => r.full_name));
|
||||
const at = Date.parse(floor);
|
||||
for (const p of pulls) {
|
||||
if (!p.updated_at || !(Date.parse(p.updated_at) < at)) moved.add(`${p.owner}/${p.name}`);
|
||||
}
|
||||
return repos.filter((r) => moved.has(r.full_name));
|
||||
}
|
||||
|
||||
/** How long a head may go without a merge check before it is said: the announcer's passes are half a minute
|
||||
* apart and the check is asked at once, so ten minutes is a fault, never a queue. */
|
||||
export const UNANNOUNCED_AFTER_MS = 10 * 60_000;
|
||||
|
||||
/** An open pull request as the watcher of announcements reads it. */
|
||||
export interface OpenPull {
|
||||
/** `owner/repo#number`. */
|
||||
key: string;
|
||||
base: string;
|
||||
head_sha: string;
|
||||
/** When its head last moved, or it was opened or retargeted. */
|
||||
updated_at?: string;
|
||||
/** The status contexts its head has, any state. */
|
||||
statuses: string[];
|
||||
/** The contexts its base branch's protection requires. */
|
||||
required: string[];
|
||||
}
|
||||
|
||||
/** The open pull requests whose base requires the merge check and whose head has had none for ten minutes
|
||||
* (novox/hq issue 347): announced by nobody, checked by nothing, and blocked from merging with no word why. */
|
||||
export function unannounced(pulls: OpenPull[], now: number, after = UNANNOUNCED_AFTER_MS): OpenPull[] {
|
||||
return pulls.filter((p) => p.required.includes(CHECK_CONTEXT) && !p.statuses.includes(CHECK_CONTEXT) &&
|
||||
!!p.updated_at && now - Date.parse(p.updated_at) >= after);
|
||||
}
|
||||
|
||||
// **A required status is success or it blocks** (novox/hq issue 293). The forge combines `warning` as a
|
||||
// failure, and a branch whose protection requires mesh/merge-gate or mesh/repo-check — with no
|
||||
// administrator override — will not merge past one. So the controller's `warning`, a note and never a
|
||||
|
||||
Reference in New Issue
Block a user