The merge tool emitted at the instant it acted; a merge made in the forge's own pages or over its API emitted nothing, and the mesh went on believing every module current with its source (novox/hq 04-ISSUES/131). Merged pull requests are now watched the way repositories are: what the forge holds, asked for on a tick, announced once, with the merge commit and the clone URL a build needs. What has been announced is kept beside the module's state, so a restart does not announce the whole history again, and a first tick with no record announces nothing.
132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
// gitea's events. The tool runtime imports this once the broker is bound. It watches the forge and
|
|
// emits what appeared.
|
|
//
|
|
// Emits (novox/hq ADR 0041/0042):
|
|
// module.gitea.repo.created — a repository appeared, however it was made (push, web UI, or tool)
|
|
//
|
|
// issue.opened and pull.merged are emitted from the tools (tools/index.ts), at the instant the mesh
|
|
// takes that action — the natural point, and one process only. repo.created belongs here instead:
|
|
// a repository is usually born from a `git push` or the web UI, which no tool sees, so polling the
|
|
// repo list is the only way to catch every path — and keeping it out of the create-repo tool means
|
|
// the fact is never announced twice from two processes.
|
|
//
|
|
// The polling is deliberately unhurried: an event a minute late is still an event, whereas hammering
|
|
// the forge for an immediacy nobody asked for is not.
|
|
|
|
import { emit } from "@novox/mesh-sdk/events";
|
|
import { GiteaClient } from "./client.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
|
|
// or reuses the token, so the runtime's start also shows what it did about it.
|
|
let gitea: GiteaClient | null = null;
|
|
try {
|
|
gitea = GiteaClient.fromEnv();
|
|
} catch (err) {
|
|
console.log(`[gitea] not watching — ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
|
|
// New repositories, by diffing the repo list. Primed silently on the first look, or a restart would
|
|
// re-announce every existing repository as freshly created.
|
|
const seen = new Set<string>();
|
|
let primed = false;
|
|
async function pollRepos(client: GiteaClient): Promise<void> {
|
|
const repos = await client.listRepos(1, 50);
|
|
for (const repo of repos) {
|
|
if (!seen.has(repo.full_name)) {
|
|
if (primed) {
|
|
await emit("repo.created", {
|
|
full_name: repo.full_name,
|
|
owner: repo.owner,
|
|
name: repo.name,
|
|
private: repo.private,
|
|
html_url: repo.html_url,
|
|
});
|
|
}
|
|
seen.add(repo.full_name);
|
|
}
|
|
}
|
|
primed = true;
|
|
}
|
|
|
|
// **A merge is announced whoever made it.** The merge tool below emits at the instant it acts; a
|
|
// merge made in the forge's own pages or over its API would emit nothing, and the mesh would go on
|
|
// believing every module current with its source (novox/hq 04-ISSUES/131). So merged pull requests
|
|
// are watched the way repositories are: what the forge holds, asked for on a tick, announced once.
|
|
// What has been announced is kept beside the module's state, so a restart does not announce the
|
|
// whole history again — and the first tick on a machine with no record announces nothing, because
|
|
// everything it sees then predates the watching.
|
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
const mergedRecord = process.env.MESH_GITEA_STATE_DIR ? join(process.env.MESH_GITEA_STATE_DIR, "merged-announced.json") : null;
|
|
const announced = new Set<string>();
|
|
let primedMerges = false;
|
|
if (mergedRecord && existsSync(mergedRecord)) {
|
|
try {
|
|
for (const sha of JSON.parse(readFileSync(mergedRecord, "utf8")) as string[]) announced.add(sha);
|
|
primedMerges = true;
|
|
} catch {
|
|
// An unreadable record is treated as no record: prime again rather than re-announce history.
|
|
}
|
|
}
|
|
function keepAnnounced(): void {
|
|
if (!mergedRecord) return;
|
|
mkdirSync(join(mergedRecord, ".."), { recursive: true });
|
|
const tmp = mergedRecord + ".tmp";
|
|
writeFileSync(tmp, JSON.stringify([...announced].slice(-2000)));
|
|
renameSync(tmp, mergedRecord);
|
|
}
|
|
async function pollMerged(client: GiteaClient): Promise<void> {
|
|
const repos = await client.listRepos(1, 50);
|
|
let changed = false;
|
|
for (const repo of repos) {
|
|
const pulls = await client.listPullRequests(repo.owner, repo.name, { state: "closed", sort: "recentupdate", limit: "20" });
|
|
for (const pull of pulls) {
|
|
if (!pull.merged || !pull.merge_commit_sha || announced.has(pull.merge_commit_sha)) continue;
|
|
if (primedMerges) {
|
|
await emit("pull.merged", {
|
|
owner: repo.owner,
|
|
repo: repo.name,
|
|
number: pull.number,
|
|
title: pull.title,
|
|
head: pull.head,
|
|
base: pull.base,
|
|
merge_commit_sha: pull.merge_commit_sha,
|
|
merged_at: pull.merged_at,
|
|
clone_url: repo.clone_url,
|
|
html_url: pull.html_url,
|
|
});
|
|
}
|
|
announced.add(pull.merge_commit_sha);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!primedMerges || changed) keepAnnounced();
|
|
primedMerges = true;
|
|
}
|
|
|
|
if (gitea) {
|
|
const client = gitea;
|
|
// A poll that fails says so once, not once a minute: the same reason repeating (the forge not up
|
|
// yet, the admin account refused on a restored forge) is one fact, and a recovery is worth a line.
|
|
let failing: string | null = null;
|
|
const tick = (fn: () => Promise<void>, everyMs: number): void => {
|
|
const run = (): void =>
|
|
void fn()
|
|
.then(() => {
|
|
if (failing !== null) console.log("[gitea] watching again");
|
|
failing = null;
|
|
})
|
|
.catch((err) => {
|
|
const why = err instanceof Error ? err.message : String(err);
|
|
if (why !== failing) console.error(`[gitea] not watching until this clears — ${why}`);
|
|
failing = why;
|
|
});
|
|
setInterval(run, everyMs);
|
|
run();
|
|
};
|
|
tick(() => pollRepos(client), 60_000);
|
|
tick(() => pollMerged(client), 30_000);
|
|
console.log("[gitea] watching for new repositories and merged pull requests");
|
|
}
|