From 0b29dbe6d8922825cf95e56c7b885d06d16b2ca7 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 8 May 2026 13:17:08 -0700 Subject: [PATCH] feat(runs): link stored pull requests --- apps/fabro-web/app/components/icons.tsx | 7 +++ apps/fabro-web/app/data/runs.test.ts | 17 ++++++ apps/fabro-web/app/data/runs.ts | 4 ++ apps/fabro-web/app/routes/run-detail.test.ts | 32 +++++++++- apps/fabro-web/app/routes/run-detail.tsx | 12 ++++ apps/fabro-web/app/routes/runs.tsx | 59 ++++++++++++------- apps/fabro-web/app/routes/workflow-runs.tsx | 33 +++++++---- docs/public/api-reference/fabro-api.yaml | 9 +++ .../fabro-api/tests/run_summary_round_trip.rs | 21 ++++++- lib/crates/fabro-server/src/demo/mod.rs | 2 + .../fabro-server/src/server/handler/runs.rs | 32 ++++++++++ lib/crates/fabro-store/src/run_state.rs | 37 ++++++++++++ lib/crates/fabro-types/src/run_summary.rs | 48 ++++++++++++++- .../src/models/run-pull-request.ts | 4 ++ .../src/models/run-summary.ts | 4 ++ 15 files changed, 282 insertions(+), 39 deletions(-) create mode 100644 apps/fabro-web/app/components/icons.tsx diff --git a/apps/fabro-web/app/components/icons.tsx b/apps/fabro-web/app/components/icons.tsx new file mode 100644 index 000000000..6ef921ad6 --- /dev/null +++ b/apps/fabro-web/app/components/icons.tsx @@ -0,0 +1,7 @@ +export function GitPullRequestIcon({ className }: { className?: string }) { + return ( + + ); +} diff --git a/apps/fabro-web/app/data/runs.test.ts b/apps/fabro-web/app/data/runs.test.ts index 0ed38a8b0..04aabfee2 100644 --- a/apps/fabro-web/app/data/runs.test.ts +++ b/apps/fabro-web/app/data/runs.test.ts @@ -27,6 +27,10 @@ describe("mapRunListItem", () => { created_at: "2026-04-08T12:00:00Z", start_time: "2026-04-08T12:00:00Z", pending_control: null, + pull_request: { + number: 123, + html_url: "https://github.com/fabro-sh/fabro/pull/123", + }, } as const; const item = mapRunListItem(summary); expect(item.id).toBe("01ABC"); @@ -37,6 +41,8 @@ describe("mapRunListItem", () => { expect(item.elapsed).toBeDefined(); expect(item.column).toBe("running"); expect(item.lifecycleStatus).toBe("paused"); + expect(item.number).toBe(123); + expect(item.pullRequestUrl).toBe("https://github.com/fabro-sh/fabro/pull/123"); }); test("uses a fallback title when the server title is blank", () => { @@ -81,6 +87,15 @@ describe("mapRunSummaryToRunItem", () => { created_at: "2026-04-08T12:00:00Z", start_time: "2026-04-08T12:00:00Z", pending_control: null, + pull_request: { + html_url: "https://github.com/fabro-sh/fabro/pull/456", + number: 456, + owner: "fabro-sh", + repo: "fabro", + base_branch: "main", + head_branch: "fabro/run/demo", + title: "Add run PR chip", + }, }; const item = mapRunSummaryToRunItem(summary); expect(item.id).toBe("01ABC"); @@ -90,6 +105,8 @@ describe("mapRunSummaryToRunItem", () => { expect(item.sourceDirectory).toBe("/home/user/myrepo"); expect(item.elapsed).toBeDefined(); expect(item.lifecycleStatus).toBe("running"); + expect(item.number).toBe(456); + expect(item.pullRequestUrl).toBe("https://github.com/fabro-sh/fabro/pull/456"); }); test("handles missing optional fields", () => { diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts index 6546ecd35..8f47ef866 100644 --- a/apps/fabro-web/app/data/runs.ts +++ b/apps/fabro-web/app/data/runs.ts @@ -26,6 +26,7 @@ export interface RunItem { lifecycleStatus?: RunStatus | null; lifecycleStatusLabel?: string; number?: number; + pullRequestUrl?: string; additions?: number; deletions?: number; checks?: CheckRun[]; @@ -92,6 +93,7 @@ export function mapRunListItem(item: RunListItem): RunItem { lifecycleStatus, lifecycleStatusLabel: lifecycleStatusLabel(item.status), number: item.pull_request?.number, + pullRequestUrl: item.pull_request?.html_url, additions: item.pull_request?.additions, deletions: item.pull_request?.deletions, checks: item.pull_request?.checks?.map((c) => ({ @@ -122,6 +124,8 @@ export function mapRunSummaryToRunItem(summary: RunSummary): RunItem { workflow: summary.workflow_slug ?? summary.workflow_name ?? "unknown", lifecycleStatus, lifecycleStatusLabel: lifecycleStatusLabel(summary.status), + number: summary.pull_request?.number, + pullRequestUrl: summary.pull_request?.html_url, sourceDirectory: summary.source_directory ?? undefined, elapsed: summary.elapsed_secs != null diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts index f2d83db99..f6c4f0619 100644 --- a/apps/fabro-web/app/routes/run-detail.test.ts +++ b/apps/fabro-web/app/routes/run-detail.test.ts @@ -71,7 +71,7 @@ type RunDetailActionResult = import("./run-detail").RunDetailActionResult; const h = createElement; -function makeRunSummary(status = "succeeded", diffSummary: any = null) { +function makeRunSummary(status = "succeeded", diffSummary: any = null, pullRequest: any = null) { return { run_id: "run_1", title: "Run 1", @@ -83,6 +83,7 @@ function makeRunSummary(status = "succeeded", diffSummary: any = null) { elapsed_secs: null, source_directory: null, diff_summary: diffSummary, + pull_request: pullRequest, }; } @@ -109,13 +110,15 @@ async function renderRunDetail({ status = "succeeded", questions = [], diffSummary = null, + pullRequest = null, }: { initialEntry: string; status?: string; questions?: any[]; diffSummary?: any; + pullRequest?: any; }) { - currentRunSummary = makeRunSummary(status, diffSummary); + currentRunSummary = makeRunSummary(status, diffSummary, pullRequest); currentQuestions = questions; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; @@ -399,6 +402,31 @@ describe("RunDetail full-height child routes", () => { expect(tabCountBadges(renderer)).toHaveLength(0); }); + test("shows a linked pull request chip in the run header", async () => { + const renderer = await renderRunDetail({ + initialEntry: "/runs/run_1", + pullRequest: { + html_url: "https://github.com/fabro-sh/fabro/pull/123", + number: 123, + owner: "fabro-sh", + repo: "fabro", + base_branch: "main", + head_branch: "fabro/run/demo", + title: "Add run PR chip", + }, + }); + + const links = renderer.root.findAll( + (node) => + node.type === "a" && + node.props.href === "https://github.com/fabro-sh/fabro/pull/123", + ); + + expect(links).toHaveLength(1); + expect(links[0].props.target).toBe("_blank"); + expect(links[0].children.filter((child) => typeof child !== "object").join("")).toBe("#123"); + }); + test("keeps blocked full-height children clear of the interview dock without an h-72 sibling", async () => { const renderer = await renderRunDetail({ initialEntry: "/runs/run_1/files", diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx index 82abec071..63bcffe0b 100644 --- a/apps/fabro-web/app/routes/run-detail.tsx +++ b/apps/fabro-web/app/routes/run-detail.tsx @@ -17,6 +17,7 @@ import { Link, Outlet, useLocation, useMatches, useNavigate } from "react-router import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { InterviewDock } from "../components/interview-dock"; +import { GitPullRequestIcon } from "../components/icons"; import { SteerBar, type SteerBarHandle } from "../components/steer-bar"; import { ErrorState } from "../components/state"; import { useToast } from "../components/toast"; @@ -323,6 +324,17 @@ export default function RunDetail({ params }: { params: { id: string } }) { )} + {run.number != null && run.pullRequestUrl && ( + + + #{run.number} + + )} diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 2f0f751e9..93dcbbaba 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -25,6 +25,7 @@ import { ciConfig, columnStatusDisplay, columnStatuses, deriveCiStatus, mapRunLi import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus, ColumnStatus } from "../data/runs"; import { formatRelativeTime } from "../lib/format"; import { EmptyState } from "../components/state"; +import { GitPullRequestIcon } from "../components/icons"; import { useToast } from "../components/toast"; import { shouldRefreshBoardForEvent, useBoardEvents } from "../lib/board-events"; import { useAuthConfig, useBoardsRuns, useSystemInfo } from "../lib/queries"; @@ -128,19 +129,6 @@ function listLifecycleStatusLabel(run: Pick