Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ea4961980 | ||
|
|
2b8a668d06 | ||
|
|
719fb1e025 | ||
|
|
ac5630bee2 | ||
|
|
1c995fa9fc | ||
|
|
d70cb18ea0 | ||
|
|
1d71787896 |
+16
-2
@@ -121,9 +121,23 @@ export class GiteaClient {
|
|||||||
|
|
||||||
// ---- Repositories ----
|
// ---- 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[]> {
|
async listRepos(page = 1, limit = 20): Promise<GiteaRepo[]> {
|
||||||
const repos = await this.request<any[]>(`/user/repos?page=${page}&limit=${limit}`);
|
const found = await this.request<{ data?: any[] }>(`/repos/search?page=${page}&limit=${limit}`);
|
||||||
return (repos ?? []).map(GiteaClient.mapRepo);
|
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: {
|
async createRepo(data: {
|
||||||
|
|||||||
+20
-5
@@ -31,7 +31,7 @@ try {
|
|||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
let primed = false;
|
let primed = false;
|
||||||
async function pollRepos(client: GiteaClient): Promise<void> {
|
async function pollRepos(client: GiteaClient): Promise<void> {
|
||||||
const repos = await client.listRepos(1, 50);
|
const repos = await client.listAllRepos();
|
||||||
for (const repo of repos) {
|
for (const repo of repos) {
|
||||||
if (!seen.has(repo.full_name)) {
|
if (!seen.has(repo.full_name)) {
|
||||||
if (primed) {
|
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 mergedRecord = process.env.MESH_GITEA_STATE_DIR ? join(process.env.MESH_GITEA_STATE_DIR, "merged-announced.json") : null;
|
||||||
const announced = new Set<string>();
|
const announced = new Set<string>();
|
||||||
let primedMerges = false;
|
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)) {
|
if (mergedRecord && existsSync(mergedRecord)) {
|
||||||
try {
|
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;
|
primedMerges = true;
|
||||||
} catch {
|
} catch {
|
||||||
// An unreadable record is treated as no record: prime again rather than re-announce history.
|
// 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;
|
if (!mergedRecord) return;
|
||||||
mkdirSync(join(mergedRecord, ".."), { recursive: true });
|
mkdirSync(join(mergedRecord, ".."), { recursive: true });
|
||||||
const tmp = mergedRecord + ".tmp";
|
const tmp = mergedRecord + ".tmp";
|
||||||
writeFileSync(tmp, JSON.stringify([...announced].slice(-2000)));
|
writeFileSync(tmp, JSON.stringify({ announced: [...announced].slice(-2000), since }));
|
||||||
renameSync(tmp, mergedRecord);
|
renameSync(tmp, mergedRecord);
|
||||||
}
|
}
|
||||||
async function pollMerged(client: GiteaClient): Promise<void> {
|
async function pollMerged(client: GiteaClient): Promise<void> {
|
||||||
const repos = await client.listRepos(1, 50);
|
const repos = await client.listAllRepos();
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const repo of repos) {
|
for (const repo of repos) {
|
||||||
const pulls = await client.listPullRequests(repo.owner, repo.name, { state: "closed", sort: "recentupdate", limit: "20" });
|
const pulls = await client.listPullRequests(repo.owner, repo.name, { state: "closed", sort: "recentupdate", limit: "20" });
|
||||||
for (const pull of pulls) {
|
for (const pull of pulls) {
|
||||||
if (!pull.merged || !pull.merge_commit_sha || announced.has(pull.merge_commit_sha)) continue;
|
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", {
|
await emit("pull.merged", {
|
||||||
owner: repo.owner,
|
owner: repo.owner,
|
||||||
repo: repo.name,
|
repo: repo.name,
|
||||||
@@ -96,11 +107,15 @@ async function pollMerged(client: GiteaClient): Promise<void> {
|
|||||||
clone_url: repo.clone_url,
|
clone_url: repo.clone_url,
|
||||||
html_url: pull.html_url,
|
html_url: pull.html_url,
|
||||||
});
|
});
|
||||||
|
// Said, because a trigger that fires silently is indistinguishable from one that did not
|
||||||
|
// fire (novox/hq 04-ISSUES/131) — this line is how an operator knows the mesh was told.
|
||||||
|
console.log(`[gitea] announced merge ${repo.full_name}#${pull.number} (${pull.merge_commit_sha.slice(0, 8)}) into ${pull.base}`);
|
||||||
}
|
}
|
||||||
announced.add(pull.merge_commit_sha);
|
announced.add(pull.merge_commit_sha);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!primedMerges) since = new Date().toISOString();
|
||||||
if (!primedMerges || changed) keepAnnounced();
|
if (!primedMerges || changed) keepAnnounced();
|
||||||
primedMerges = true;
|
primedMerges = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user