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

The terminal's Deploys screen lists the app's open pulls with their check word and preview address #6158

Merged opened this

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

Merged

scout/forge:scout/6074-the-terminal-s-deploys-screen-lists-the → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6074-the-terminal-s-deploys-screen-lists-the was merged into main as b25967c.

Review

Sign in to review this change.
3 files changed +90 −3
src/actions/apps.ts +27
@@ -12,6 +12,8 @@ import { mayView } from "../core/sealed.ts";
1212 import { requireVerified } from "./verify.ts";
1313 import { requireAppRoom } from "./app_limit.ts";
1414 import type { App } from "../app.ts";
15 import { checkWord, type CheckWord } from "../apps/check_word.ts";
16 import { openPulls } from "../apps/door_pulls.ts";
1517 import { goldenPath, goldenPathLine } from "../apps/golden_path.ts";
1618 import { appVitals, type AppVitals } from "../apps/vitals.ts";
1719 import { appsRuntime, envsFor, type EnvStatus } from "../apps/runtime.ts";
@@ -511,6 +513,31 @@ export function appPace(app: App, actor: User | undefined, repo: Repo): string {
511513 return record === undefined ? "" : goldenPathLine(goldenPath(app.db, record));
512514 }
513515
516 export interface AppPullLine {
517 readonly number: number;
518 readonly title: string;
519 readonly check: CheckWord;
520 /** The pull's preview address, or `undefined` when the forge serves none. */
521 readonly preview: string | undefined;
522 }
523
524 /**
525 * The app's open pulls with their check word and preview address, for a
526 * signed-in viewer who may read the repository; a guest gets none.
527 */
528 export function appPullLines(app: App, actor: User | undefined, repo: Repo): AppPullLine[] {
529 if (actor === undefined) return [];
530 const record = getAppByRepo(app.db, repo.id);
531 if (record === undefined) return [];
532 const runtime = appsRuntime(app);
533 return openPulls(app.db, repo, actor).map((p) => ({
534 number: p.number,
535 title: p.title,
536 check: checkWord(p.run, p.headOid),
537 preview: runtime?.urlFor(record, previewEnv(p.number)),
538 }));
539 }
540
514541 export type EnvError = "not_found" | "no_runtime" | "no_such_env";
515542
516543 /** The URL to open an environment at — via a grant for a private app. */
src/tui/screens/deploys.ts +31 −3
@@ -5,9 +5,13 @@
55 * a pull or an issue shows holds one. This is the list that does, read
66 * through `listRepoRuns` (so it is gated like every other run read) and
77 * joined to `deploy_pull` in one grouped read for `carries #3 #5`.
8 *
9 * Below the deploys, a signed-in viewer sees the app's open pulls, one line
10 * each: `#N title · check <word> · <preview address or none>`. A guest sees
11 * none.
812 */
913
10 import { appPace, appVisitors } from "../../actions/apps.ts";
14 import { appPace, appPullLines, appVisitors, type AppPullLine } from "../../actions/apps.ts";
1115 import { trendLine, visitorsLine } from "../../apps/vitals.ts";
1216 import { listRepoRuns } from "../../actions/runs.ts";
1317 import { carriedByRuns, carriesText } from "../../core/carried.ts";
@@ -35,6 +39,12 @@ function readDeploys(f: Session, repo: Repo): Run[] {
3539 return listed.ok ? listed.value : [];
3640 }
3741
42 /** `#N title · check <word> · <preview address or none>` */
43 function pullLine(p: AppPullLine): string {
44 const title = p.title.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, "").trim();
45 return `#${p.number} ${title} · check ${p.check} · ${p.preview ?? "none"}`;
46 }
47
3848 export function deploysScreen(f: Session, ctx: Ctx, repo: Repo): Screen {
3949 const list = new RunList(f);
4050 let carries = new Map<string, number[]>();
@@ -42,6 +52,7 @@ export function deploysScreen(f: Session, ctx: Ctx, repo: Repo): Screen {
4252 let visitors = "";
4353 let trend = "";
4454 let pace = "";
55 let pulls: AppPullLine[] = [];
4556
4657 const read = (): void => {
4758 const runs = readDeploys(f, repo);
@@ -54,6 +65,7 @@ export function deploysScreen(f: Session, ctx: Ctx, repo: Repo): Screen {
5465 visitors = seen === undefined ? "" : visitorsLine(seen);
5566 trend = seen === undefined ? "" : trendLine(seen.loadDays);
5667 pace = appPace(f.app, f.user, repo);
68 pulls = appPullLines(f.app, f.user, repo);
5769 loaded = true;
5870 ctx.invalidate();
5971 };
@@ -65,12 +77,28 @@ export function deploysScreen(f: Session, ctx: Ctx, repo: Repo): Screen {
6577 keys: [
6678 binding(["j", "k"], "move"),
6779 binding(["enter"], "open the log"),
68 binding(["r"], "refresh"),
80 binding(["r"], "refresh (the open pulls below the deploys: check, preview)"),
6981 ],
7082 load: async () => read(),
7183 render(area, s) {
7284 const c = ctx.theme.c;
73 const [head, body] = split(area, "v", [len(1 + (visitors === "" ? 0 : 1) + (trend === "" ? 0 : 1) + (pace === "" ? 0 : 1)), fill(1)]);
85 const [head, upper, tail] = split(area, "v", [
86 len(1 + (visitors === "" ? 0 : 1) + (trend === "" ? 0 : 1) + (pace === "" ? 0 : 1)),
87 fill(1),
88 len(pulls.length === 0 ? 0 : pulls.length + 1),
89 ]);
90 const body = upper;
91 pulls.forEach((p, i) =>
92 s.text(
93 tail!.x,
94 tail!.y + 1 + i,
95 truncate(pullLine(p), tail!.w),
96 style(c.mutedFg, c.bg),
97 tail!.x + tail!.w,
98 ),
99 );
100 if (pulls.length > 0)
101 s.text(tail!.x, tail!.y, truncate("Open pulls", tail!.w), style(c.mutedFg, c.bg), tail!.x + tail!.w);

The const body = upper alias exists only to keep the later code unchanged. Destructure [head, body, tail] directly. When there are no pulls the tail is len(0), so nothing changes.

74102 s.text(
75103 head!.x,
76104 head!.y,
test/tui-deploys.test.ts +32
@@ -141,3 +141,35 @@ test("an admin sees the app's visitors and speed above the deploys, and a reader
141141 if (!c.ok) throw new Error("signup failed");
142142 expect(await screenText(c.value, r.value)).not.toContain("visitors");
143143 });
144
145 test("the app's open pulls list below the deploys with check word and preview, and a guest sees none", async () => {
146 const r = await createRepo(forge, alice, { name: "kiosk", visibility: "public" });
147 if (!r.ok) throw new Error("repo failed");
148 const made = createAppRecord(forge.db, { repoId: r.value.id, slug: "kiosk", access: "public" });
149 if (!made.ok) throw new Error(made.error);
150 const id = crypto.randomUUID();
151 forge.db
152 .query("INSERT INTO issue (id, repo_id, number, kind, title, author_id) VALUES (?, ?, 4, 'pull', 'Add a lamp', ?)")
153 .run(id, r.value.id, alice.id);
154 forge.db
155 .query("INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid) VALUES (?, 'main', 'lamp', 'b', 'h4')")
156 .run(id);
157 forge.db
158 .query("INSERT INTO run (id, repo_id, kind, issue_id, commit_oid, state) VALUES (?, ?, 'check', ?, 'h4', 'ok')")
159 .run(crypto.randomUUID(), r.value.id, id);
160 expect(await screenText(alice, r.value)).toContain("#4 Add a lamp · check green · none");
161
162 const { app, session } = createForgeApp(
163 forge,
164 { sshUsername: "guest", remoteAddress: "127.0.0.1" },
165 { terminal: headlessTerminal(120, 30), exitHooks: false },
166 );
167 app.start();
168 await settleApp(app);
169 app.push(deploysScreen(session, app.ctx, r.value));
170 await settleApp(app);
171 const text = app.screen.toText();
172 app.stop();
173 expect(text).not.toContain("Add a lamp");
174 expect(text).not.toContain("Open pulls");
175 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs