From 2b8a668d06e0c87ea49263e623f1e5ddc3b280f4 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 05:08:20 +0200 Subject: [PATCH] A merge older than the watching is history, not news An old merge past the first page of the forge's listing surfaced as newer pull requests were updated, and was announced as if it had just happened; the mesh then rebuilt everything built from that repository, once per old merge. The moment the watching began is kept with the record, and only a merge made since is announced. --- modules/gitea/index.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/gitea/index.ts b/modules/gitea/index.ts index 9db8955..096fb6b 100644 --- a/modules/gitea/index.ts +++ b/modules/gitea/index.ts @@ -61,9 +61,17 @@ 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(); let primedMerges = false; +// since is the moment the watching began: a merge made before it is history, whatever page of the +// forge's listing it surfaces on. Without it, an old merge past the first page — pushed into view +// as newer pull requests were updated — was announced as if it had just happened, and the mesh +// rebuilt everything built from that repository, once per old merge (2026-09-28). +let since = ""; if (mergedRecord && existsSync(mergedRecord)) { try { - for (const sha of JSON.parse(readFileSync(mergedRecord, "utf8")) as string[]) announced.add(sha); + const kept = JSON.parse(readFileSync(mergedRecord, "utf8")) as string[] | { announced: string[]; since: string }; + const list = Array.isArray(kept) ? kept : kept.announced; + for (const sha of list) announced.add(sha); + since = Array.isArray(kept) ? new Date().toISOString() : kept.since; primedMerges = true; } catch { // An unreadable record is treated as no record: prime again rather than re-announce history. @@ -73,7 +81,7 @@ function keepAnnounced(): void { if (!mergedRecord) return; mkdirSync(join(mergedRecord, ".."), { recursive: true }); const tmp = mergedRecord + ".tmp"; - writeFileSync(tmp, JSON.stringify([...announced].slice(-2000))); + writeFileSync(tmp, JSON.stringify({ announced: [...announced].slice(-2000), since })); renameSync(tmp, mergedRecord); } async function pollMerged(client: GiteaClient): Promise { @@ -83,7 +91,10 @@ async function pollMerged(client: GiteaClient): Promise { 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) { + // Announced only if merged since the watching began; recorded either way, so it is looked + // at once. + const fresh = !!pull.merged_at && !!since && pull.merged_at > since; + if (primedMerges && fresh) { await emit("pull.merged", { owner: repo.owner, repo: repo.name, @@ -101,6 +112,7 @@ async function pollMerged(client: GiteaClient): Promise { changed = true; } } + if (!primedMerges) since = new Date().toISOString(); if (!primedMerges || changed) keepAnnounced(); primedMerges = true; }