Public: anyone can read this; the crew can change it.

A forge preview takes owner looks of an app's page, so a pull touching it shows main beside head #9784

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9782-a-forge-preview-takes-owner-looks-of-an → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/9782-a-forge-preview-takes-owner-looks-of-an was merged into main as a0b3c39.

Review

Sign in to review this change.
3 files changed +81 −1
src/spawn/preview-looks.ts +10 −1
@@ -1,7 +1,7 @@
11 /**
22 * The looks every preview of the forge takes as its owner (#8974): home
33 * with its pins, /flow, the seeded repository's head, its pull's page and
4 * its Iterations page (#9182), the Later gate's proposal (#9212), its
4 * its Iterations page (#9182), the seeded app's page (#9782), the Later gate's proposal (#9212), its
55 * milestones (#9326) and its waves (#9325), each at desk and phone width.
66 * The looks service adds them to whatever forge.json lists for a preview
77 * of the forge's own repository (`FORGE_REPO`), so no edit of forge.json can
@@ -27,6 +27,14 @@ export const PREVIEW_PULL = 3;
2727 export const PREVIEW_REPO_HOME = `/${PREVIEW_ADMIN}/${PREVIEW_REPO}`;
2828 export const PREVIEW_PULL_PAGE = `${PREVIEW_REPO_HOME}/pulls/${String(PREVIEW_PULL)}`;
2929
30 /**
31 * The seeded app: a second repository whose only app fact is its record, so
32 * its home is the app page (#9782). Nothing runs: the page says so. The seed
33 * refuses to finish unless that page answers 200.
34 */
35 export const PREVIEW_APP = "hello";
36 export const PREVIEW_APP_PAGE = `/${PREVIEW_ADMIN}/${PREVIEW_APP}`;
37
3038 /** The Iterations page of the seeded fork family (#9182). */
3139 export const PREVIEW_ITERATIONS_PAGE = `${PREVIEW_REPO_HOME}/iterations`;
3240 /**
@@ -104,6 +112,7 @@ export const PREVIEW_OWNER_LOOKS: readonly OwnerLook[] = [
104112 "/",
105113 "/flow",
106114 PREVIEW_REPO_HOME,
115 PREVIEW_APP_PAGE,
107116 PREVIEW_PULL_PAGE,
108117 PREVIEW_ITERATIONS_PAGE,
109118 PREVIEW_HELD_PAGE,
src/spawn/preview-seed.ts +37
@@ -15,6 +15,7 @@
1515 */
1616
1717 import type { App } from "../app.ts";
18 import type { Ctx } from "../http/context.ts";
1819 import type { Issue } from "../core/issues.ts";
1920 import { getRepo, type Repo } from "../core/repos.ts";
2021 import { pinPanel } from "../actions/board-pins.ts";
@@ -26,6 +27,8 @@ import { mergePull, openPull } from "../actions/pulls.ts";
2627 import { commitToBranch, createRepo, forkRepo } from "../actions/repos.ts";
2728 import { reviewGate } from "../actions/gate_review.ts";
2829 import { linkIssue } from "../actions/dependencies.ts";
30 import { createApp as createAppRecord } from "../core/apps.ts";
31 import { studioApp } from "../web/studio/app.ts";
2932 import { THEMES_AT_REST } from "../web/pages/gate.ts";
3033 import { iterationsOf } from "../core/iterations.ts";
3134 import { turnValveFor } from "../actions/valves.ts";
@@ -42,6 +45,8 @@ import { err, ok, type Result } from "../kernel/result.ts";
4245 import { PREVIEW_CONFIG_FILES } from "./preview-config.ts";
4346 import {
4447 PREVIEW_ADMIN,
48 PREVIEW_APP,
49 PREVIEW_APP_PAGE,
4550 PREVIEW_FORKS,
4651 PREVIEW_HELD_PAGE,
4752 PREVIEW_HELD_THEMES,
@@ -53,6 +58,8 @@ import {
5358
5459 export {
5560 PREVIEW_ADMIN,
61 PREVIEW_APP,
62 PREVIEW_APP_PAGE,
5663 PREVIEW_FORKS,
5764 PREVIEW_HELD_PAGE,
5865 PREVIEW_HELD_THEMES,
@@ -244,6 +251,8 @@ async function seedWorld(app: App, admin: User): Promise<Result<void, string>> {
244251 if (!family.ok) return family;
245252 const gated = seedLaterGate(app, admin, repo.value);
246253 if (!gated.ok) return gated;
254 const hosted = await seedApp(app, admin);
255 if (!hosted.ok) return hosted;
247256 const valve = turnValveFor(app, admin, `${PREVIEW_ADMIN}/${PREVIEW_REPO}`, 60);
248257 if (!valve.ok) return err(`the valve: ${valve.error}`);
249258
@@ -484,3 +493,31 @@ function seedLaterGate(app: App, admin: User, repo: Repo): Result<void, string>
484493 return err(`${PREVIEW_HELD_PAGE} shows ${String(themes)} themes, not more than the ${String(THEMES_AT_REST)} drawn at rest`);
485494 return ok();
486495 }
496
497 /**
498 * One app, for the app page's look (#9782): a repository and its record,
499 * nothing more. No process starts and no network is reached; the page says
500 * the app is not running. Refused when the page would answer anything but 200.
501 */
502 async function seedApp(app: App, admin: User): Promise<Result<void, string>> {
503 const repo = await createRepo(app, admin, {
504 name: PREVIEW_APP,
505 visibility: "public",
506 description: "A preview's sample app",
507 });
508 if (!repo.ok) return err(`the sample app's repository: ${String(repo.error)}`);
509 const first = await commitToBranch(app, admin, repo.value, {
510 message: "Start",
511 files: [{ path: "README.md", content: "# hello\n\nA preview's sample app.\n" }],
512 });
513 if (!first.ok) return err(`the sample app's commit: ${JSON.stringify(first.error)}`);
514 const record = createAppRecord(app.db, { repoId: repo.value.id, slug: PREVIEW_APP, access: "public" });
515 if (!record.ok) return err(`the sample app's record: ${record.error}`);
516 const url = new URL(`https://preview.invalid${PREVIEW_APP_PAGE}`);
517 const answer = await studioApp(
518 { app, request: new Request(url.href), url, params: {}, auth: { user: admin }, theme: undefined } as Ctx,
519 repo.value,
520 record.value,
521 );
522 return answer.status === 200 ? ok() : err(`${PREVIEW_APP_PAGE} would answer ${String(answer.status)}, not 200`);
523 }

Hand-built Ctx cast with as Ctx hides missing fields; build it through the same helper the router uses, and add a test that forces a non-200 to prove the seed refuses.

test/preview-app-look.test.ts added +34
@@ -0,0 +1,34 @@
1 /** The preview seeds one app and takes its page at desk and phone as the owner (#9782). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { getAppByRepo } from "../src/core/apps.ts";
8 import { getRepo } from "../src/core/repos.ts";
9 import { PREVIEW_ADMIN, PREVIEW_APP, PREVIEW_APP_PAGE, PREVIEW_OWNER_LOOKS, seedPreview } from "../src/spawn/preview-seed.ts";
10
11 let dir: string;
12 let app: App;
13 beforeAll(async () => {
14 dir = await mkdtemp(join(tmpdir(), "forge-preview-app-"));
15 app = createApp({ dataDir: dir, port: 0 });
16 const seeded = await seedPreview(app, { FORGE_APP_ENV: "preview" });
17 if (!seeded.ok) throw new Error(seeded.error);
18 }, 60_000);
19 afterAll(async () => {
20 app.close();
21 await rm(dir, { recursive: true, force: true });
22 });
23
24 test("the named list holds the app's page at desktop and phone", () => {
25 expect(PREVIEW_APP_PAGE).toBe(`/${PREVIEW_ADMIN}/${PREVIEW_APP}`);
26 expect(PREVIEW_OWNER_LOOKS).toContainEqual({ page: PREVIEW_APP_PAGE, kind: "desktop" });
27 expect(PREVIEW_OWNER_LOOKS).toContainEqual({ page: PREVIEW_APP_PAGE, kind: "phone" });
28 });
29
30 test("the seed made one app, as a record only", () => {
31 const repo = getRepo(app.db, PREVIEW_ADMIN, PREVIEW_APP)!;
32 expect(getAppByRepo(app.db, repo.id)?.slug).toBe(PREVIEW_APP);
33 expect(app.db.query<{ n: number }, []>("SELECT count(*) AS n FROM app").get()!.n).toBe(1);
34 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs