Public: anyone can read this; the crew can change it.
Every preview of the forge pictures the Iterations page of a seeded fork family, at desk and phone width #9186
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9182-every-preview-of-the-forge-pictures-the → main 5 commits fork 2 threads · 2 unresolved
4 files
changed +115
−9
src/spawn/preview-looks.ts +21 −4
| @@ -1,9 +1,10 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * The looks every preview of the forge takes as its owner (#8974): home |
| 3 | * with its pins, /flow, the seeded repository's head and its pull's page, | |
| 4 | * each at desk and phone width. The looks service adds them to whatever | |
| 5 | * forge.json lists for a preview of the forge's own repository | |
| 6 | * (`FORGE_REPO`), so no edit of forge.json can drop one. | |
| 3 | * with its pins, /flow, the seeded repository's head, its pull's page and | |
| 4 | * its Iterations page (#9182), each at desk and phone width. The looks | |
| 5 | * service adds them to whatever forge.json lists for a preview of the | |
| 6 | * forge's own repository (`FORGE_REPO`), so no edit of forge.json can | |
| 7 | * drop one. | |
| 7 | 8 | * |
| 8 | 9 | * Plain data and no imports beyond a type: the seed (`preview-seed.ts`) |
| 9 | 10 | * makes what these pages show, and the looks service reads the list. |
| @@ -25,6 +26,21 @@ export const PREVIEW_PULL = 3; | ||
| 25 | 26 | export const PREVIEW_REPO_HOME = `/${PREVIEW_ADMIN}/${PREVIEW_REPO}`; |
| 26 | 27 | export const PREVIEW_PULL_PAGE = `${PREVIEW_REPO_HOME}/pulls/${String(PREVIEW_PULL)}`; |
| 27 | 28 | |
| 29 | /** The Iterations page of the seeded fork family (#9182). */ | |
| 30 | export const PREVIEW_ITERATIONS_PAGE = `${PREVIEW_REPO_HOME}/iterations`; | |
| 31 | /** | |
| 32 | * The seeded forks, in the order they are made: each is `<owner>/sample`, a | |
| 33 | * fork of the sample or of the fork whose owner is named by `of` (one person | |
| 34 | * forks a repository once). The seed refuses to finish if the page would not | |
| 35 | * draw them. | |
| 36 | */ | |
| 37 | export const PREVIEW_FORKS: readonly { readonly owner: string; readonly of?: string }[] = [ | |
| 38 | { owner: "preview-ada" }, | |
| 39 | { owner: "preview-bo" }, | |
| 40 | { owner: "preview-cy" }, | |
| 41 | { owner: "preview-dee", of: "preview-ada" }, | |
| 42 | ]; | |
| 43 | ||
| 28 | 44 | export interface OwnerLook { |
| 29 | 45 | readonly page: string; |
| 30 | 46 | readonly kind: LookKind; |
| @@ -36,6 +52,7 @@ export const PREVIEW_OWNER_LOOKS: readonly OwnerLook[] = [ | ||
| 36 | 52 | "/flow", |
| 37 | 53 | PREVIEW_REPO_HOME, |
| 38 | 54 | PREVIEW_PULL_PAGE, |
| 55 | PREVIEW_ITERATIONS_PAGE, | |
| 39 | 56 | ].flatMap((page) => (["desktop", "phone"] as const).map((kind) => ({ page, kind }))); |
| 40 | 57 | |
| 41 | 58 | /** |
src/spawn/preview-seed.ts +62 −2
| @@ -21,12 +21,15 @@ import { pinPanel } from "../actions/board-pins.ts"; | ||
| 21 | 21 | import { writeBoard } from "../actions/boards.ts"; |
| 22 | 22 | import { backBet } from "../actions/bets.ts"; |
| 23 | 23 | import { openIssue, setIssueState } from "../actions/issues.ts"; |
| 24 | import { pinBuild } from "../actions/builds.ts"; | |
| 24 | 25 | import { openPull } from "../actions/pulls.ts"; |
| 25 | import { commitToBranch, createRepo } from "../actions/repos.ts"; | |
| 26 | import { commitToBranch, createRepo, forkRepo } from "../actions/repos.ts"; | |
| 27 | import { iterationsOf } from "../core/iterations.ts"; | |
| 26 | 28 | import { turnValveFor } from "../actions/valves.ts"; |
| 27 | 29 | import { insertBet } from "../core/bets.ts"; |
| 28 | 30 | import { addDependency } from "../core/dependencies.ts"; |
| 29 | 31 | import { claimRun, createRun, finishRun } from "../core/runs.ts"; |
| 32 | import { setMember } from "../core/roles.ts"; | |
| 30 | 33 | import { noteLimit } from "../core/standing.ts"; |
| 31 | 34 | import { addSubscription } from "../core/subscriptions.ts"; |
| 32 | 35 | import { recordUsage } from "../core/usage.ts"; |
| @@ -34,10 +37,19 @@ import { getUserSetting, setUserSetting } from "../core/settings.ts"; | ||
| 34 | 37 | import { createUser, getUserByName, type User } from "../core/users.ts"; |
| 35 | 38 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 36 | 39 | import { PREVIEW_CONFIG_FILES } from "./preview-config.ts"; |
| 37 | import { PREVIEW_ADMIN, PREVIEW_PULL, PREVIEW_PULL_PAGE, PREVIEW_REPO } from "./preview-looks.ts"; | |
| 40 | import { | |
| 41 | PREVIEW_ADMIN, | |
| 42 | PREVIEW_FORKS, | |
| 43 | PREVIEW_ITERATIONS_PAGE, | |
| 44 | PREVIEW_PULL, | |
| 45 | PREVIEW_PULL_PAGE, | |
| 46 | PREVIEW_REPO, | |
| 47 | } from "./preview-looks.ts"; | |
| 38 | 48 | |
| 39 | 49 | export { |
| 40 | 50 | PREVIEW_ADMIN, |
| 51 | PREVIEW_FORKS, | |
| 52 | PREVIEW_ITERATIONS_PAGE, | |
| 41 | 53 | PREVIEW_OWNER_LOOKS, |
| 42 | 54 | PREVIEW_PULL, |
| 43 | 55 | PREVIEW_PULL_PAGE, |
| @@ -199,6 +211,8 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> { | ||
| 199 | 211 | if (!waved.ok) return waved; |
| 200 | 212 | const bets = seedBets(app, admin, repo.value, open); |
| 201 | 213 | if (!bets.ok) return bets; |
| 214 | const family = await seedForkFamily(app, admin, repo.value, start.value.commitOid); | |
| 215 | if (!family.ok) return family; | |
| 202 | 216 | const valve = turnValveFor(app, admin, `${PREVIEW_ADMIN}/${PREVIEW_REPO}`, 60); |
| 203 | 217 | if (!valve.ok) return err(`the valve: ${valve.error}`); |
| 204 | 218 | |
| @@ -341,3 +355,49 @@ function seedBets(app: App, admin: User, repo: Repo, open: readonly Issue[]): Re | ||
| 341 | 355 | const result = backBet(app, admin, repo, backed.id, 20); |
| 342 | 356 | return result.ok ? ok() : err(`backing a bet: ${result.error.message}`); |
| 343 | 357 | } |
| 358 | ||
| 359 | /** | |
| 360 | * The fork family the Iterations page draws: the sample pinned at its first | |
| 361 | * commit, three forks of it and a fork of the first, each with a commit of | |
| 362 | * its own (so ahead/behind is not zero) and a pin on it. Refused when the | |
| 363 | * page would still be a 404 or would draw fewer cards than forks. | |
| 364 | */ | |
| 365 | async function seedForkFamily(app: App, admin: User, repo: Repo, first: string): Promise<Result<void, string>> { | |
| 366 | const pinned = await pinBuild(app, admin, repo, { label: "v1", oid: first, note: "The sample, as started" }); | |
| 367 | if (!pinned.ok) return err(`the sample's pin: ${JSON.stringify(pinned.error)}`); | |
| 368 | const made = new Map<string, Repo>(); | |
| 369 | for (const { owner, of } of PREVIEW_FORKS) { | |
| 370 | const parent = of === undefined ? repo : made.get(of); | |
| 371 | if (parent === undefined) return err(`the fork of ${owner} comes before ${String(of)}'s`); | |
| 372 | const person = await createUser(app.db, { | |
| 373 | origin: "seed", | |
| 374 | trusted: true, | |
| 375 | allowReserved: true, | |
| 376 | username: owner, | |
| 377 | email: `${owner}@preview.invalid`, | |
| 378 | }); | |
| 379 | if (!person.ok) return err(`the fork's owner ${owner}: ${person.error}`); | |
| 380 | setMember(app.db, person.value.id, true, admin.id, new Date()); | |
| 381 | const fork = await forkRepo(app, person.value, parent, PREVIEW_REPO, { visibility: "public" }); | |
| 382 | if (!fork.ok) return err(`the fork of ${owner}: ${String(fork.error)}`); | |
| 383 | made.set(owner, fork.value); | |
| 384 | const commit = await commitToBranch(app, person.value, fork.value, { | |
| 385 | message: `${owner} goes its own way`, | |
| 386 | files: [{ path: `${owner}.md`, content: `# ${owner}\n\nA change of its own.\n` }], | |
| 387 | }); | |
| 388 | if (!commit.ok) return err(`the commit of ${owner}: ${JSON.stringify(commit.error)}`); | |
| 389 | const pin = await pinBuild(app, person.value, fork.value, { label: "try-1", oid: commit.value.commitOid, note: `${owner}, first try` }); | |
| 390 | if (!pin.ok) return err(`the pin of ${owner}: ${JSON.stringify(pin.error)}`); | |
| 391 | } | |
| 392 | const home = getRepo(app.db, admin.username, PREVIEW_REPO); | |
| 393 | const drawn = home === undefined ? undefined : await iterationsOf({ app, auth: { user: admin } }, home); | |
| 394 | if (drawn === undefined) return err(`${PREVIEW_ITERATIONS_PAGE} would be a 404`); | |
| 395 | if (drawn.forks.length < PREVIEW_FORKS.length + 1) return err(`${PREVIEW_ITERATIONS_PAGE} draws ${String(drawn.forks.length)} forks`); | |
The seed refuses to finish if the page draws fewer than the sample plus the forks. It does not check that the family contains a fork of a fork. Check that the dee entry has forkOf set to the ada fork, so the tree cannot silently flatten. |
||
| 396 | // Each fork hangs under the one it was made from, so the tree cannot flatten. | |
| 397 | for (const { owner, of } of PREVIEW_FORKS) { | |
| 398 | const card = drawn.forks.find((f) => f.repo === `${owner}/${PREVIEW_REPO}`); | |
| 399 | const under = of === undefined ? `${admin.username}/${PREVIEW_REPO}` : `${of}/${PREVIEW_REPO}`; | |
| 400 | if (card?.forkOf !== under) return err(`${PREVIEW_ITERATIONS_PAGE} draws ${owner}/${PREVIEW_REPO} under ${String(card?.forkOf)}, not ${under}`); | |
| 401 | } | |
| 402 | return ok(); | |
| 403 | } |
test/looks-owner.test.ts +31 −2
| @@ -45,7 +45,8 @@ import { | ||
| 45 | 45 | seedPreview, |
| 46 | 46 | } from "../src/spawn/preview-seed.ts"; |
| 47 | 47 | import { PREVIEW_CONFIG_FILES } from "../src/spawn/preview-config.ts"; |
| 48 | import { ownerLooksFor } from "../src/spawn/preview-looks.ts"; | |
| 48 | import { ownerLooksFor, PREVIEW_FORKS, PREVIEW_ITERATIONS_PAGE } from "../src/spawn/preview-looks.ts"; | |
| 49 | import { iterationsOf } from "../src/core/iterations.ts"; | |
| 49 | 50 | import { localSource } from "../src/view/local.ts"; |
| 50 | 51 | import { isFormConfig, parseConfig } from "../src/web/render/config-format.ts"; |
| 51 | 52 | import { quietLogs } from "./helpers/globals.ts"; |
| @@ -484,7 +485,7 @@ describe("looks of a public app", () => { | ||
| 484 | 485 | if (!opened.ok) throw new Error(String(opened.error)); |
| 485 | 486 | const env = previewEnv(opened.value.number); |
| 486 | 487 | const commit = branch.value.commitOid; |
| 487 | const pages = ["/", "/flow", PREVIEW_REPO_HOME, PREVIEW_PULL_PAGE]; | |
| 488 | const pages = ["/", "/flow", PREVIEW_REPO_HOME, PREVIEW_PULL_PAGE, PREVIEW_ITERATIONS_PAGE]; | |
| 488 | 489 | const deadline = Date.now() + 90_000; |
| 489 | 490 | const light = () => |
| 490 | 491 | looksForEnv(app.db, record.id, env).filter((l) => l.commitOid === commit && l.theme === "light"); |
| @@ -524,3 +525,31 @@ describe("looks of a public app", () => { | ||
| 524 | 525 | expect(ownerLooksFor("alice/forge", forge, previewEnv(1)).length).toBeGreaterThan(0); |
| 525 | 526 | }); |
| 526 | 527 | }); |
| 528 | ||
| 529 | describe("the Iterations page of the seeded fork family (#9182)", () => { | |
| 530 | test("the owner's looks include it at desk and phone", () => { | |
| 531 | const forge = { ownerName: "alice", name: "forge" }; | |
| 532 | const looks = ownerLooksFor("alice/forge", forge, previewEnv(1)).filter((l) => l.page === PREVIEW_ITERATIONS_PAGE); | |
| 533 | expect(PREVIEW_ITERATIONS_PAGE).toBe("/preview-admin/sample/iterations"); | |
| 534 | expect(looks.map((l) => l.kind).sort()).toEqual(["desktop", "phone"]); | |
| 535 | }); | |
| 536 | ||
| 537 | test("the seeded repository has the forks the page draws, each with a build of its own", async () => { | |
| 538 | const dir = await mkdtemp(join(tmpdir(), "forge-iterations-seed-")); | |
| 539 | const app = createApp({ dataDir: dir, port: 0 }); | |
| 540 | try { | |
| 541 | const seeded = await seedPreview(app, { FORGE_APP_ENV: "preview" }); | |
| 542 | if (!seeded.ok) throw new Error(seeded.error); | |
| 543 | const admin = getUserByName(app.db, PREVIEW_ADMIN)!; | |
| 544 | const repo = getRepo(app.db, PREVIEW_ADMIN, PREVIEW_REPO)!; | |
| 545 | const drawn = await iterationsOf({ app, auth: { user: admin } }, repo); | |
| 546 | expect(drawn?.forks.length).toBeGreaterThanOrEqual(4); | |
| 547 | expect(drawn!.forks.filter((f) => f.forkOf !== null).length).toBe(PREVIEW_FORKS.length); | |
| 548 | expect(drawn!.forks.some((f) => f.forkOf === `preview-ada/${PREVIEW_REPO}`)).toBe(true); | |
| 549 | expect(drawn!.forks.filter((f) => f.forkOf !== null).every((f) => f.builds.length > 0)).toBe(true); | |
| 550 | } finally { | |
| 551 | app.close(); | |
| 552 | await rm(dir, { recursive: true, force: true }); | |
| 553 | } | |
| 554 | }, 30_000); | |
| 555 | }); |
test/signup-gate-parent.test.ts +1 −1
| @@ -115,7 +115,7 @@ test("every caller of createUser says where its account comes from", async () => | ||
| 115 | 115 | "src/actions/spawn.ts": ["spawn", "spawn"], |
| 116 | 116 | "src/actions/users.ts": ["signup", "passkey"], |
| 117 | 117 | "src/chaos/world.ts": ["seed"], |
| 118 | "src/spawn/preview-seed.ts": ["seed"], | |
| 118 | "src/spawn/preview-seed.ts": ["seed", "seed"], | |
| 119 | 119 | "src/web/pages/claim.ts": ["claim"], |
| 120 | 120 | "src/web/pages/everyday.ts": ["everyday"], |
| 121 | 121 | }); |
new above
offline
This line is much longer than the rest of the comment. Rewrap it.