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

Merged opened this

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

Merged

mason/forge:mason/5613-releases-sentences-live-in-one-module-re → main 1 commit fork

  • Merged. mason/5613-releases-sentences-live-in-one-module-re was merged into main as deba5c7.

Review

Sign in to review this change.
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";
1010 import { running } from "../../kernel/version.ts";
1111 import { BOOTED, canRollBackTo, mayRead, mayRollBack, rollBackTo } from "../../release/gate.ts";
1212 import { listReleases, type Release } from "../../release/record.ts";
13 import { liveSince, NO_RELEASES_YET, plain } from "../../release/words.ts";
1314 import type { Ctx, Screen } from "../app/app.ts";
1415 import { binding } from "../app/keys.ts";
1516 import type { Session } from "../session.ts";
@@ -26,10 +27,10 @@ export interface ReleaseLine {
2627 readonly rollable: boolean;
2728 }
2829
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. */
3031 export function liveText(f: Session): string | undefined {
3132 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));
3334 }
3435
3536 /** 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 {
8788 if (lines.length === 0) {
8889 emptyState(s, body!, c, {
8990 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,
9192 });
9293 return;
9394 }
src/web/pages/releases.ts +4 −3
@@ -13,18 +13,19 @@ import { running } from "../../kernel/version.ts";
1313 import type { Ctx } from "../../http/context.ts";
1414 import { listReleases, type Release } from "../../release/record.ts";
1515 import { BOOTED, canRollBackTo, mayRead, mayRollBack, rollBackTo } from "../../release/gate.ts";
16 import { liveSince, NO_RELEASES_YET } from "../../release/words.ts";
1617 import { notFoundPage, signInFirst } from "../chrome.ts";
1718 import { readForm } from "../form.ts";
1819 import { html, when, type Html } from "../html.ts";
1920 import { flashTo, page } from "../layout.ts";
2021 import { badge, btn, card, empty, sha, time } from "../ui.ts";
2122
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. */
2324 export function liveLine(ctx: Ctx): Html | undefined {
2425 const live = running().commit;
2526 if (live === null || !mayRead(ctx.app, ctx.auth.user)) return undefined;
2627 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>
2829 </p>`;
2930 }
3031
@@ -69,7 +70,7 @@ export async function releasesPage(ctx: Ctx): Promise<Response> {
6970 }
7071 const body =
7172 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 })
7374 : card(html`<table class="table">
7475 <thead><tr><th>Release</th><th>Pull</th><th>Landed</th><th>Healthy</th><th></th></tr></thead>
7576 <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 &lt;now&gt;',
28 );
29 expect(NO_RELEASES_YET).toBe("A pull landed on the forge's own repository is recorded here.");
30 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs