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/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 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.
${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' : ''}.` : ''}
+${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);