Public: anyone can read this; the crew can change it.
An app's /__forge/ door offers "Make it yours" and links its "more pulls" line to the repository's open pulls #6666
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6652-an-app-s-forge-door-offers-make-it-yours → main 5 commits fork 1 thread · 1 unresolved
3 files
changed +51
−9
docs/APPS.md +3 −2
| @@ -210,8 +210,9 @@ nothing when the crew is idle. | ||
| 210 | 210 | **When the pill is not shown** (an app whose own policy refuses foreign |
| 211 | 211 | scripts), `/__forge/` on the app's host is the way back: a small page from the |
| 212 | 212 | forge, never the app, with no script, saying the app and where you are |
| 213 | (`production` or `preview of pull #N`) and linking Home, the repository (to | |
| 214 | who may read it) and *Tell the crew* (the same link `/__forge/crew` gives; signed | |
| 213 | (`production` or `preview of pull #N`) and linking Home, the repository and *Make it yours* | |
| 214 | (`<repo>/make-it-yours`; both only to who may read the repository, who also | |
| 215 | get the "more pulls" line linked to `<repo>/pulls`) and *Tell the crew* (the same link `/__forge/crew` gives; signed | |
| 215 | 216 | out it offers sign in). It also says what is serving, read from the deploy |
| 216 | 217 | record (no git): production shows the newest release and the three before it |
| 217 | 218 | (a redeploy of the same commit is not another release), a preview shows its |
src/apps/door.ts +16 −7
| @@ -92,22 +92,31 @@ function servingHtml(facts: DoorFacts): string { | ||
| 92 | 92 | return `<p>Serving ${sha(now)} since ${stamp(now.at)}</p>${older === "" ? "" : `<h2>Before it</h2><ul>${older}</ul>`}`; |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | function pullsHtml(pulls: DoorPullLine[] | undefined): string { | |
| 95 | function pullsHtml(pulls: DoorPullLine[] | undefined, morePulls: string | undefined): string { | |
| 96 | 96 | if (pulls === undefined || pulls.length === 0) return ""; |
| 97 | 97 | const items = pulls.slice(0, DOOR_PULL_LIMIT).map((p) => |
| 98 | 98 | link(p.href, `#${p.number} ${[...p.title].slice(0, 120).join("")} · check ${p.check}`), |
| 99 | 99 | ); |
| 100 | const more = pulls.length > DOOR_PULL_LIMIT ? "<p>More pulls are open on the forge.</p>" : ""; | |
| 100 | const words = "More pulls are open on the forge."; | |
| 101 | const more = | |
| 102 | pulls.length <= DOOR_PULL_LIMIT | |
| 103 | ? "" | |
| 104 | : morePulls === undefined | |
| 105 | ? `<p>${words}</p>` | |
| 106 | : `<p><a href="${escapeAttr(morePulls)}">${words}</a></p>`; | |
| 101 | 107 | return `<h2>Open pulls</h2><ul>${items.join("")}</ul>${more}`; |
| 102 | 108 | } |
| 103 | 109 | |
| 104 | 110 | export function doorPage(facts: DoorFacts, head: boolean): Response { |
| 105 | 111 | const { where, forgeUrl } = facts; |
| 106 | 112 | const links = [link(forgeUrl, "Home")]; |
| 107 | if (where.repo !== undefined) | |
| 108 | links.push( | |
| 109 | link(new URL(`/${where.repo}`, forgeUrl).href, `The repository, ${where.repo}`), | |
| 110 | ); | |
| 113 | let morePulls: string | undefined; | |
| 114 | if (where.repo !== undefined) { | |
| 115 | const repoUrl = new URL(`/${where.repo}`, forgeUrl).href; | |
| 116 | links.push(link(repoUrl, `The repository, ${where.repo}`)); | |
| 117 | links.push(link(`${repoUrl}/make-it-yours`, "Make it yours")); | |
| 118 | morePulls = `${repoUrl}/pulls`; | |
| 119 | } | |
| 111 | 120 | if (facts.tell !== undefined) links.push(link(facts.tell, "Tell the crew")); |
| 112 | 121 | else if (facts.signin !== undefined) |
| 113 | 122 | links.push(link(facts.signin, "Sign in to tell the crew")); |
| @@ -119,7 +128,7 @@ export function doorPage(facts: DoorFacts, head: boolean): Response { | ||
| 119 | 128 | : `<p>Terminal:</p><pre><code>${escapeAttr(facts.ssh)}</code></pre>`; |
| 120 | 129 | const html = `<!doctype html> |
| 121 | 130 | <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>${escapeAttr(where.app)}, ${escapeAttr(doorWhere(where))}</title><style>${STYLE}</style></head> |
| 122 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${whereLine(where, forgeUrl)}</p>${servingHtml(facts)}${pullsHtml(facts.pulls)}<ul>${links.join("")}</ul>${terminal}</main></body></html> | |
| 131 | <body><main><h1>${escapeAttr(where.app)}</h1><p>${whereLine(where, forgeUrl)}</p>${servingHtml(facts)}${pullsHtml(facts.pulls, morePulls)}<ul>${links.join("")}</ul>${terminal}</main></body></html> | |
| 123 | 132 | `; |
| 124 | 133 | return new Response(head ? null : html, { |
| 125 | 134 | headers: { |
test/door-home-links.test.ts added +32
| @@ -0,0 +1,32 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { doorPage, DOOR_PULL_LIMIT, type DoorPullLine } from "../src/apps/door.ts"; | |
| 3 | ||
| 4 | const forgeUrl = "https://forge.test"; | |
| 5 | const pulls: DoorPullLine[] = Array.from({ length: DOOR_PULL_LIMIT + 1 }, (_, i) => ({ | |
| 6 | number: i + 1, | |
| 7 | title: "t", | |
| 8 | check: "pass", | |
| 9 | href: `https://forge.test/o/n/pulls/${i + 1}`, | |
| 10 | })); | |
| 11 | const render = (repo: string | undefined) => | |
| 12 | doorPage({ where: { app: "a", env: "production", ...(repo === undefined ? {} : { repo }) }, forgeUrl, pulls }, false).text(); | |
| 13 | ||
| 14 | test("a reader sees Make it yours and a linked more-pulls line", async () => { | |
| 15 | const html = await render("o/n"); | |
| 16 | expect(html).toContain('<a href="https://forge.test/o/n/make-it-yours">Make it yours</a>'); | |
| 17 | expect(html.indexOf("The repository")).toBeLessThan(html.indexOf("Make it yours")); | |
| 18 | expect(html).toContain('<a href="https://forge.test/o/n/pulls">More pulls are open on the forge.</a>'); | |
| 19 | }); | |
| 20 | ||
| 21 | test("repo names are escaped in the links", async () => { | |
| 22 | const html = await render('o/"><b>'); | |
| 23 | expect(html).not.toContain("<b>"); | |
| 24 | expect(html).toContain("make-it-yours"); | |
| 25 | }); | |
| 26 | ||
| 27 | test("a guest sees neither link, and the more line stays plain", async () => { | |
| 28 | const html = await render(undefined); | |
| 29 | expect(html).not.toContain("make-it-yours"); | |
| 30 | expect(html).not.toMatch(/<a [^>]*>More pulls/); | |
| 31 | expect(html).toContain("<p>More pulls are open on the forge.</p>"); | |
| 32 | }); |
new above
offline
The nested ternary is hard to read. An early return for
<= DOOR_PULL_LIMIT, then a one-line choice between the link and plain text, would be clearer. Not blocking.