Compare commits

Author SHA1 Message Date
jschoubben 2b8a668d06 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.
2026-09-28 05:08:20 +02:00
mesh-admin 1c995fa9fc Merge pull request 'The forge watches every repository, not the administrator's own' (#125) from fix/the-forge-watches-every-repository into main 2026-09-28 02:31:25 +00:00
jschoubben d70cb18ea0 The forge watches every repository, not the administrator's own
/user/repos lists what the token's user owns, which for the mesh's administrator is nothing — so
the forge module watched an empty list and never announced a merge. It reads the forge's whole
view through the search endpoint, every page.
2026-09-28 04:31:20 +02:00
mesh-admin 1d71787896 Merge pull request 'The forge announces every merge, whoever made it' (#124) from feat/the-forge-announces-every-merge into main 2026-09-28 01:03:48 +00:00
2 changed files with 33 additions and 7 deletions
+16 -2
View File
@@ -121,9 +121,23 @@ export class GiteaClient {
// ---- Repositories ----
/** Every repository this token can see, one page. `/user/repos` is only what the token's own
* user owns — for the mesh's administrator that is nothing, which is how the forge watched an
* empty list and announced no merge (2026-09-28). The search endpoint is the forge's whole view. */
async listRepos(page = 1, limit = 20): Promise<GiteaRepo[]> {
const repos = await this.request<any[]>(`/user/repos?page=${page}&limit=${limit}`);
return (repos ?? []).map(GiteaClient.mapRepo);
const found = await this.request<{ data?: any[] }>(`/repos/search?page=${page}&limit=${limit}`);
return (found?.data ?? []).map(GiteaClient.mapRepo);
}
/** Every repository, all pages. */
async listAllRepos(): Promise<GiteaRepo[]> {
const all: GiteaRepo[] = [];
for (let page = 1; page < 100; page++) {
const batch = await this.listRepos(page, 50);
all.push(...batch);
if (batch.length < 50) break;
}
return all;
}
async createRepo(data: {
+17 -5
View File
@@ -31,7 +31,7 @@ try {
const seen = new Set<string>();
let primed = false;
async function pollRepos(client: GiteaClient): Promise<void> {
const repos = await client.listRepos(1, 50);
const repos = await client.listAllRepos();
for (const repo of repos) {
if (!seen.has(repo.full_name)) {
if (primed) {
@@ -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<string>();
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,17 +81,20 @@ 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<void> {
const repos = await client.listRepos(1, 50);
const repos = await client.listAllRepos();
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) {
// 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<void> {
changed = true;
}
}
if (!primedMerges) since = new Date().toISOString();
if (!primedMerges || changed) keepAnnounced();
primedMerges = true;
}