Public: anyone can read this; the crew can change it.
A preview's /__forge/ door links the live app, so one tap compares the pull with production #7929
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/7925-a-preview-s-forge-door-links-the-live-ap → main 4 commits fork
4 files
changed +64
−5
docs/APPS.md +5 −1
| @@ -242,7 +242,11 @@ forge, never the app, with no script, saying the app and where you are | ||
| 242 | 242 | (`production` or `preview of pull #N`) and linking Home, the repository and *Make it yours* |
| 243 | 243 | (`<repo>/make-it-yours`; both only to who may read the repository, who also |
| 244 | 244 | get the "more pulls" line linked to `<repo>/pulls`) and *Tell the crew* (the same link `/__forge/crew` gives; signed |
| 245 | out it offers sign in). It also says what is serving, read from the deploy | |
| 245 | out it offers sign in). On a preview it also links *The live app* (the production | |
| 246 | address, so one tap compares the pull with production), only to who may read the | |
| 247 | repository, as *What changed* is; production's door has no such link. In the | |
| 248 | open-pulls list the preview's own pull is plain text marked "(this preview)" | |
| 249 | instead of a link back to itself. It also says what is serving, read from the deploy | |
| 246 | 250 | record (no git): production shows the newest release and the three before it |
| 247 | 251 | (a redeploy of the same commit is not another release), a preview shows its |
| 248 | 252 | pull and the commit it last served, or "not deployed yet". A viewer who cannot |
src/apps/door.ts +10 −4
| @@ -27,6 +27,8 @@ export type DoorFacts = { | ||
| 27 | 27 | pulls?: DoorPullLine[] | undefined; |
| 28 | 28 | /** Who of the crew is working here now, as the pill's crew line says it. */ |
| 29 | 29 | crew?: CrewNow | undefined; |
| 30 | /** The production host of this app, only on a preview. */ | |
| 31 | live?: string | undefined; | |
| 30 | 32 | /** This host's own report door, only for a public app anyone may report on. */ |
| 31 | 33 | report?: string | undefined; |
| 32 | 34 | }; |
| @@ -34,7 +36,7 @@ export type DoorFacts = { | ||
| 34 | 36 | /** How many pulls the door lists; one more read than this means there are others. */ |
| 35 | 37 | export const DOOR_PULL_LIMIT = 5; |
| 36 | 38 | |
| 37 | export type DoorPullLine = { number: number; title: string; check: string; href: string }; | |
| 39 | export type DoorPullLine = { number: number; title: string; check: string; href: string; self?: boolean }; | |
| 38 | 40 | |
| 39 | 41 | const STYLE = |
| 40 | 42 | "html{color-scheme:light dark}body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}li{margin:.4rem 0}pre{overflow-x:auto;padding:.6rem;background:#eee;color:#111;user-select:all}" + |
| @@ -100,9 +102,10 @@ function servingHtml(facts: DoorFacts): string { | ||
| 100 | 102 | |
| 101 | 103 | function pullsHtml(pulls: DoorPullLine[] | undefined, morePulls: string | undefined): string { |
| 102 | 104 | if (pulls === undefined || pulls.length === 0) return ""; |
| 103 | const items = pulls.slice(0, DOOR_PULL_LIMIT).map((p) => | |
| 104 | link(p.href, `#${p.number} ${[...p.title].slice(0, 120).join("")} · check ${p.check}`), | |
| 105 | ); | |
| 105 | const items = pulls.slice(0, DOOR_PULL_LIMIT).map((p) => { | |
| 106 | const text = `#${p.number} ${[...p.title].slice(0, 120).join("")} · check ${p.check}`; | |
| 107 | return p.self === true ? `<li>${escapeAttr(text)} (this preview)</li>` : link(p.href, text); | |
| 108 | }); | |
| 106 | 109 | const words = "More pulls are open on the forge."; |
| 107 | 110 | const more = |
| 108 | 111 | pulls.length <= DOOR_PULL_LIMIT |
| @@ -130,6 +133,9 @@ export function doorPage(facts: DoorFacts, head: boolean): Response { | ||
| 130 | 133 | links.push(link(new URL(`/${where.repo}/pulls/${where.pull}/files`, forgeUrl).href, "What changed")); |
| 131 | 134 | links.push(link(`${repoUrl}/make-it-yours`, "Make it yours")); |
| 132 | 135 | morePulls = `${repoUrl}/pulls`; |
| 136 | // Production's address is for a reader of the app, as What changed is. | |
| 137 | if (facts.live !== undefined && where.env !== "production") | |
| 138 | links.push(link(facts.live, "The live app")); | |
| 133 | 139 | } |
| 134 | 140 | if (facts.tell !== undefined) links.push(link(facts.tell, "Tell the crew")); |
| 135 | 141 | else if (facts.signin !== undefined) |
src/apps/proxy.ts +4
| @@ -474,6 +474,7 @@ function doorPulls( | ||
| 474 | 474 | number: p.number, |
| 475 | 475 | title: p.title, |
| 476 | 476 | check: checkWord(p.run, p.headOid), |
| 477 | ...(env === target.env ? { self: true } : {}), | |
| 477 | 478 | href: live |
| 478 | 479 | ? runtime.urlFor(target.app, env) |
| 479 | 480 | : new URL( |
| @@ -944,6 +945,9 @@ export async function proxyApp( | ||
| 944 | 945 | { |
| 945 | 946 | where, |
| 946 | 947 | pulls, |
| 948 | ...(target.env === "production" | |
| 949 | ? {} | |
| 950 | : { live: runtime.urlFor(target.app, "production") }), | |
| 947 | 951 | ...(crew === undefined ? {} : { crew }), |
| 948 | 952 | ...(serving === undefined ? {} : { serving }), |
| 949 | 953 | forgeUrl: runtime.options.forgeUrl, |
test/apps-door.test.ts +45
| @@ -443,6 +443,51 @@ test("a preview links What changed for a reader only, never on production or for | ||
| 443 | 443 | expect(await text({ app: "notes", env: "production", repo: "alice/notes" })).not.toContain("What changed"); |
| 444 | 444 | }); |
| 445 | 445 | |
| 446 | test("a preview's door links the live app and marks its own pull; production's has neither", async () => { | |
| 447 | const live = runtime.urlFor(record, "production"); | |
| 448 | const preview = doorPage( | |
| 449 | { | |
| 450 | where: { app: "notes", env: "pull-7", pull: 7, repo: "alice/notes" }, | |
| 451 | forgeUrl: "http://forge.test", | |
| 452 | live, | |
| 453 | pulls: [ | |
| 454 | { number: 7, title: "mine", check: "none", href: "http://x/7", self: true }, | |
| 455 | { number: 8, title: "other", check: "none", href: "http://x/8" }, | |
| 456 | ], | |
| 457 | }, | |
| 458 | false, | |
| 459 | ); | |
| 460 | const html = await preview.text(); | |
| 461 | expect(html).toContain(`<li><a href="${live}">The live app</a></li>`); | |
| 462 | expect(html).toContain("#7 mine · check none (this preview)"); | |
| 463 | expect(html).not.toContain('href="http://x/7"'); | |
| 464 | expect(html).toContain('href="http://x/8"'); | |
| 465 | const prod = await doorPage( | |
| 466 | { where: { app: "notes", env: "production" }, forgeUrl: "http://forge.test", pulls: [] }, | |
| 467 | false, | |
| 468 | ).text(); | |
| 469 | expect(prod).not.toContain("The live app"); | |
| 470 | expect(prod).not.toContain("this preview"); | |
| 471 | // Without the repository to read, no production address either. | |
| 472 | const bare = await doorPage( | |
| 473 | { where: { app: "notes", env: "pull-7", pull: 7 }, forgeUrl: "http://forge.test", live }, | |
| 474 | false, | |
| 475 | ).text(); | |
| 476 | expect(bare).not.toContain("The live app"); | |
| 477 | // The newest pull is among the five the door lists, so the proxy marks it. | |
| 478 | const fresh = createIssue(app.db, { repoId: record.repoId, authorId: alice, title: "newest", kind: "pull" }); | |
| 479 | if (!fresh.ok) throw new Error(fresh.error); | |
| 480 | app.db | |
| 481 | .query( | |
| 482 | "INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid) VALUES (?, 'main', 'h', 'b', 'x')", | |
| 483 | ) | |
| 484 | .run(fresh.value.id); | |
| 485 | const real = await (await door(`notes--${fresh.value.number}.${DOMAIN}`)).text(); | |
| 486 | expect(real).toContain(">The live app</a>"); | |
| 487 | expect(real).toContain(`#${fresh.value.number} newest · check none (this preview)</li>`); | |
| 488 | expect(await (await door(`notes.${DOMAIN}`)).text()).not.toContain("The live app"); | |
| 489 | }); | |
| 490 | ||
| 446 | 491 | test("the door names the crew run working here, linked, to who may see it", async () => { |
| 447 | 492 | const bob = await createUser(app.db, { |
| 448 | 493 | origin: "seed", |
new above
offline