Public: anyone can read this; the crew can change it.
Releases' sentences live in one module, read by the web page and the terminal #5658
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5613-releases-sentences-live-in-one-module-re → main 1 commit fork
4 files
changed +60
−6
src/release/words.ts added +22
| @@ -0,0 +1,22 @@ | ||
| 1 | /** | |
| 2 | * The releases view's sentences, said once for the web page and the | |
| 3 | * terminal alike (#5613). The live line is written as a template each | |
| 4 | * surface tags its own way: the page with `html`, escaping and marking up | |
| 5 | * the sha; the terminal with `plain`, joining the parts as they are. | |
| 6 | */ | |
| 7 | ||
| 8 | /** A template tag: the page's `html`, or `plain` for the terminal. */ | |
| 9 | export type Tag<T, V> = (strings: TemplateStringsArray, ...values: V[]) => T; | |
| 10 | ||
| 11 | /** Joins a template's parts as they are: the terminal's tag. */ | |
| 12 | export function plain(strings: TemplateStringsArray, ...values: string[]): string { | |
| 13 | return String.raw({ raw: strings }, ...values); | |
| 14 | } | |
| 15 | ||
| 16 | /** "live: <sha> since <time>", in the surface's own tag. */ | |
| 17 | export function liveSince<T, V>(tag: Tag<T, V>, sha: V, since: V): T { | |
| 18 | return tag`live: ${sha} since ${since}`; | |
| 19 | } | |
| 20 | ||
| 21 | /** What the empty releases view says will fill it. */ | |
| 22 | export const NO_RELEASES_YET = "A pull landed on the forge's own repository is recorded here."; |
src/tui/screens/releases.ts +4 −3
| @@ -10,6 +10,7 @@ import { getRepoById } from "../../core/repos.ts"; | ||
| 10 | 10 | import { running } from "../../kernel/version.ts"; |
| 11 | 11 | import { BOOTED, canRollBackTo, mayRead, mayRollBack, rollBackTo } from "../../release/gate.ts"; |
| 12 | 12 | import { listReleases, type Release } from "../../release/record.ts"; |
| 13 | import { liveSince, NO_RELEASES_YET, plain } from "../../release/words.ts"; | |
| 13 | 14 | import type { Ctx, Screen } from "../app/app.ts"; |
| 14 | 15 | import { binding } from "../app/keys.ts"; |
| 15 | 16 | import type { Session } from "../session.ts"; |
| @@ -26,10 +27,10 @@ export interface ReleaseLine { | ||
| 26 | 27 | readonly rollable: boolean; |
| 27 | 28 | } |
| 28 | 29 | |
| 29 | /** "live: <sha> since <time>", or nothing when no release is known to be live. */ | |
| 30 | /** The live line (`liveSince`), or nothing when no release is known to be live. */ | |
| 30 | 31 | export function liveText(f: Session): string | undefined { |
| 31 | 32 | const live = running().commit; |
| 32 | return live === null || !mayRead(f.app, f.user) ? undefined : `live: ${live.slice(0, 7)} since ${ago(BOOTED)}`; | |
| 33 | return live === null || !mayRead(f.app, f.user) ? undefined : liveSince(plain, live.slice(0, 7), ago(BOOTED)); | |
| 33 | 34 | } |
| 34 | 35 | |
| 35 | 36 | /** The kept releases this person may see; none at all for anyone the page would 404. */ |
| @@ -87,7 +88,7 @@ export function releasesScreen(f: Session, ctx: Ctx): Screen { | ||
| 87 | 88 | if (lines.length === 0) { |
| 88 | 89 | emptyState(s, body!, c, { |
| 89 | 90 | title: loaded ? "No releases yet" : "Reading…", |
| 90 | body: "A pull landed on the forge's own repository is recorded here.", | |
| 91 | body: NO_RELEASES_YET, | |
| 91 | 92 | }); |
| 92 | 93 | return; |
| 93 | 94 | } |
src/web/pages/releases.ts +4 −3
| @@ -13,18 +13,19 @@ import { running } from "../../kernel/version.ts"; | ||
| 13 | 13 | import type { Ctx } from "../../http/context.ts"; |
| 14 | 14 | import { listReleases, type Release } from "../../release/record.ts"; |
| 15 | 15 | import { BOOTED, canRollBackTo, mayRead, mayRollBack, rollBackTo } from "../../release/gate.ts"; |
| 16 | import { liveSince, NO_RELEASES_YET } from "../../release/words.ts"; | |
| 16 | 17 | import { notFoundPage, signInFirst } from "../chrome.ts"; |
| 17 | 18 | import { readForm } from "../form.ts"; |
| 18 | 19 | import { html, when, type Html } from "../html.ts"; |
| 19 | 20 | import { flashTo, page } from "../layout.ts"; |
| 20 | 21 | import { badge, btn, card, empty, sha, time } from "../ui.ts"; |
| 21 | 22 | |
| 22 | /** "live: <sha> since <time>" for the home, or nothing when no release is known to be live. */ | |
| 23 | /** The live line (`liveSince`) for the home, or nothing when no release is known to be live. */ | |
| 23 | 24 | export function liveLine(ctx: Ctx): Html | undefined { |
| 24 | 25 | const live = running().commit; |
| 25 | 26 | if (live === null || !mayRead(ctx.app, ctx.auth.user)) return undefined; |
| 26 | 27 | return html`<p class="small muted" id="live-release"> |
| 27 | <a href="/releases">live: <span class="sha">${live.slice(0, 7)}</span> since ${time(BOOTED)}</a> | |
| 28 | <a href="/releases">${liveSince(html, html`<span class="sha">${live.slice(0, 7)}</span>`, time(BOOTED))}</a> | |
| 28 | 29 | </p>`; |
| 29 | 30 | } |
| 30 | 31 | |
| @@ -69,7 +70,7 @@ export async function releasesPage(ctx: Ctx): Promise<Response> { | ||
| 69 | 70 | } |
| 70 | 71 | const body = |
| 71 | 72 | rows.length === 0 |
| 72 | ? empty({ icon: "clock", title: "No releases yet", note: "A pull landed on the forge's own repository is recorded here." }) | |
| 73 | ? empty({ icon: "clock", title: "No releases yet", note: NO_RELEASES_YET }) | |
| 73 | 74 | : card(html`<table class="table"> |
| 74 | 75 | <thead><tr><th>Release</th><th>Pull</th><th>Landed</th><th>Healthy</th><th></th></tr></thead> |
| 75 | 76 | <tbody>${rows}</tbody> |
test/release-words.test.ts added +30
| @@ -0,0 +1,30 @@ | ||
| 1 | /** | |
| 2 | * The releases view's sentences are said from src/release/words.ts alone | |
| 3 | * (#5613): the web page and the terminal screen import them, and a copy | |
| 4 | * typed back into either fails here. | |
| 5 | */ | |
| 6 | ||
| 7 | import { expect, test } from "bun:test"; | |
| 8 | import { readFileSync } from "node:fs"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import { html } from "../src/web/html.ts"; | |
| 11 | import { liveSince, NO_RELEASES_YET, plain } from "../src/release/words.ts"; | |
| 12 | ||
| 13 | const FILES = ["src/web/pages/releases.ts", "src/tui/screens/releases.ts"]; | |
| 14 | // The live line's opening as a string, a template or markup would type it; | |
| 15 | // a bare "live: " is also the `live: boolean` field of a release line. | |
| 16 | const LITERALS = ['"live: ', "`live: ", ">live: ", "live: ${", "live: <", "since <time>", "A pull landed on the forge's own repository is recorded here.", "recorded here"]; | |
| 17 | ||
| 18 | test.each(FILES)("%s types none of the releases sentences", (file) => { | |
| 19 | const text = readFileSync(join(import.meta.dir, "..", file), "utf8"); | |
| 20 | for (const literal of LITERALS) expect(text).not.toContain(literal); | |
| 21 | expect(text).toContain('from "../../release/words.ts"'); | |
| 22 | }); | |
| 23 | ||
| 24 | test("the live line reads the same in both tags", () => { | |
| 25 | expect(liveSince(plain, "abc1234", "5m ago")).toBe("live: abc1234 since 5m ago"); | |
| 26 | expect(liveSince(html, html`<span class="sha">${"abc1234"}</span>`, "<now>").toString()).toBe( | |
| 27 | 'live: <span class="sha">abc1234</span> since <now>', | |
| 28 | ); | |
| 29 | expect(NO_RELEASES_YET).toBe("A pull landed on the forge's own repository is recorded here."); | |
| 30 | }); |
new above
offline