Merge pull request 'The forge announces every merge, whoever made it' (#124) from feat/the-forge-announces-every-merge into main

This commit was merged in pull request #124.
This commit is contained in:
2026-09-28 01:03:48 +00:00
3 changed files with 70 additions and 1 deletions
+8
View File
@@ -9,6 +9,8 @@ import { ConfiguredToken, MintedToken, type TokenSource } from "./token.js";
/** A repository, trimmed to what the mesh cares about. */
export interface GiteaRepo {
full_name: string;
/** The URL a build clones — what a module records as its source. */
clone_url?: string;
name: string;
owner: string;
private: boolean;
@@ -34,6 +36,9 @@ export interface GiteaPull {
title: string;
state: string;
merged: boolean;
/** The commit the merge produced — what a build of the base branch is made from. */
merge_commit_sha?: string;
merged_at?: string;
user?: string;
head?: string;
base?: string;
@@ -232,6 +237,7 @@ export class GiteaClient {
private static mapRepo(r: any): GiteaRepo {
return {
full_name: r.full_name,
clone_url: r.clone_url ?? undefined,
name: r.name,
owner: r.owner?.login ?? r.full_name?.split("/")[0] ?? "unknown",
private: Boolean(r.private),
@@ -259,6 +265,8 @@ export class GiteaClient {
title: p.title,
state: p.state,
merged: Boolean(p.merged),
merge_commit_sha: p.merge_commit_sha ?? undefined,
merged_at: p.merged_at ?? undefined,
user: p.user?.login,
head: p.head?.ref,
base: p.base?.ref,
+58 -1
View File
@@ -49,6 +49,62 @@ async function pollRepos(client: GiteaClient): Promise<void> {
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
@@ -70,5 +126,6 @@ if (gitea) {
run();
};
tick(() => pollRepos(client), 60_000);
console.log("[gitea] watching for new repositories");
tick(() => pollMerged(client), 30_000);
console.log("[gitea] watching for new repositories and merged pull requests");
}
+4
View File
@@ -231,6 +231,8 @@ export function getGiteaTools(gitea: GiteaClient): ToolDefinition[] {
// Read the PR first, so the merged event carries a title and branches, not just a number.
const pull = await gitea.getPullRequest(owner, repo, number);
await gitea.mergePullRequest(owner, repo, number, method, deleteBranch);
// Read it again: the merge commit only exists now, and it is what a build is made from.
const merged = await gitea.getPullRequest(owner, repo, number);
await emit("pull.merged", {
owner,
repo,
@@ -238,6 +240,8 @@ export function getGiteaTools(gitea: GiteaClient): ToolDefinition[] {
title: pull.title,
head: pull.head,
base: pull.base,
merge_commit_sha: merged.merge_commit_sha,
merged_at: merged.merged_at,
method,
html_url: pull.html_url,
});