Say a pull request nobody checks as a delivery held, owned by mesh-delivery (hq issue 347)
mesh/merge-gate pass: builds gitea, mesh-delivery → 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 delivery to the same trunk took over its walk
mesh/merge-gate pass: builds gitea, mesh-delivery → 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 delivery to the same trunk took over its walk
The confirmation review asked for the delivery domain's own vocabulary instead of a provider standing. A gitea health check would need a `tools` list (turning gitea's tools into seat verbs), a check of another kind beside it, and would hold the forge's own deliveries at the first-node gate; so the delivery's owner says it, with the word it already has. - gitea_open_pulls (gitea): every open pull request from the forge itself, with what its base requires and the statuses its head has. - mesh-delivery asks it every five minutes and keeps its own first-seen time per head (the forge's update time moves on every comment). A head on a base requiring mesh/merge-gate OR mesh/repo-check (hq requires only the second) with neither for ten minutes is a `stalled` line in state `unannounced`, which the controller raises as delivery.<id>.stalled (D14); H2 has nothing to close. Once announced it is a delivery of its own. - The gitea-side watcher and its provider standing are gone.
This commit is contained in:
+1
-64
@@ -28,7 +28,7 @@
|
||||
import { emit, on } from "@novox/mesh-sdk/events";
|
||||
import { GiteaClient, movedSince } from "./client.js";
|
||||
import { CHECK_CONTEXT, REPO_CHECK_CONTEXT, commentFor, headsToAnnounce, statusesFor, type Announced, type Checked,
|
||||
type RepoCheckFacts, reposToLook, unannounced, type OpenPull } from "./pulls.js";
|
||||
type RepoCheckFacts, reposToLook } from "./pulls.js";
|
||||
|
||||
// Without a way to a token — configured, or mintable with the admin account (token.ts) — there is
|
||||
// nothing to watch; log and stay quiet rather than crash the runtime. With one, the first poll mints
|
||||
@@ -290,68 +290,6 @@ async function pollPulls(client: GiteaClient): Promise<void> {
|
||||
lastPullLook = began;
|
||||
}
|
||||
|
||||
// **An open pull request nobody checks is said** (novox/hq issue 347). One whose base requires the merge check and
|
||||
// whose head has had none for ten minutes was announced by nobody — the fault this watcher exists for, which went
|
||||
// unseen while the pull request could not merge. Said as this forge failing the merge check, the provider standing
|
||||
// the controller keeps as a condition (ADR 0224) and says to the operator; said again every pass while it lasts, and
|
||||
// recovered when a check arrives or the pull request is no longer open. Asked of the forge itself, never of what
|
||||
// the announcer remembers, so a fault in the announcer cannot hide itself.
|
||||
const unannouncedRecord = process.env.MESH_GITEA_STATE_DIR ? join(process.env.MESH_GITEA_STATE_DIR, "pulls-unannounced.json") : null;
|
||||
let failing: Record<string, string> = {};
|
||||
if (unannouncedRecord && existsSync(unannouncedRecord)) {
|
||||
try {
|
||||
failing = (JSON.parse(readFileSync(unannouncedRecord, "utf8")) as { failing: Record<string, string> }).failing ?? {};
|
||||
} catch {
|
||||
// Unreadable is no record: a pull request still unchecked is said again at once.
|
||||
}
|
||||
}
|
||||
function keepFailing(): void {
|
||||
if (!unannouncedRecord) return;
|
||||
mkdirSync(join(unannouncedRecord, ".."), { recursive: true });
|
||||
const tmp = unannouncedRecord + ".tmp";
|
||||
writeFileSync(tmp, JSON.stringify({ failing }));
|
||||
renameSync(tmp, unannouncedRecord);
|
||||
}
|
||||
const consumerOf = (key: string) => `the merge check of ${key}`;
|
||||
async function watchUnannounced(client: GiteaClient): Promise<void> {
|
||||
const open: OpenPull[] = [];
|
||||
const rules = new Map<string, Awaited<ReturnType<GiteaClient["branchProtections"]>> | undefined>();
|
||||
for (const found of await client.searchOpenPulls()) {
|
||||
const full = `${found.owner}/${found.name}`;
|
||||
const pull = await client.getPullRequest(found.owner, found.name, found.number);
|
||||
if (pull.state !== "open" || !pull.head_sha) continue;
|
||||
if (!rules.has(full)) rules.set(full, await client.branchProtections(found.owner, found.name).catch(() => undefined));
|
||||
const rule = rules.get(full)?.find((p) => (p.rule_name ?? p.branch_name) === (pull.base || "main"));
|
||||
const statuses = Object.keys(await client.commitStatuses(found.owner, found.name, pull.head_sha));
|
||||
open.push({ key: `${full}#${pull.number}`, base: pull.base ?? "", head_sha: pull.head_sha, updated_at: pull.updated_at,
|
||||
statuses, required: rule?.enable_status_check ? (rule.status_check_contexts ?? []) : [] });
|
||||
}
|
||||
const now = Date.now();
|
||||
const unchecked = new Map(unannounced(open, now).map((p) => [p.key, p] as const));
|
||||
const node = process.env.MESH_NODE ?? "";
|
||||
for (const [key, p] of unchecked) {
|
||||
const since = failing[key] ?? p.updated_at ?? new Date(now).toISOString();
|
||||
await emit("provisioner.failing", {
|
||||
provider: "git", "provider-node": node, consumer: consumerOf(key), node: "", class: "unannounced",
|
||||
error: `${key} is open on ${p.base}, which requires ${CHECK_CONTEXT}, and its head ${p.head_sha.slice(0, 12)} has had no ` +
|
||||
`merge check for ${Math.round((now - Date.parse(p.updated_at ?? since)) / 60_000)} minutes: the mesh was never asked to check it`,
|
||||
since, attempts: 1,
|
||||
});
|
||||
if (!failing[key]) console.error(`[gitea] ${key} has had no merge check for ten minutes: said to the mesh`);
|
||||
failing[key] = since;
|
||||
}
|
||||
for (const key of Object.keys(failing)) {
|
||||
if (unchecked.has(key)) continue;
|
||||
await emit("provisioner.recovered", {
|
||||
provider: "git", "provider-node": node, consumer: consumerOf(key), node: "", since: failing[key], attempts: 0,
|
||||
why: "it has a merge check, or is no longer open",
|
||||
});
|
||||
console.log(`[gitea] ${key} has a merge check now, or is no longer open`);
|
||||
delete failing[key];
|
||||
}
|
||||
keepFailing();
|
||||
}
|
||||
|
||||
// **The verdict, set where the pull request shows it.** Every verdict is the head commit's status; one
|
||||
// that is not a pass also leaves the check's own account as a comment, so the reason is read where the
|
||||
// change is reviewed. An error — the check could not run — is the forge's `error`, never a success.
|
||||
@@ -425,7 +363,6 @@ if (gitea) {
|
||||
tick(() => pollRepos(client), 60_000);
|
||||
tick(() => pollMerged(client), 30_000);
|
||||
tick(() => pollPulls(client), 30_000);
|
||||
tick(() => watchUnannounced(client), 5 * 60_000);
|
||||
await on("mesh-controller.checked", (event) => setVerdict(client, event));
|
||||
console.log("[gitea] watching for new repositories and merged pull requests");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user