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

The /pipeline page speaks the home page's language #9838

Merged opened this

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

Merged

scout/forge:scout/9835-the-pipeline-page-speaks-the-home-page-s → main 2 commits fork

  • Merged. scout/9835-the-pipeline-page-speaks-the-home-page-s was merged into main as 1dab53b.

Review

Sign in to review this change.
2 files changed +68 −40
src/web/pages/pipeline.ts +33 −40
@@ -13,7 +13,7 @@ import { notePhase, takeSessionTime } from "../../core/route_timing.ts";
1313 import { html } from "../html.ts";
1414 import { page } from "../layout.ts";
1515 import { reportPath } from "../../checks/report.ts";
16 import { card, row, stat } from "../ui.ts";
16 import { stat } from "../ui.ts";
1717
1818 /** What a pull waits on at each step, what only that step catches, and the parts inside it. */
1919 export const STEP_DETAIL: Record<Step, { waits: string; catches: string; inside: readonly string[] }> = {
@@ -77,48 +77,41 @@ export function pipelinePage(ctx: Ctx): Response {
7777 one it is at and, in one sentence, why it waits there.
7878 </p>
7979 </div>
80 <ol class="list" id="path">
80 <div id="path">
8181 ${STEPS.map((s, i) => {
8282 const d = STEP_DETAIL[s];
83 return row({
84 id: s,
85 data: { step: s },
86 title: `${i + 1}. ${title(s)}`,
87 meta: html`${STEP_MEANS[s]}`,
88 fold: {
89 id: `inside-${s}`,
90 open: false,
91 body: html`<div class="card-body">
92 <p class="small">It waits for ${d.waits} Only this step catches ${d.catches}</p>
93 <ul class="small">${d.inside.map((line) => html`<li>${line}</li>`)}</ul>
94 <p class="small muted">Measured, last ${STEP_WINDOW_DAYS} days, public repositories: median time, then how often red.</p>
95 <div class="stats" data-measures="${s}">${STEP_MEASURES[s].map((k) => {
96 const m = stats.get(k);
97 if (m === undefined) return "";
98 const p = statParts(m);
99 return stat({ label: m.label, value: p.value, title: m.medianMs === null ? "no median" : `median ${p.value}`, note: p.note, noteTitle: p.note });
100 })}</div>
101 </div>`,
102 },
103 });
83 return html`<section class="mb" id="${s}" data-step="${s}">
84 <h2>${i + 1}. ${title(s)}</h2>
85 <p>${STEP_MEANS[s]}</p>
86 <details id="inside-${s}">
87 <summary class="small">What is inside</summary>
88 <p class="small">It waits for ${d.waits} Only this step catches ${d.catches}</p>
89 <ul class="small">${d.inside.map((line) => html`<li>${line}</li>`)}</ul>
90 <p class="small muted">Measured, last ${STEP_WINDOW_DAYS} days, public repositories: median time, then how often red.</p>
91 <div class="stats" data-measures="${s}">${STEP_MEASURES[s].map((k) => {
92 const m = stats.get(k);
93 if (m === undefined) return "";
94 const p = statParts(m);
95 return stat({ label: m.label, value: p.value, title: m.medianMs === null ? "no median" : `median ${p.value}`, note: p.note, noteTitle: p.note });
96 })}</div>
97 </details>
98 </section>`;
10499 })}
105 </ol>
106 ${card(
107 html`<div class="card-body">
108 <p class="small">
109 The whole test suite does not stand in the path: a pull's check runs the tests its change reaches,
110 and the rest of the path runs lint and types. An admin sees how many changes sit at each step right now on
111 <a href="/boards">Boards</a>.
112 </p>
113 <p class="small" id="full-suite">
114 The whole suite still runs, as a report: after a landing, on a schedule, against the default branch's tip.
115 Each test file it finds failing gets one issue labelled <span class="mono">full-suite</span>, listed
116 <a href="${reportPath()}">here</a>. The report holds no pull, batch or release.
117 </p>
118 <p class="muted small">For agents: the MCP tool <span class="mono">pull</span> answers <span class="mono">step: { step, why, done }</span> with these four words.</p>
119 </div>`,
120 { head: html`<h2>Where to look next</h2>` },
121 )}`;
100 </div>
101 <section class="mb">
102 <h2>Where to look next</h2>
103 <p class="small">
104 The whole test suite does not stand in the path: a pull's check runs the tests its change reaches,
105 and the rest of the path runs lint and types. An admin sees how many changes sit at each step right now on
106 <a href="/boards">Boards</a>.
107 </p>
108 <p class="small" id="full-suite">
109 The whole suite still runs, as a report: after a landing, on a schedule, against the default branch's tip.
110 Each test file it finds failing gets one issue labelled <span class="mono">full-suite</span>, listed
111 <a href="${reportPath()}">here</a>. The report holds no pull, batch or release.
112 </p>
113 <p class="muted small">For agents: the MCP tool <span class="mono">pull</span> answers <span class="mono">step: { step, why, done }</span> with these four words.</p>
114 </section>`;
122115 const response = page({ ctx, title: "How a change lands", body });
123116 notePhase(ctx.request, "render", performance.now() - rendering);
124117 return response;
test/pipeline-language.test.ts added +35
@@ -0,0 +1,35 @@
1 /** /pipeline speaks the home page's language (#9835): one heading, four plain sections, no card. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp } from "../src/app.ts";
7 import { serve } from "../src/http/server.ts";
8 import { STEPS } from "../src/flow/path.ts";
9
10 let dir = "";
11 let app: ReturnType<typeof createApp>;
12 let servers: Awaited<ReturnType<typeof serve>>;
13
14 beforeAll(async () => {
15 dir = await mkdtemp(join(tmpdir(), "forge-pipeline-lang-"));
16 app = createApp({ dataDir: dir, port: 0 });
17 servers = await serve(app);
18 });
19 afterAll(async () => {
20 servers.stop();
21 app.close();
22 await rm(dir, { recursive: true, force: true });
23 });
24
25 test("/pipeline signed out has one h1, a heading per step and no card", async () => {
26 const res = await fetch(servers.url + "/pipeline");
27 expect(res.status).toBe(200);
28 const page = await res.text();
29 const main = page.slice(page.indexOf("<main"), page.indexOf("</main>"));
30 expect(main.match(/<h1[ >]/g)?.length).toBe(1);
31 expect(main).not.toContain('class="card');
32 expect(main).not.toContain("card-body");
33 for (const s of STEPS) expect(main).toMatch(new RegExp(`<h2>\\d\\. ${s.charAt(0).toUpperCase()}${s.slice(1)}</h2>`));
34 expect(main).toContain('href="/boards"');
35 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs