From 545c79a49ccdc42b33602d1e36a9f95a9e8f8a59 Mon Sep 17 00:00:00 2001
From: azank
Date: Tue, 13 Oct 2026 17:51:04 +0500
Subject: [PATCH 1/2] gallery: feature golang/go, which the hosted builder can
replay, in rust-lang/rust's place
rust-lang/rust (998 MB, under the 1.5 GB cap) runs out of memory at replay time, so
it never had a city and its featured slot stayed empty: the row showed nine cities.
golang/go, the other language repo of its kind, replays in 40 s (3,748 commits fetched,
99.9 MB of packs).
---
web/lib/routes/gallery.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/web/lib/routes/gallery.ts b/web/lib/routes/gallery.ts
index dd09937..4586125 100644
--- a/web/lib/routes/gallery.ts
+++ b/web/lib/routes/gallery.ts
@@ -2,7 +2,9 @@
import { type GalleryEntry, readGallery, readMeta } from '../city.js';
import { handle, json } from '../http.js';
-const FEATURED = ['torvalds', 'sindresorhus', 'vercel', 'react/react', 'denoland/deno', 'gaearon', 'karpathy', 'rust-lang/rust', 'withastro', 'antfu'];
+// every one must be a city the hosted builder can replay, or its slot stays empty: rust-lang/rust, under the size
+// cap, still outgrows the builder's memory at replay time
+const FEATURED = ['torvalds', 'sindresorhus', 'vercel', 'react/react', 'denoland/deno', 'gaearon', 'karpathy', 'golang/go', 'withastro', 'antfu'];
// a city built under a repo's old name (facebook/react, now react/react) is a second city of the same repo: the
// list shows the one under the name GitHub uses now
From d405a6e2569a63d58a103a4a138a48e81071ee16 Mon Sep 17 00:00:00 2001
From: azank
Date: Tue, 13 Oct 2026 17:51:04 +0500
Subject: [PATCH 2/2] builds: a city whose replays all failed says which and
why, not "no commits"
When every repo of a city failed to replay, finish found nothing to draw and answered
"no commits in the last year". For rust-lang/rust that is 34,368 commits. The
discovery-time check from fa997db only covers repos over the size cap; a repo under it
can still fail at replay time (memory, time, fetch).
finish now answers 422 repo_failed, naming the failed repos with their commit counts,
whenever a repo with commits has no record; "no commits" remains for a city that really
has none. The page fills in each failed repo's reason from its row in the build list,
says how many commits it has (and, for too big or too long, that one hosted build cannot
replay them), and offers Try again.
---
web/lib/city.ts | 6 ++++++
web/public/s/city.js | 18 +++++++++++++++++-
web/test/bundle.test.ts | 22 ++++++++++++++++++++++
3 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/web/lib/city.ts b/web/lib/city.ts
index 9d2259e..e5cca08 100644
--- a/web/lib/city.ts
+++ b/web/lib/city.ts
@@ -227,6 +227,12 @@ export async function finishBuild(ticketToken: string, planToken: string): Promi
maxRepos: 60,
});
} catch {
+ // nothing to draw. Repos whose replays failed have commits (rust-lang/rust: 34,368 in the year, too big at
+ // replay time though under the size cap), so "no commits" would be false: name them. The page has the reasons.
+ const failed = missing.filter(r => r.c > 0).sort((a, b) => b.c - a.c);
+ if (failed.length) {
+ throw new HttpError(422, `${failed[0].n} could not be replayed`, { status: 'repo_failed', skipped: failed.slice(0, 5).map(r => ({ name: r.n, commits: r.c })) });
+ }
throw new HttpError(422, p.kind === 'user' ? 'no public commits on default branches in the last year' : 'no commits in the last year', { status: 'empty' });
}
const v = Date.now();
diff --git a/web/public/s/city.js b/web/public/s/city.js
index b28df94..c55d040 100644
--- a/web/public/s/city.js
+++ b/web/public/s/city.js
@@ -210,7 +210,14 @@
let meta;
try {
meta = await call('/api/build/finish', { ticket: st.ticket, plan: st.plan });
- } catch (e) { clearInterval(clock); return fail(e); }
+ } catch (e) {
+ clearInterval(clock);
+ // nothing drawable because replays failed: each failed repo's own reason is the one its row showed
+ if (e.info && e.info.status === 'repo_failed') {
+ for (const s of e.info.skipped || []) { const j = jobs.find(x => x.name === s.name && x.s === 'bad'); if (j) s.reason = j.note; }
+ }
+ return fail(e);
+ }
clearInterval(clock);
if (history.replaceState) history.replaceState(null, '', pagePath() + location.hash);
await open(meta);
@@ -238,6 +245,15 @@
for now the hosted builder takes repos up to about 1.5 GB.
${foot}`);
}
+ if (s === 'repo_failed') {
+ tellParent('missing');
+ const bad = (info.skipped || [])[0] || {};
+ const owner = isRepo ? CC.target.split('/')[0] : '';
+ const heavy = /too big|too long/.test(bad.reason || ''); // not for a fetch that failed or a busy builder
+ return show(`${head('commit.city', CC.target)}${esc(bad.name || CC.target)} could not be replayed${bad.reason ? `: ${esc(bad.reason)}` : ''}.
+ ${bad.commits ? `It has ${fmt(bad.commits)} commits from the last year${heavy ? ', more than one hosted build can replay' : ''}.` : ''}
+ ${foot}`), wireRetry();
+ }
if (s === 'empty') {
const whole = isRepo && !CC.all;
return show(`${head('commit.city', CC.target)}${esc(e.message)}.
diff --git a/web/test/bundle.test.ts b/web/test/bundle.test.ts
index a78351d..f46c462 100644
--- a/web/test/bundle.test.ts
+++ b/web/test/bundle.test.ts
@@ -8,6 +8,7 @@ import { finishBuild, readGallery, REPLAY_GEN, repoPath, usableGen } from '../li
import { aggregate, budgetCaps, bundle, mergeEvents, mineOf, type Payload } from '../lib/engine/bundle';
import { replayRepo } from '../lib/engine/repo';
import type { RepoRecord } from '../lib/engine/replay';
+import { HttpError } from '../lib/http';
import { sign } from '../lib/secret';
import { getJSON, putJSON, setStore, type Store, StoreConflict } from '../lib/store';
import { localTransport, Repo, REPO_ROOT, sh, tmpdir } from './helpers';
@@ -151,6 +152,27 @@ test('finish step: records of another version are never drawn; meta and gallery
assert.equal((await getJSON<{ v: number }>('meta/old-me.json'))?.v, meta.v); // the name the build was asked for points here
});
+test('finish step: a city whose replays all failed names them, instead of claiming no commits', async () => {
+ setStore(new MemStore());
+ const start = T0 + 2 * DAY, head = 'c'.repeat(40);
+ const plan = (r: object[]) => sign({
+ k: 'rust-lang/rust', kind: 'repo', mode: 'all', s: start, n: T0 + 30 * DAY,
+ p: { login: 'rust-lang/rust', name: 'rust-lang/rust', avatar: '', url: '', createdAt: '' }, c: null, x: 0, o: [0, 0], r,
+ }, 600);
+ const ticket = sign({ k: 'rust-lang/rust', t: 'rust-lang/rust', s: start, w: false, r: [['rust-lang/rust', head, 0, 1_021_559]] }, 600);
+ // its replay failed, so there is no record: 34,368 commits are not "no commits"
+ await assert.rejects(finishBuild(ticket, plan([{ n: 'rust-lang/rust', f: false, c: 34_368, h: head }])), (e: unknown) => {
+ assert.ok(e instanceof HttpError);
+ assert.equal(e.status, 422);
+ assert.equal(e.extra.status, 'repo_failed');
+ assert.equal(e.message, 'rust-lang/rust could not be replayed');
+ assert.deepEqual(e.extra.skipped, [{ name: 'rust-lang/rust', commits: 34_368 }]);
+ return true;
+ });
+ // nothing failed and nothing to draw: that one is empty
+ await assert.rejects(finishBuild(ticket, plan([])), (e: unknown) => e instanceof HttpError && e.extra.status === 'empty' && e.message === 'no commits in the last year');
+});
+
test('a generation-1 record stays in use only where the merged-branch fix cannot change it', async () => {
setStore(new MemStore());
const r = makeRepo(path.join(tmpdir(), 'gen'), 1);