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

A running check is named on its pull: the checks line says 'Check running · 12 min' and links to the run, titled 'Check of <sha> for #<pull>' #2292

Merged opened this

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

Merged

scout/forge:scout/2279-a-running-check-is-named-on-its-pull-the → main 1 commit fork

  • Merged. scout/2279-a-running-check-is-named-on-its-pull-the was merged into main as f805dc3.

Review

Sign in to review this change.
4 files changed +103 −5
src/checks/summary.ts +40 −1
@@ -7,7 +7,7 @@
77 * the run's log, and never a guess at *why* a test failed.
88 */
99
10 import type { CheckDetail } from "../core/runs.ts";
10 import type { CheckDetail, Run } from "../core/runs.ts";
1111 import { parseBunTestFailure } from "./bun-test.ts";
1212
1313 /**
@@ -69,3 +69,42 @@ export function summariseCheckFailure(
6969 }
7070 return { summary: fallback, detail: undefined };
7171 }
72
73 /** `45s`, `12 min`, `1h 5m`: a span in the fewest words that stay true. */
74 export function spanWords(ms: number): string {
75 const seconds = Math.max(0, Math.round(ms / 1000));
76 if (seconds < 60) return `${seconds}s`;
77 const minutes = Math.floor(seconds / 60);
78 if (minutes < 60) return `${minutes} min`;
79 const rest = minutes % 60;
80 return rest === 0
81 ? `${Math.floor(minutes / 60)}h`
82 : `${Math.floor(minutes / 60)}h ${rest}m`;
83 }
84
85 /**
86 * What a pull's checks line says of a *check* run, in the check's own
87 * words — never "deploy". `undefined` for a state the line words itself
88 * (queued, cancelled, none) or when the run is not a check.
89 */
90 export function checkWords(
91 run: Pick<
92 Run,
93 "kind" | "state" | "createdAt" | "startedAt" | "finishedAt" | "summary" | "checkDetail"
94 >,
95 now = Date.now(),
96 ): string | undefined {
97 if (run.kind !== "check") return undefined;
98 const began = Date.parse(run.startedAt ?? run.createdAt);
99 if (run.state === "running")
100 return `Check running · ${spanWords(now - began)}`;
101 if (run.state === "ok")
102 return run.finishedAt === null
103 ? "Check passed"
104 : `Check passed in ${spanWords(Date.parse(run.finishedAt) - began)}`;
105 if (run.state === "failed") {
106 const first = run.checkDetail?.names[0] ?? run.summary;
107 return first === "" ? "Check failed" : `Check failed: ${first}`;
108 }
109 return undefined;
110 }
src/web/pages/pull.ts +6 −4
@@ -34,6 +34,7 @@ import {
3434 type PullStatus,
3535 } from "../../core/pulls.ts";
3636 import { isProposed, pullMayRun } from "../../core/pull_trust.ts";
37 import { checkWords } from "../../checks/summary.ts";
3738 import { checkStale } from "../../checks/stale.ts";
3839 import { checkSummary, type CheckSummary } from "../../core/runs.ts";
3940 import {
@@ -228,8 +229,9 @@ export function checksLine(
228229 ? null
229230 : html`<span class="where-sep" aria-hidden="true">·</span
230231 ><a href="${base}/runs/${run.id}"
231 >deploy ${run.commitOid.slice(0, 7)}</a
232 >${run.kind === "check" ? "Check of" : "deploy"} ${run.commitOid.slice(0, 7)}</a
232233 >`;
234 const words = run === null ? undefined : checkWords(run);
233235 const against =
234236 run !== null && run.baseOid !== "" && baseBranch !== ""
235237 ? html`<span class="where-sep" aria-hidden="true">·</span
@@ -247,16 +249,16 @@ export function checksLine(
247249 >${icon("clock")}Checked against an older ${baseBranch || "base"}</span
248250 >${link}`
249251 : check.state === "ok"
250 ? html`<span class="ok">${icon("check")}Checks passed</span>${link}`
252 ? html`<span class="ok">${icon("check")}${words ?? "Checks passed"}</span>${link}`
251253 : check.state === "failed"
252 ? html`<span class="bad">${icon("x")}Checks failed</span>${link}`
254 ? html`<span class="bad">${icon("x")}${words ?? "Checks failed"}</span>${link}`
253255 : check.state === "queued"
254256 ? html`<span class="now"
255257 >${icon("loader")}Checks
256258 ${run ? queuedPhrase(run.createdAt) : "queued"}</span
257259 >${link}`
258260 : check.state === "running"
259 ? html`<span class="now">${icon("loader")}Checks running</span
261 ? html`<span class="now">${icon("loader")}${words ?? "Checks running"}</span
260262 >${link}`
261263 : check.state === "cancelled"
262264 ? html`<span>${icon("ban")}Checks cancelled</span>${link}`
src/web/pages/runs.ts +5
@@ -68,6 +68,11 @@ export function runTitle(
6868 return number === undefined ? "" : `#${number}`;
6969 })()
7070 : `#${issue.number} ${issue.title}`;
71 if (run.kind === "check") {
72 const number = issue?.number ?? envNumber(run.env);
73 const of = `Check of ${run.commitOid.slice(0, 7)}`;
74 return number === undefined ? of : `${of} for #${number}`;
75 }
7176 if (run.kind === "deploy")
7277 return about === "" ? "Deploy production" : `Deploy preview ${about}`;
7378 const who = actor ?? "an agent";
test/check-named.test.ts added +52
@@ -0,0 +1,52 @@
1 import { expect, test } from "bun:test";
2 import { checkWords } from "../src/checks/summary.ts";
3 import { runTitle } from "../src/web/pages/runs.ts";
4 import { checksLine } from "../src/web/pages/pull.ts";
5 import type { Run } from "../src/core/runs.ts";
6
7 const T0 = Date.parse("2026-10-16T10:00:00Z");
8 const run = (over: Partial<Run> = {}): Run =>
9 ({
10 id: "r1",
11 kind: "check",
12 env: "pull-7",
13 commitOid: "abcdef1234567",
14 state: "running",
15 summary: "",
16 createdAt: "2026-10-16T10:00:00Z",
17 startedAt: "2026-10-16T10:00:00Z",
18 finishedAt: null,
19 checkDetail: null,
20 baseOid: "",
21 ...over,
22 }) as Run;
23 const line = (r: Run) =>
24 checksLine("/a/b", { state: r.state, run: r }, false, true).text;
25
26 test("a running check is named with its age and links to its run", () => {
27 expect(checkWords(run(), T0 + 12 * 60_000)).toBe("Check running · 12 min");
28 const text = line(run());
29 expect(text).toContain("Check running · ");
30 expect(text).toContain('href="/a/b/runs/r1"');
31 expect(text).toContain("Check of");
32 expect(text.toLowerCase()).not.toContain("deploy");
33 });
34
35 test("a finished check says how long it took or what failed first", () => {
36 const done = run({ state: "ok", finishedAt: "2026-10-16T10:03:00Z" });
37 expect(checkWords(done)).toBe("Check passed in 3 min");
38 expect(line(done)).toContain("Check passed in 3 min");
39 const bad = run({
40 state: "failed",
41 summary: "1 of 9 tests",
42 checkDetail: { passed: 8, failed: 1, names: ["it adds"] } as Run["checkDetail"],
43 });
44 expect(checkWords(bad)).toBe("Check failed: it adds");
45 expect(line(bad)).toContain("Check failed: it adds");
46 expect(line(bad).toLowerCase()).not.toContain("deploy");
47 });
48
49 test("a check's run page is titled by its commit and pull", () => {
50 expect(runTitle(run(), undefined, undefined)).toBe("Check of abcdef1 for #7");
51 expect(runTitle(run({ env: "" }), undefined, undefined)).toBe("Check of abcdef1");
52 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs