mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
feat(runs): canonicalize paginated run list responses
Unify /api/v1/runs and /api/v1/boards/runs around a shared paginated summary contract with additive convenience fields. Update the server, demo data, generated clients, CLI pagination, and web consumers so board views become a thin projection over the canonical run summary surface.
This commit is contained in:
parent
7933e1ed5c
commit
8ab689da78
20 changed files with 794 additions and 657 deletions
|
|
@ -1,18 +1,52 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import { mapRunSummaryToRunItem } from "./runs";
|
||||
import { mapRunListItem, mapRunSummaryToRunItem } from "./runs";
|
||||
|
||||
describe("mapRunSummaryToRunItem", () => {
|
||||
test("maps store run summary to RunItem", () => {
|
||||
describe("mapRunListItem", () => {
|
||||
test("trusts shared server fields for board items", () => {
|
||||
const summary = {
|
||||
run_id: "01ABC",
|
||||
goal: "Fix the build",
|
||||
goal: "## Fix the build",
|
||||
title: "Server supplied title",
|
||||
workflow_slug: "fix_build",
|
||||
workflow_name: "Fix Build",
|
||||
host_repo_path: "/home/user/myrepo",
|
||||
repository: { name: "myrepo" },
|
||||
status: "running",
|
||||
labels: {},
|
||||
column: "running",
|
||||
elapsed_secs: 65,
|
||||
duration_ms: 65000,
|
||||
total_usd_micros: 500000,
|
||||
created_at: "2026-04-08T12:00:00Z",
|
||||
start_time: "2026-04-08T12:00:00Z",
|
||||
status_reason: null,
|
||||
pending_control: null,
|
||||
} as const;
|
||||
const item = mapRunListItem(summary);
|
||||
expect(item.id).toBe("01ABC");
|
||||
expect(item.title).toBe("Server supplied title");
|
||||
expect(item.workflow).toBe("fix_build");
|
||||
expect(item.repo).toBe("myrepo");
|
||||
expect(item.elapsed).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapRunSummaryToRunItem", () => {
|
||||
test("maps canonical run summary to RunItem", () => {
|
||||
const summary = {
|
||||
run_id: "01ABC",
|
||||
goal: "Fix the build",
|
||||
title: "Fix the build",
|
||||
workflow_slug: "fix_build",
|
||||
workflow_name: "Fix Build",
|
||||
host_repo_path: "/home/user/myrepo",
|
||||
repository: { name: "myrepo" },
|
||||
status: "running",
|
||||
duration_ms: 65000,
|
||||
elapsed_secs: 65,
|
||||
total_usd_micros: 500000,
|
||||
labels: {},
|
||||
created_at: "2026-04-08T12:00:00Z",
|
||||
start_time: "2026-04-08T12:00:00Z",
|
||||
status_reason: null,
|
||||
pending_control: null,
|
||||
|
|
@ -28,21 +62,25 @@ describe("mapRunSummaryToRunItem", () => {
|
|||
test("handles missing optional fields", () => {
|
||||
const summary = {
|
||||
run_id: "01DEF",
|
||||
goal: null,
|
||||
goal: "",
|
||||
title: "",
|
||||
workflow_slug: null,
|
||||
workflow_name: null,
|
||||
host_repo_path: null,
|
||||
repository: { name: "unknown" },
|
||||
status: "submitted",
|
||||
duration_ms: null,
|
||||
elapsed_secs: null,
|
||||
total_usd_micros: null,
|
||||
labels: {},
|
||||
created_at: "2026-04-08T12:00:00Z",
|
||||
start_time: null,
|
||||
status_reason: null,
|
||||
pending_control: null,
|
||||
};
|
||||
const item = mapRunSummaryToRunItem(summary);
|
||||
expect(item.id).toBe("01DEF");
|
||||
expect(item.title).toBe("Untitled run");
|
||||
expect(item.title).toBe("");
|
||||
expect(item.workflow).toBe("unknown");
|
||||
expect(item.repo).toBe("unknown");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { formatElapsedSecs, formatDurationSecs } from "../lib/format";
|
||||
import type { RunListItem } from "@qltysh/fabro-api-client";
|
||||
import type { RunListItem, StoreRunSummary } from "@qltysh/fabro-api-client";
|
||||
|
||||
export type CiStatus = "passing" | "failing" | "pending";
|
||||
|
||||
|
|
@ -29,13 +29,10 @@ export interface RunItem {
|
|||
sandboxId?: string;
|
||||
}
|
||||
|
||||
export type ColumnStatus = "working" | "initializing" | "review" | "merge" | "running" | "waiting" | "succeeded" | "failed";
|
||||
export type ColumnStatus = "initializing" | "running" | "waiting" | "succeeded" | "failed";
|
||||
|
||||
export const columnNames: Record<ColumnStatus, string> = {
|
||||
working: "Working",
|
||||
initializing: "Initializing",
|
||||
review: "Verify",
|
||||
merge: "Merge",
|
||||
running: "Running",
|
||||
waiting: "Waiting",
|
||||
succeeded: "Succeeded",
|
||||
|
|
@ -47,17 +44,12 @@ export interface RunWithStatus extends RunItem {
|
|||
statusLabel: string;
|
||||
}
|
||||
|
||||
function truncateGoal(goal: string): string {
|
||||
const firstLine = goal.split("\n")[0].replace(/^#+\s*/, "").trim();
|
||||
return firstLine.length > 100 ? firstLine.slice(0, 100) + "…" : firstLine;
|
||||
}
|
||||
|
||||
export function mapRunListItem(item: RunListItem): RunItem {
|
||||
return {
|
||||
id: item.id,
|
||||
id: item.run_id,
|
||||
repo: item.repository.name,
|
||||
title: truncateGoal(item.title),
|
||||
workflow: item.workflow.slug,
|
||||
title: item.title,
|
||||
workflow: item.workflow_slug ?? item.workflow_name ?? "unknown",
|
||||
number: item.pull_request?.number,
|
||||
additions: item.pull_request?.additions,
|
||||
deletions: item.pull_request?.deletions,
|
||||
|
|
@ -66,8 +58,7 @@ export function mapRunListItem(item: RunListItem): RunItem {
|
|||
status: c.status,
|
||||
duration: c.duration_secs != null ? formatDurationSecs(c.duration_secs) : undefined,
|
||||
})),
|
||||
elapsed: item.timings?.elapsed_secs != null ? formatElapsedSecs(item.timings.elapsed_secs) : undefined,
|
||||
elapsedWarning: item.timings?.elapsed_warning,
|
||||
elapsed: item.elapsed_secs != null ? formatElapsedSecs(item.elapsed_secs) : undefined,
|
||||
resources: item.sandbox?.resources ? `${item.sandbox.resources.cpu} CPU / ${item.sandbox.resources.memory} GB` : undefined,
|
||||
comments: item.pull_request?.comments,
|
||||
question: item.question?.text,
|
||||
|
|
@ -75,36 +66,42 @@ export function mapRunListItem(item: RunListItem): RunItem {
|
|||
};
|
||||
}
|
||||
|
||||
export interface RunSummaryResponse {
|
||||
run_id: string;
|
||||
goal: string | null;
|
||||
workflow_slug: string | null;
|
||||
workflow_name: string | null;
|
||||
host_repo_path: string | null;
|
||||
status: string | null;
|
||||
status_reason: string | null;
|
||||
pending_control: string | null;
|
||||
duration_ms: number | null;
|
||||
total_usd_micros: number | null;
|
||||
labels: Record<string, string>;
|
||||
start_time: string | null;
|
||||
}
|
||||
export type RunSummaryResponse = StoreRunSummary;
|
||||
|
||||
export function mapRunSummaryToRunItem(summary: RunSummaryResponse): RunItem {
|
||||
const repoPath = summary.host_repo_path ?? "";
|
||||
const repoName = repoPath.split("/").pop() || "unknown";
|
||||
return {
|
||||
id: summary.run_id,
|
||||
repo: repoName,
|
||||
title: summary.goal ? truncateGoal(summary.goal) : "Untitled run",
|
||||
workflow: summary.workflow_slug ?? "unknown",
|
||||
repo: summary.repository.name,
|
||||
title: summary.title,
|
||||
workflow: summary.workflow_slug ?? summary.workflow_name ?? "unknown",
|
||||
elapsed:
|
||||
summary.duration_ms != null
|
||||
summary.elapsed_secs != null
|
||||
? formatElapsedSecs(summary.elapsed_secs)
|
||||
: summary.duration_ms != null
|
||||
? formatElapsedSecs(summary.duration_ms / 1000)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function columnForStatus(status: string | null | undefined): ColumnStatus {
|
||||
switch (status) {
|
||||
case "submitted":
|
||||
case "starting":
|
||||
return "initializing";
|
||||
case "running":
|
||||
return "running";
|
||||
case "paused":
|
||||
return "waiting";
|
||||
case "succeeded":
|
||||
return "succeeded";
|
||||
case "failed":
|
||||
case "dead":
|
||||
case "removing":
|
||||
default:
|
||||
return "failed";
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveCiStatus(checks: CheckRun[]): CiStatus {
|
||||
if (checks.some((c) => c.status === "failure")) return "failing";
|
||||
if (checks.some((c) => c.status === "pending" || c.status === "queued")) return "pending";
|
||||
|
|
@ -112,10 +109,7 @@ export function deriveCiStatus(checks: CheckRun[]): CiStatus {
|
|||
}
|
||||
|
||||
export const statusColors: Record<ColumnStatus, { dot: string; text: string }> = {
|
||||
working: { dot: "bg-teal-500", text: "text-teal-500" },
|
||||
initializing: { dot: "bg-amber", text: "text-amber" },
|
||||
review: { dot: "bg-mint", text: "text-mint" },
|
||||
merge: { dot: "bg-teal-300", text: "text-teal-300" },
|
||||
running: { dot: "bg-teal-500", text: "text-teal-500" },
|
||||
waiting: { dot: "bg-amber", text: "text-amber" },
|
||||
succeeded: { dot: "bg-teal-300", text: "text-teal-300" },
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { CSS } from "@dnd-kit/utilities";
|
|||
import { ciConfig, statusColors, deriveCiStatus, mapRunListItem } from "../data/runs";
|
||||
import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus, ColumnStatus } from "../data/runs";
|
||||
import { apiJson } from "../api";
|
||||
import type { PaginatedRunList } from "@qltysh/fabro-api-client";
|
||||
import type { PaginatedBoardRunList } from "@qltysh/fabro-api-client";
|
||||
|
||||
export function meta({}: any) {
|
||||
return [{ title: "Runs — Fabro" }];
|
||||
|
|
@ -35,10 +35,7 @@ interface ColumnStyle {
|
|||
}
|
||||
|
||||
const columnStyles: Record<string, ColumnStyle> = {
|
||||
working: { accent: "bg-teal-500", iconColor: "text-teal-500", iconType: "branch", actions: ["Watch", "Steer"] },
|
||||
initializing: { accent: "bg-amber", iconColor: "text-amber", iconType: "branch", actions: [] },
|
||||
review: { accent: "bg-mint", iconColor: "text-mint", iconType: "pr", actions: [] },
|
||||
merge: { accent: "bg-teal-300", iconColor: "text-teal-300", iconType: "pr", actions: ["Merge"] },
|
||||
running: { accent: "bg-teal-500", iconColor: "text-teal-500", iconType: "branch", actions: ["Watch", "Steer"] },
|
||||
waiting: { accent: "bg-amber", iconColor: "text-amber", iconType: "branch", actions: ["Answer Question"] },
|
||||
succeeded: { accent: "bg-teal-300", iconColor: "text-teal-300", iconType: "pr", actions: [] },
|
||||
|
|
@ -49,8 +46,8 @@ const defaultColumnStyle: ColumnStyle = { accent: "bg-fg-muted", iconColor: "tex
|
|||
|
||||
interface BoardRunsResponse {
|
||||
columns: { id: string; name: string }[];
|
||||
data: PaginatedRunList["data"];
|
||||
meta: PaginatedRunList["meta"];
|
||||
data: PaginatedBoardRunList["data"];
|
||||
meta: PaginatedBoardRunList["meta"];
|
||||
}
|
||||
|
||||
export async function loader({ request }: any) {
|
||||
|
|
@ -62,8 +59,8 @@ export async function loader({ request }: any) {
|
|||
grouped.set(col.id, []);
|
||||
}
|
||||
for (const apiRun of apiRuns) {
|
||||
if (grouped.has(apiRun.status)) {
|
||||
grouped.get(apiRun.status)?.push(mapRunListItem(apiRun));
|
||||
if (grouped.has(apiRun.column)) {
|
||||
grouped.get(apiRun.column)?.push(mapRunListItem(apiRun));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import { ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { Link, useParams } from "react-router";
|
||||
import { ciConfig, columnNames, deriveCiStatus, mapRunListItem, statusColors } from "../data/runs";
|
||||
import { ciConfig, columnNames, columnForStatus, deriveCiStatus, mapRunSummaryToRunItem, statusColors } from "../data/runs";
|
||||
import type { ColumnStatus, RunWithStatus } from "../data/runs";
|
||||
import { apiJsonOrNull } from "../api";
|
||||
import type { PaginatedRunList } from "@qltysh/fabro-api-client";
|
||||
|
|
@ -10,9 +10,9 @@ export async function loader({ request, params }: any) {
|
|||
const result = await apiJsonOrNull<PaginatedRunList>(`/workflows/${params.name}/runs`, { request });
|
||||
const apiRuns = result?.data ?? [];
|
||||
const runs: RunWithStatus[] = apiRuns.map((r) => ({
|
||||
...mapRunListItem(r),
|
||||
status: r.status as ColumnStatus,
|
||||
statusLabel: columnNames[r.status as ColumnStatus] ?? r.status,
|
||||
...mapRunSummaryToRunItem(r),
|
||||
status: columnForStatus(r.status),
|
||||
statusLabel: columnNames[columnForStatus(r.status)],
|
||||
}));
|
||||
return { runs };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,15 +124,16 @@ paths:
|
|||
tags: [Runs]
|
||||
summary: List Runs
|
||||
description: Returns durable run summaries from the backing store, including runs persisted before the current server boot.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PageLimit"
|
||||
- $ref: "#/components/parameters/PageOffset"
|
||||
responses:
|
||||
"200":
|
||||
description: Durable run summaries
|
||||
description: Paginated durable run summaries
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/StoreRunSummary"
|
||||
$ref: "#/components/schemas/PaginatedRunList"
|
||||
post:
|
||||
operationId: createRun
|
||||
tags: [Runs]
|
||||
|
|
@ -428,7 +429,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PaginatedRunList"
|
||||
$ref: "#/components/schemas/PaginatedBoardRunList"
|
||||
|
||||
/api/v1/runs/{id}/state:
|
||||
get:
|
||||
|
|
@ -1702,6 +1703,25 @@ components:
|
|||
- data
|
||||
- meta
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/StoreRunSummary"
|
||||
meta:
|
||||
$ref: "#/components/schemas/PaginationMeta"
|
||||
|
||||
PaginatedBoardRunList:
|
||||
description: Paginated list of board runs with shared canonical fields plus board metadata.
|
||||
type: object
|
||||
required:
|
||||
- columns
|
||||
- data
|
||||
- meta
|
||||
properties:
|
||||
columns:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/BoardColumnDefinition"
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -3007,7 +3027,11 @@ components:
|
|||
type: object
|
||||
required:
|
||||
- run_id
|
||||
- goal
|
||||
- title
|
||||
- labels
|
||||
- repository
|
||||
- created_at
|
||||
properties:
|
||||
run_id:
|
||||
type: string
|
||||
|
|
@ -3016,16 +3040,23 @@ components:
|
|||
workflow_slug:
|
||||
type: ["string", "null"]
|
||||
goal:
|
||||
type: ["string", "null"]
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
labels:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
host_repo_path:
|
||||
type: ["string", "null"]
|
||||
repository:
|
||||
$ref: "#/components/schemas/RepositoryReference"
|
||||
start_time:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
status:
|
||||
type: ["string", "null"]
|
||||
status_reason:
|
||||
|
|
@ -3038,6 +3069,8 @@ components:
|
|||
type: ["integer", "null"]
|
||||
format: int64
|
||||
minimum: 0
|
||||
elapsed_secs:
|
||||
type: ["number", "null"]
|
||||
total_usd_micros:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
|
|
@ -3048,10 +3081,22 @@ components:
|
|||
description: Board column status for a run in the list view.
|
||||
type: string
|
||||
enum:
|
||||
- working
|
||||
- initializing
|
||||
- review
|
||||
- merge
|
||||
- running
|
||||
- waiting
|
||||
- succeeded
|
||||
- failed
|
||||
|
||||
BoardColumnDefinition:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
CheckRunStatus:
|
||||
description: Status of a CI check run.
|
||||
|
|
@ -3346,34 +3391,66 @@ components:
|
|||
# ── Run Board Schemas (updated) ─────────────────────────────────────
|
||||
|
||||
RunListItem:
|
||||
description: Summary of a run shown in the board view.
|
||||
description: Canonical run summary shown in the board view, extended with board-specific metadata.
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- repository
|
||||
- run_id
|
||||
- goal
|
||||
- title
|
||||
- workflow
|
||||
- status
|
||||
- labels
|
||||
- repository
|
||||
- created_at
|
||||
- column
|
||||
properties:
|
||||
id:
|
||||
run_id:
|
||||
type: string
|
||||
description: Unique run identifier (ULID).
|
||||
example: 01JNQVR7M0EJ5GKAT2SC4ERS1Z
|
||||
workflow_name:
|
||||
type: ["string", "null"]
|
||||
workflow_slug:
|
||||
type: ["string", "null"]
|
||||
goal:
|
||||
type: string
|
||||
repository:
|
||||
$ref: "#/components/schemas/RepositoryReference"
|
||||
title:
|
||||
type: string
|
||||
description: Human-readable title describing the run's goal.
|
||||
example: Add rate limiting to auth endpoints
|
||||
workflow:
|
||||
$ref: "#/components/schemas/WorkflowReference"
|
||||
status:
|
||||
type: string
|
||||
labels:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
host_repo_path:
|
||||
type: ["string", "null"]
|
||||
start_time:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
status_reason:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/StatusReason"
|
||||
- type: "null"
|
||||
pending_control:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/RunControlAction"
|
||||
- type: "null"
|
||||
duration_ms:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
minimum: 0
|
||||
elapsed_secs:
|
||||
type: ["number", "null"]
|
||||
total_usd_micros:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
column:
|
||||
$ref: "#/components/schemas/BoardColumn"
|
||||
pull_request:
|
||||
$ref: "#/components/schemas/RunPullRequest"
|
||||
timings:
|
||||
$ref: "#/components/schemas/RunTimings"
|
||||
sandbox:
|
||||
$ref: "#/components/schemas/RunSandbox"
|
||||
question:
|
||||
|
|
|
|||
|
|
@ -589,17 +589,35 @@ impl ServerStoreClient {
|
|||
}
|
||||
|
||||
pub(crate) async fn list_store_runs(&self) -> Result<Vec<RunSummary>> {
|
||||
let response = self
|
||||
.client
|
||||
.list_runs()
|
||||
.send()
|
||||
.await
|
||||
.map_err(map_api_error)?;
|
||||
response
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(convert_type)
|
||||
.collect::<Result<Vec<_>>>()
|
||||
let mut all_runs = Vec::new();
|
||||
let mut offset = 0_u64;
|
||||
let limit = 100_u64;
|
||||
|
||||
loop {
|
||||
let response = self
|
||||
.client
|
||||
.list_runs()
|
||||
.page_limit(limit)
|
||||
.page_offset(offset)
|
||||
.send()
|
||||
.await
|
||||
.map_err(map_api_error)?;
|
||||
let parsed = response.into_inner();
|
||||
let batch = parsed
|
||||
.data
|
||||
.into_iter()
|
||||
.map(convert_type)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let batch_len = batch.len() as u64;
|
||||
all_runs.extend(batch);
|
||||
|
||||
if !parsed.meta.has_more || batch_len == 0 {
|
||||
break;
|
||||
}
|
||||
offset += batch_len;
|
||||
}
|
||||
|
||||
Ok(all_runs)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_run_state(&self, run_id: &RunId) -> Result<RunProjection> {
|
||||
|
|
|
|||
|
|
@ -263,23 +263,27 @@ fn ps_uses_configured_server_target_without_server_flag() {
|
|||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::json!([
|
||||
{
|
||||
serde_json::json!({
|
||||
"data": [{
|
||||
"run_id": run_id,
|
||||
"workflow_name": "Remote Workflow",
|
||||
"workflow_slug": "remote-workflow",
|
||||
"goal": "Remote goal",
|
||||
"title": "Remote goal",
|
||||
"labels": {
|
||||
"suite": "remote"
|
||||
},
|
||||
"host_repo_path": "/srv/repo",
|
||||
"repository": { "name": "repo" },
|
||||
"start_time": "2026-04-05T12:00:00Z",
|
||||
"created_at": "2026-04-05T12:00:00Z",
|
||||
"status": "succeeded",
|
||||
"status_reason": null,
|
||||
"duration_ms": 123,
|
||||
"total_usd_micros": null
|
||||
}
|
||||
])
|
||||
}],
|
||||
"meta": { "has_more": false }
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -157,21 +157,25 @@ fn rm_force_removes_active_run() {
|
|||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::json!([
|
||||
{
|
||||
serde_json::json!({
|
||||
"data": [{
|
||||
"run_id": run_id,
|
||||
"workflow_name": "Active Workflow",
|
||||
"workflow_slug": "active-workflow",
|
||||
"goal": "Active goal",
|
||||
"title": "Active goal",
|
||||
"labels": {},
|
||||
"host_repo_path": null,
|
||||
"repository": { "name": "unknown" },
|
||||
"start_time": "2026-04-05T12:00:00Z",
|
||||
"created_at": "2026-04-05T12:00:00Z",
|
||||
"status": "running",
|
||||
"status_reason": null,
|
||||
"duration_ms": 123,
|
||||
"total_usd_micros": null
|
||||
}
|
||||
])
|
||||
}],
|
||||
"meta": { "has_more": false }
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
});
|
||||
|
|
@ -270,21 +274,25 @@ fn rm_uses_configured_server_target_without_local_run_dir() {
|
|||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::json!([
|
||||
{
|
||||
serde_json::json!({
|
||||
"data": [{
|
||||
"run_id": run_id,
|
||||
"workflow_name": "Remote Workflow",
|
||||
"workflow_slug": "remote-workflow",
|
||||
"goal": "Remote goal",
|
||||
"title": "Remote goal",
|
||||
"labels": {},
|
||||
"host_repo_path": null,
|
||||
"repository": { "name": "unknown" },
|
||||
"start_time": "2026-04-05T12:00:00Z",
|
||||
"created_at": "2026-04-05T12:00:00Z",
|
||||
"status": "succeeded",
|
||||
"status_reason": null,
|
||||
"duration_ms": 123,
|
||||
"total_usd_micros": null
|
||||
}
|
||||
])
|
||||
}],
|
||||
"meta": { "has_more": false }
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -238,21 +238,25 @@ fn attach_smoke_covers_arg_validation_and_remote_server_behaviors() {
|
|||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::json!([
|
||||
{
|
||||
serde_json::json!({
|
||||
"data": [{
|
||||
"run_id": success_run_id,
|
||||
"workflow_name": "Remote Workflow",
|
||||
"workflow_slug": "remote-workflow",
|
||||
"goal": "Remote output",
|
||||
"title": "Remote output",
|
||||
"labels": {},
|
||||
"host_repo_path": null,
|
||||
"repository": { "name": "unknown" },
|
||||
"start_time": "2026-04-05T12:00:00Z",
|
||||
"created_at": "2026-04-05T12:00:00Z",
|
||||
"status": "running",
|
||||
"status_reason": null,
|
||||
"duration_ms": 12,
|
||||
"total_usd_micros": null
|
||||
}
|
||||
])
|
||||
}],
|
||||
"meta": { "has_more": false }
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
});
|
||||
|
|
@ -336,21 +340,25 @@ fn attach_smoke_covers_arg_validation_and_remote_server_behaviors() {
|
|||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(
|
||||
serde_json::json!([
|
||||
{
|
||||
serde_json::json!({
|
||||
"data": [{
|
||||
"run_id": eof_run_id,
|
||||
"workflow_name": "Remote Workflow",
|
||||
"workflow_slug": "remote-workflow",
|
||||
"goal": "Remote output",
|
||||
"title": "Remote output",
|
||||
"labels": {},
|
||||
"host_repo_path": null,
|
||||
"repository": { "name": "unknown" },
|
||||
"start_time": "2026-04-05T12:00:00Z",
|
||||
"created_at": "2026-04-05T12:00:00Z",
|
||||
"status": "running",
|
||||
"status_reason": null,
|
||||
"duration_ms": 12,
|
||||
"total_cost": null
|
||||
}
|
||||
])
|
||||
"total_usd_micros": null
|
||||
}],
|
||||
"meta": { "has_more": false }
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
#![allow(clippy::default_trait_access, clippy::unreadable_literal)]
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
|
|
@ -42,7 +41,7 @@ pub(crate) async fn list_runs(
|
|||
State(_state): State<Arc<AppState>>,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> Response {
|
||||
paginated_response(runs::list_items(), &pagination)
|
||||
paginated_response(runs::summaries(), &pagination)
|
||||
}
|
||||
|
||||
pub(crate) async fn list_board_runs(
|
||||
|
|
@ -50,21 +49,19 @@ pub(crate) async fn list_board_runs(
|
|||
State(_state): State<Arc<AppState>>,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> Response {
|
||||
let items = runs::list_items();
|
||||
let items = runs::board_items();
|
||||
let limit = pagination.limit.clamp(1, 100) as usize;
|
||||
let offset = pagination.offset as usize;
|
||||
let mut data: Vec<_> = items.into_iter().skip(offset).take(limit + 1).collect();
|
||||
let has_more = data.len() > limit;
|
||||
data.truncate(limit);
|
||||
let columns = json!([
|
||||
{"id": "working", "name": "Working"},
|
||||
{"id": "pending", "name": "Pending"},
|
||||
{"id": "review", "name": "Review"},
|
||||
{"id": "merge", "name": "Merge"},
|
||||
]);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({ "columns": columns, "data": data, "meta": { "has_more": has_more } })),
|
||||
Json(json!({
|
||||
"columns": runs::columns(),
|
||||
"data": data,
|
||||
"meta": { "has_more": has_more }
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
|
@ -202,32 +199,8 @@ pub(crate) async fn get_run_status(
|
|||
State(_state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
match runs::list_items().into_iter().find(|r| r.id == id) {
|
||||
Some(item) => {
|
||||
let elapsed_ms = item
|
||||
.timings
|
||||
.as_ref()
|
||||
.and_then(|t| Duration::try_from_secs_f64(t.elapsed_secs).ok())
|
||||
.and_then(|duration| u64::try_from(duration.as_millis()).ok());
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"run_id": item.id,
|
||||
"goal": item.title,
|
||||
"workflow_slug": item.workflow.slug,
|
||||
"workflow_name": item.workflow.slug,
|
||||
"host_repo_path": format!("/demo/{}", item.repository.name),
|
||||
"labels": {},
|
||||
"start_time": item.created_at.to_rfc3339(),
|
||||
"status": "running",
|
||||
"status_reason": null,
|
||||
"pending_control": null,
|
||||
"duration_ms": elapsed_ms,
|
||||
"total_usd_micros": null,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
match runs::summaries().into_iter().find(|run| run.run_id == id) {
|
||||
Some(run) => (StatusCode::OK, Json(run)).into_response(),
|
||||
None => ApiError::not_found("Run not found.").into_response(),
|
||||
}
|
||||
}
|
||||
|
|
@ -669,462 +642,310 @@ fn ts(s: &str) -> DateTime<Utc> {
|
|||
}
|
||||
|
||||
mod runs {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use fabro_api::types::*;
|
||||
|
||||
use super::ts;
|
||||
|
||||
pub(super) fn list_items() -> Vec<RunListItem> {
|
||||
fn labels(entries: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
entries
|
||||
.iter()
|
||||
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn summary(
|
||||
run_id: &str,
|
||||
repo_name: &str,
|
||||
workflow_slug: &str,
|
||||
workflow_name: &str,
|
||||
goal: &str,
|
||||
status: Option<&str>,
|
||||
created_at: &str,
|
||||
elapsed_secs: Option<f64>,
|
||||
status_reason: Option<&str>,
|
||||
pending_control: Option<RunControlAction>,
|
||||
total_usd_micros: Option<i64>,
|
||||
entries: &[(&str, &str)],
|
||||
) -> StoreRunSummary {
|
||||
StoreRunSummary {
|
||||
created_at: ts(created_at),
|
||||
duration_ms: elapsed_secs.map(|secs| (secs * 1000.0).round() as i64),
|
||||
elapsed_secs,
|
||||
goal: goal.into(),
|
||||
host_repo_path: Some(format!("/demo/{repo_name}")),
|
||||
labels: labels(entries),
|
||||
pending_control,
|
||||
repository: RepositoryReference {
|
||||
name: repo_name.into(),
|
||||
},
|
||||
run_id: run_id.into(),
|
||||
start_time: Some(ts(created_at)),
|
||||
status: status.map(str::to_string),
|
||||
status_reason: status_reason.map(str::to_string),
|
||||
title: goal.into(),
|
||||
total_usd_micros,
|
||||
workflow_name: Some(workflow_name.into()),
|
||||
workflow_slug: Some(workflow_slug.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_summary(
|
||||
summaries: &mut HashMap<String, StoreRunSummary>,
|
||||
run_id: &str,
|
||||
) -> StoreRunSummary {
|
||||
summaries
|
||||
.remove(run_id)
|
||||
.unwrap_or_else(|| panic!("missing demo summary: {run_id}"))
|
||||
}
|
||||
|
||||
fn board_item(
|
||||
summary: StoreRunSummary,
|
||||
column: BoardColumn,
|
||||
pull_request: Option<RunPullRequest>,
|
||||
sandbox: Option<RunSandbox>,
|
||||
question: Option<RunQuestion>,
|
||||
) -> RunListItem {
|
||||
let status_reason = summary
|
||||
.status_reason
|
||||
.as_deref()
|
||||
.and_then(|reason| StatusReason::try_from(reason).ok());
|
||||
|
||||
RunListItem {
|
||||
column,
|
||||
created_at: summary.created_at,
|
||||
duration_ms: summary.duration_ms,
|
||||
elapsed_secs: summary.elapsed_secs,
|
||||
goal: summary.goal,
|
||||
host_repo_path: summary.host_repo_path,
|
||||
labels: summary.labels,
|
||||
pending_control: summary.pending_control,
|
||||
pull_request,
|
||||
question,
|
||||
repository: summary.repository,
|
||||
run_id: summary.run_id,
|
||||
sandbox,
|
||||
start_time: summary.start_time,
|
||||
status: summary.status.unwrap_or_default(),
|
||||
status_reason,
|
||||
title: summary.title,
|
||||
total_usd_micros: summary.total_usd_micros,
|
||||
workflow_name: summary.workflow_name,
|
||||
workflow_slug: summary.workflow_slug,
|
||||
}
|
||||
}
|
||||
|
||||
fn check(name: &str, status: CheckRunStatus, duration_secs: Option<f64>) -> CheckRun {
|
||||
CheckRun {
|
||||
name: name.into(),
|
||||
status,
|
||||
duration_secs,
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox(id: &str, cpu: i64, memory: i64) -> RunSandbox {
|
||||
RunSandbox {
|
||||
id: id.into(),
|
||||
resources: Some(SandboxResources { cpu, memory }),
|
||||
}
|
||||
}
|
||||
|
||||
fn pull_request(
|
||||
number: i64,
|
||||
additions: i64,
|
||||
deletions: i64,
|
||||
comments: i64,
|
||||
checks: Vec<CheckRun>,
|
||||
) -> RunPullRequest {
|
||||
RunPullRequest {
|
||||
number,
|
||||
additions: Some(additions),
|
||||
deletions: Some(deletions),
|
||||
comments: Some(comments),
|
||||
checks,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn columns() -> Vec<BoardColumnDefinition> {
|
||||
vec![
|
||||
RunListItem {
|
||||
id: "run-1".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "api-server".into(),
|
||||
},
|
||||
title: "Add rate limiting to auth endpoints".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "implement".into(),
|
||||
},
|
||||
status: BoardColumn::Working,
|
||||
pull_request: None,
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 420.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-a1b2c3d4".into(),
|
||||
resources: Some(SandboxResources {
|
||||
cpu: 4,
|
||||
memory: 8,
|
||||
}),
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-03-06T14:30:00Z"),
|
||||
BoardColumnDefinition {
|
||||
id: "initializing".into(),
|
||||
name: "Initializing".into(),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-2".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "web-dashboard".into(),
|
||||
},
|
||||
title: "Migrate to React Router v7".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "implement".into(),
|
||||
},
|
||||
status: BoardColumn::Working,
|
||||
pull_request: None,
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 8100.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-e5f6g7h8".into(),
|
||||
resources: Some(SandboxResources {
|
||||
cpu: 8,
|
||||
memory: 16,
|
||||
}),
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-03-06T12:00:00Z"),
|
||||
BoardColumnDefinition {
|
||||
id: "running".into(),
|
||||
name: "Running".into(),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-3".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "cli-tools".into(),
|
||||
},
|
||||
title: "Fix config parsing for nested values".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "fix_build".into(),
|
||||
},
|
||||
status: BoardColumn::Working,
|
||||
pull_request: None,
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 2700.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-i9j0k1l2".into(),
|
||||
resources: Some(SandboxResources {
|
||||
cpu: 2,
|
||||
memory: 4,
|
||||
}),
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-03-05T09:20:00Z"),
|
||||
BoardColumnDefinition {
|
||||
id: "waiting".into(),
|
||||
name: "Waiting".into(),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-4".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "api-server".into(),
|
||||
},
|
||||
title: "Update OpenAPI spec for v3".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "expand".into(),
|
||||
},
|
||||
status: BoardColumn::Initializing,
|
||||
pull_request: Some(RunPullRequest {
|
||||
number: 0,
|
||||
additions: Some(567),
|
||||
deletions: Some(234),
|
||||
comments: Some(0),
|
||||
checks: vec![],
|
||||
}),
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 4320.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-q7r8s9t0".into(),
|
||||
resources: None,
|
||||
}),
|
||||
question: Some(RunQuestion {
|
||||
BoardColumnDefinition {
|
||||
id: "succeeded".into(),
|
||||
name: "Succeeded".into(),
|
||||
},
|
||||
BoardColumnDefinition {
|
||||
id: "failed".into(),
|
||||
name: "Failed".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub(super) fn summaries() -> Vec<StoreRunSummary> {
|
||||
vec![
|
||||
summary(
|
||||
"run-1",
|
||||
"api-server",
|
||||
"implement",
|
||||
"Implement",
|
||||
"Add rate limiting to auth endpoints",
|
||||
Some("running"),
|
||||
"2026-03-06T14:30:00Z",
|
||||
Some(420.0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[("branch", "rate-limit"), ("team", "platform")],
|
||||
),
|
||||
summary(
|
||||
"run-2",
|
||||
"web-dashboard",
|
||||
"implement",
|
||||
"Implement",
|
||||
"Migrate to React Router v7",
|
||||
Some("running"),
|
||||
"2026-03-06T12:00:00Z",
|
||||
Some(8100.0),
|
||||
None,
|
||||
Some(RunControlAction::Pause),
|
||||
None,
|
||||
&[("owner", "frontend")],
|
||||
),
|
||||
summary(
|
||||
"run-3",
|
||||
"shared-types",
|
||||
"expand",
|
||||
"Expand",
|
||||
"Update OpenAPI spec for v3",
|
||||
Some("starting"),
|
||||
"2026-03-04T15:00:00Z",
|
||||
Some(4320.0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[("priority", "high")],
|
||||
),
|
||||
summary(
|
||||
"run-4",
|
||||
"shared-types",
|
||||
"implement",
|
||||
"Implement",
|
||||
"Add pipeline event types",
|
||||
Some("paused"),
|
||||
"2026-03-04T10:00:00Z",
|
||||
Some(1680.0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&[("owner", "runtime")],
|
||||
),
|
||||
summary(
|
||||
"run-5",
|
||||
"web-dashboard",
|
||||
"implement",
|
||||
"Implement",
|
||||
"Add dark mode toggle",
|
||||
Some("failed"),
|
||||
"2026-03-03T16:45:00Z",
|
||||
Some(2100.0),
|
||||
Some("workflow_error"),
|
||||
None,
|
||||
None,
|
||||
&[("environment", "staging")],
|
||||
),
|
||||
summary(
|
||||
"run-6",
|
||||
"api-server",
|
||||
"implement",
|
||||
"Implement",
|
||||
"Implement webhook retry logic",
|
||||
Some("succeeded"),
|
||||
"2026-02-28T14:00:00Z",
|
||||
Some(259200.0),
|
||||
Some("completed"),
|
||||
None,
|
||||
Some(720000),
|
||||
&[("release", "preview")],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub(super) fn board_items() -> Vec<RunListItem> {
|
||||
let mut summaries = summaries()
|
||||
.into_iter()
|
||||
.map(|summary| (summary.run_id.clone(), summary))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
vec![
|
||||
board_item(
|
||||
take_summary(&mut summaries, "run-1"),
|
||||
BoardColumn::Running,
|
||||
None,
|
||||
Some(sandbox("sb-a1b2c3d4", 4, 8)),
|
||||
None,
|
||||
),
|
||||
board_item(
|
||||
take_summary(&mut summaries, "run-2"),
|
||||
BoardColumn::Running,
|
||||
None,
|
||||
Some(sandbox("sb-e5f6g7h8", 8, 16)),
|
||||
None,
|
||||
),
|
||||
board_item(
|
||||
take_summary(&mut summaries, "run-3"),
|
||||
BoardColumn::Initializing,
|
||||
Some(pull_request(0, 567, 234, 0, vec![])),
|
||||
Some(sandbox("sb-q7r8s9t0", 4, 8)),
|
||||
Some(RunQuestion {
|
||||
text: "Accept or push for another round?".into(),
|
||||
}),
|
||||
created_at: ts("2026-03-04T15:00:00Z"),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-5".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "shared-types".into(),
|
||||
},
|
||||
title: "Add pipeline event types".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "implement".into(),
|
||||
},
|
||||
status: BoardColumn::Initializing,
|
||||
pull_request: Some(RunPullRequest {
|
||||
number: 0,
|
||||
additions: Some(145),
|
||||
deletions: Some(23),
|
||||
comments: Some(0),
|
||||
checks: vec![],
|
||||
}),
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 1680.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-u1v2w3x4".into(),
|
||||
resources: None,
|
||||
}),
|
||||
question: Some(RunQuestion {
|
||||
),
|
||||
board_item(
|
||||
take_summary(&mut summaries, "run-4"),
|
||||
BoardColumn::Waiting,
|
||||
Some(pull_request(0, 145, 23, 0, vec![])),
|
||||
Some(sandbox("sb-u1v2w3x4", 4, 8)),
|
||||
Some(RunQuestion {
|
||||
text: "Proceed from investigation to fix?".into(),
|
||||
}),
|
||||
created_at: ts("2026-03-04T10:00:00Z"),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-6".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "web-dashboard".into(),
|
||||
},
|
||||
title: "Add dark mode toggle".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "implement".into(),
|
||||
},
|
||||
status: BoardColumn::Review,
|
||||
pull_request: Some(RunPullRequest {
|
||||
number: 889,
|
||||
additions: Some(234),
|
||||
deletions: Some(67),
|
||||
comments: Some(4),
|
||||
checks: vec![
|
||||
CheckRun {
|
||||
name: "lint".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(23.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "typecheck".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(72.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "unit-tests".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(154.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "integration-tests".into(),
|
||||
status: CheckRunStatus::Failure,
|
||||
duration_secs: Some(296.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "e2e / chrome".into(),
|
||||
status: CheckRunStatus::Failure,
|
||||
duration_secs: Some(182.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "build".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(105.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "coverage".into(),
|
||||
status: CheckRunStatus::Skipped,
|
||||
duration_secs: None,
|
||||
},
|
||||
],
|
||||
}),
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 2100.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-m3n4o5p6".into(),
|
||||
resources: None,
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-03-03T16:45:00Z"),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-7".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "infrastructure".into(),
|
||||
},
|
||||
title: "Terraform module for Redis cluster".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "implement".into(),
|
||||
},
|
||||
status: BoardColumn::Review,
|
||||
pull_request: Some(RunPullRequest {
|
||||
number: 156,
|
||||
additions: Some(412),
|
||||
deletions: Some(0),
|
||||
comments: Some(1),
|
||||
checks: vec![
|
||||
CheckRun {
|
||||
name: "lint".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(18.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "typecheck".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(56.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "unit-tests".into(),
|
||||
status: CheckRunStatus::Pending,
|
||||
duration_secs: None,
|
||||
},
|
||||
CheckRun {
|
||||
name: "integration-tests".into(),
|
||||
status: CheckRunStatus::Queued,
|
||||
duration_secs: None,
|
||||
},
|
||||
CheckRun {
|
||||
name: "build".into(),
|
||||
status: CheckRunStatus::Pending,
|
||||
duration_secs: None,
|
||||
},
|
||||
],
|
||||
}),
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 720.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-y5z6a7b8".into(),
|
||||
resources: None,
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-03-03T11:00:00Z"),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-8".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "api-server".into(),
|
||||
},
|
||||
title: "Implement webhook retry logic".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "implement".into(),
|
||||
},
|
||||
status: BoardColumn::Merge,
|
||||
pull_request: Some(RunPullRequest {
|
||||
number: 1249,
|
||||
additions: Some(189),
|
||||
deletions: Some(45),
|
||||
comments: Some(7),
|
||||
checks: vec![
|
||||
CheckRun {
|
||||
name: "lint".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(21.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "typecheck".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(68.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "unit-tests".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(192.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "integration-tests".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(334.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "e2e / chrome".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(262.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "e2e / firefox".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(285.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "build".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(121.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "deploy-preview".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(93.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "security-scan".into(),
|
||||
status: CheckRunStatus::Skipped,
|
||||
duration_secs: None,
|
||||
},
|
||||
CheckRun {
|
||||
name: "performance".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(138.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "bundle-size".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(34.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "accessibility".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(72.0),
|
||||
},
|
||||
],
|
||||
}),
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 259200.0,
|
||||
elapsed_warning: Some(true),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-c9d0e1f2".into(),
|
||||
resources: None,
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-02-28T14:00:00Z"),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-9".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "cli-tools".into(),
|
||||
},
|
||||
title: "Add --verbose flag to run command".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "expand".into(),
|
||||
},
|
||||
status: BoardColumn::Merge,
|
||||
pull_request: Some(RunPullRequest {
|
||||
number: 430,
|
||||
additions: Some(56),
|
||||
deletions: Some(12),
|
||||
comments: Some(2),
|
||||
checks: vec![
|
||||
CheckRun {
|
||||
name: "lint".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(15.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "typecheck".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(48.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "unit-tests".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(116.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "build".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(82.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "coverage".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(124.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "bundle-size".into(),
|
||||
status: CheckRunStatus::Skipped,
|
||||
duration_secs: None,
|
||||
},
|
||||
],
|
||||
}),
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 3900.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-g3h4i5j6".into(),
|
||||
resources: None,
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-02-27T09:00:00Z"),
|
||||
},
|
||||
RunListItem {
|
||||
id: "run-10".into(),
|
||||
repository: RepositoryReference {
|
||||
name: "shared-types".into(),
|
||||
},
|
||||
title: "Export utility type helpers".into(),
|
||||
workflow: WorkflowReference {
|
||||
slug: "sync_drift".into(),
|
||||
},
|
||||
status: BoardColumn::Merge,
|
||||
pull_request: Some(RunPullRequest {
|
||||
number: 76,
|
||||
additions: Some(34),
|
||||
deletions: Some(8),
|
||||
comments: Some(0),
|
||||
checks: vec![
|
||||
CheckRun {
|
||||
name: "lint".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(12.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "typecheck".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(34.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "unit-tests".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(75.0),
|
||||
},
|
||||
CheckRun {
|
||||
name: "build".into(),
|
||||
status: CheckRunStatus::Success,
|
||||
duration_secs: Some(58.0),
|
||||
},
|
||||
],
|
||||
}),
|
||||
timings: Some(RunTimings {
|
||||
elapsed_secs: 2880.0,
|
||||
elapsed_warning: Some(false),
|
||||
}),
|
||||
sandbox: Some(RunSandbox {
|
||||
id: "sb-k7l8m9n0".into(),
|
||||
resources: None,
|
||||
}),
|
||||
question: None,
|
||||
created_at: ts("2026-02-26T08:00:00Z"),
|
||||
},
|
||||
),
|
||||
board_item(
|
||||
take_summary(&mut summaries, "run-5"),
|
||||
BoardColumn::Failed,
|
||||
Some(pull_request(889, 234, 67, 4, vec![
|
||||
check("lint", CheckRunStatus::Success, Some(23.0)),
|
||||
check("typecheck", CheckRunStatus::Success, Some(72.0)),
|
||||
check("unit-tests", CheckRunStatus::Success, Some(154.0)),
|
||||
check("integration-tests", CheckRunStatus::Failure, Some(296.0)),
|
||||
check("build", CheckRunStatus::Success, Some(105.0)),
|
||||
])),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
board_item(
|
||||
take_summary(&mut summaries, "run-6"),
|
||||
BoardColumn::Succeeded,
|
||||
Some(pull_request(1249, 189, 45, 7, vec![
|
||||
check("lint", CheckRunStatus::Success, Some(21.0)),
|
||||
check("typecheck", CheckRunStatus::Success, Some(68.0)),
|
||||
check("unit-tests", CheckRunStatus::Success, Some(192.0)),
|
||||
check("integration-tests", CheckRunStatus::Success, Some(334.0)),
|
||||
check("deploy-preview", CheckRunStatus::Success, Some(93.0)),
|
||||
])),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ use fabro_types::{
|
|||
RunSubjectProvenance,
|
||||
};
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use fabro_util::text::strip_goal_decoration;
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
use fabro_vault::{Error as VaultError, SecretType, Vault};
|
||||
use fabro_workflow::Error as WorkflowError;
|
||||
|
|
@ -2578,6 +2579,65 @@ fn board_columns() -> serde_json::Value {
|
|||
])
|
||||
}
|
||||
|
||||
fn truncate_goal(goal: &str) -> String {
|
||||
const MAX_LEN: usize = 100;
|
||||
|
||||
let stripped = strip_goal_decoration(goal);
|
||||
let char_count = stripped.chars().count();
|
||||
if char_count <= MAX_LEN {
|
||||
return stripped.to_string();
|
||||
}
|
||||
|
||||
let truncated: String = stripped.chars().take(MAX_LEN - 3).collect();
|
||||
format!("{truncated}...")
|
||||
}
|
||||
|
||||
fn repository_name(host_repo_path: Option<&str>) -> String {
|
||||
host_repo_path
|
||||
.and_then(|path| path.rsplit(['/', '\\']).find(|segment| !segment.is_empty()))
|
||||
.unwrap_or("unknown")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn elapsed_secs(duration_ms: Option<u64>) -> Option<f64> {
|
||||
duration_ms.map(|ms| ms as f64 / 1000.0)
|
||||
}
|
||||
|
||||
fn summary_to_api_run_summary(summary: fabro_store::RunSummary) -> serde_json::Value {
|
||||
let goal = summary.goal.unwrap_or_default();
|
||||
let title = truncate_goal(&goal);
|
||||
let repository = repository_name(summary.host_repo_path.as_deref());
|
||||
let created_at = summary.run_id.created_at().to_rfc3339();
|
||||
|
||||
serde_json::json!({
|
||||
"run_id": summary.run_id.to_string(),
|
||||
"workflow_name": summary.workflow_name,
|
||||
"workflow_slug": summary.workflow_slug,
|
||||
"goal": goal,
|
||||
"title": title,
|
||||
"labels": summary.labels,
|
||||
"host_repo_path": summary.host_repo_path,
|
||||
"repository": { "name": repository },
|
||||
"start_time": summary.start_time.map(|time| time.to_rfc3339()),
|
||||
"status": summary.status,
|
||||
"status_reason": summary.status_reason.map(api_status_reason),
|
||||
"pending_control": summary.pending_control.map(api_pending_control),
|
||||
"duration_ms": summary.duration_ms,
|
||||
"elapsed_secs": elapsed_secs(summary.duration_ms),
|
||||
"total_usd_micros": summary.total_usd_micros,
|
||||
"created_at": created_at,
|
||||
})
|
||||
}
|
||||
|
||||
fn paginate_items<T>(items: Vec<T>, pagination: PaginationParams) -> (Vec<T>, bool) {
|
||||
let limit = pagination.limit.clamp(1, 100) as usize;
|
||||
let offset = pagination.offset as usize;
|
||||
let mut data: Vec<_> = items.into_iter().skip(offset).take(limit + 1).collect();
|
||||
let has_more = data.len() > limit;
|
||||
data.truncate(limit);
|
||||
(data, has_more)
|
||||
}
|
||||
|
||||
async fn list_board_runs(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -2599,32 +2659,12 @@ async fn list_board_runs(
|
|||
.filter_map(|summary| {
|
||||
let status = summary.status?;
|
||||
let column = board_column(status)?;
|
||||
let title = summary.goal.as_deref().unwrap_or("Untitled run");
|
||||
let workflow_slug = summary.workflow_slug.as_deref().unwrap_or("unknown");
|
||||
let workflow_name = summary.workflow_name.as_deref().unwrap_or(workflow_slug);
|
||||
let repo_name = summary
|
||||
.host_repo_path
|
||||
.as_deref()
|
||||
.and_then(|p| p.rsplit('/').next())
|
||||
.unwrap_or("unknown");
|
||||
let elapsed_secs = summary.duration_ms.map(|ms| ms as f64 / 1000.0);
|
||||
let created_at = summary.run_id.created_at();
|
||||
Some(serde_json::json!({
|
||||
"id": summary.run_id.to_string(),
|
||||
"title": title,
|
||||
"repository": { "name": repo_name },
|
||||
"workflow": { "slug": workflow_slug, "name": workflow_name },
|
||||
"status": column,
|
||||
"created_at": created_at.to_rfc3339(),
|
||||
"timings": elapsed_secs.map(|s| serde_json::json!({ "elapsed_secs": s })),
|
||||
}))
|
||||
let mut item = summary_to_api_run_summary(summary);
|
||||
item["column"] = serde_json::json!(column);
|
||||
Some(item)
|
||||
})
|
||||
.collect();
|
||||
let limit = pagination.limit.clamp(1, 100) as usize;
|
||||
let offset = pagination.offset as usize;
|
||||
let page: Vec<_> = all_items.into_iter().skip(offset).take(limit + 1).collect();
|
||||
let has_more = page.len() > limit;
|
||||
let data: Vec<_> = page.into_iter().take(limit).collect();
|
||||
let (data, has_more) = paginate_items(all_items, pagination);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
|
|
@ -2636,13 +2676,31 @@ async fn list_board_runs(
|
|||
.into_response()
|
||||
}
|
||||
|
||||
async fn list_runs(_auth: AuthenticatedService, State(state): State<Arc<AppState>>) -> Response {
|
||||
async fn list_runs(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> Response {
|
||||
match state
|
||||
.store
|
||||
.list_runs(&fabro_store::ListRunsQuery::default())
|
||||
.await
|
||||
{
|
||||
Ok(runs) => (StatusCode::OK, Json(runs)).into_response(),
|
||||
Ok(runs) => {
|
||||
let items = runs
|
||||
.into_iter()
|
||||
.map(summary_to_api_run_summary)
|
||||
.collect::<Vec<_>>();
|
||||
let (data, has_more) = paginate_items(items, pagination);
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"data": data,
|
||||
"meta": { "has_more": has_more }
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
}
|
||||
|
|
@ -8288,7 +8346,8 @@ slug = "fabro"
|
|||
let response = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = body_json(response.into_body()).await;
|
||||
assert_eq!(body.as_array().unwrap().len(), 0);
|
||||
assert_eq!(body["data"].as_array().unwrap().len(), 0);
|
||||
assert_eq!(body["meta"]["has_more"].as_bool(), Some(false));
|
||||
|
||||
// Start a run
|
||||
let req = Request::builder()
|
||||
|
|
@ -8312,10 +8371,18 @@ slug = "fabro"
|
|||
let response = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = body_json(response.into_body()).await;
|
||||
let items = body.as_array().unwrap();
|
||||
let items = body["data"].as_array().unwrap();
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0]["run_id"].as_str().unwrap(), run_id.to_string());
|
||||
assert!(items[0]["goal"].is_string());
|
||||
assert!(items[0]["title"].is_string());
|
||||
assert!(items[0]["repository"]["name"].is_string());
|
||||
assert!(items[0]["created_at"].is_string());
|
||||
assert!(items[0]["status"].as_str().is_some());
|
||||
assert!(items[0]["labels"].is_object());
|
||||
assert!(items[0]["status_reason"].is_null());
|
||||
assert!(items[0]["pending_control"].is_null());
|
||||
assert!(items[0]["total_usd_micros"].is_null());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -8567,7 +8634,7 @@ level = "debug"
|
|||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|item| item["id"].as_str() == Some(run_id_str.as_str()));
|
||||
.find(|item| item["run_id"].as_str() == Some(run_id_str.as_str()));
|
||||
assert!(
|
||||
board_item.is_some(),
|
||||
"cancelled run should appear on the board"
|
||||
|
|
@ -8575,8 +8642,9 @@ level = "debug"
|
|||
assert_eq!(
|
||||
board_item.unwrap()["status"].as_str(),
|
||||
Some("failed"),
|
||||
"cancelled run should be in the failed column"
|
||||
"cancelled run should preserve the failed lifecycle status"
|
||||
);
|
||||
assert_eq!(board_item.unwrap()["column"].as_str(), Some("failed"));
|
||||
|
||||
let run_store = state.store.open_run_reader(&run_id).await.unwrap();
|
||||
let status = run_store.state().await.unwrap().status.unwrap();
|
||||
|
|
@ -8692,9 +8760,11 @@ level = "debug"
|
|||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|item| item["id"].as_str() == Some(run_id_str.as_str()))
|
||||
.find(|item| item["run_id"].as_str() == Some(run_id_str.as_str()))
|
||||
.expect("board item should exist");
|
||||
assert_eq!(item["status"].as_str(), Some("initializing"));
|
||||
assert!(item["status"].as_str().is_some());
|
||||
assert_eq!(item["column"].as_str(), Some("initializing"));
|
||||
assert_eq!(item["pending_control"].as_str(), Some("pause"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -9081,11 +9151,14 @@ timeout = "30s"
|
|||
let data = body["data"].as_array().expect("data should be array");
|
||||
assert!(!data.is_empty(), "demo should return runs");
|
||||
let first = &data[0];
|
||||
assert!(first["id"].is_string());
|
||||
assert!(first["run_id"].is_string());
|
||||
assert!(first["goal"].is_string());
|
||||
assert!(first["repository"].is_object());
|
||||
assert!(first["title"].is_string());
|
||||
assert!(first["workflow"].is_object());
|
||||
assert!(first["status"].is_string());
|
||||
assert!(first["column"].is_string());
|
||||
assert!(first["workflow_slug"].is_string() || first["workflow_slug"].is_null());
|
||||
assert!(first["labels"].is_object());
|
||||
assert!(first["created_at"].is_string());
|
||||
}
|
||||
|
||||
|
|
@ -9155,19 +9228,21 @@ timeout = "30s"
|
|||
let data = body["data"].as_array().expect("data should be array");
|
||||
let item = data
|
||||
.iter()
|
||||
.find(|i| i["id"].as_str() == Some(&run_id))
|
||||
.find(|i| i["run_id"].as_str() == Some(&run_id))
|
||||
.expect("run should be in board");
|
||||
// Should have RunListItem fields
|
||||
// Should have canonical run summary fields plus board-specific column
|
||||
assert!(item["goal"].is_string());
|
||||
assert!(item["title"].is_string());
|
||||
assert!(item["repository"].is_object());
|
||||
assert!(item["workflow"].is_object());
|
||||
// Status should be a board column, not a lifecycle status
|
||||
let status = item["status"].as_str().unwrap();
|
||||
assert!(
|
||||
["working", "initializing", "review", "merge"].contains(&status),
|
||||
"status should be a board column, got: {status}"
|
||||
);
|
||||
assert!(item["workflow_slug"].is_string() || item["workflow_slug"].is_null());
|
||||
assert!(item["workflow_name"].is_string() || item["workflow_name"].is_null());
|
||||
assert!(item["labels"].is_object());
|
||||
assert!(item["status"].is_string());
|
||||
assert!(item["column"].is_string());
|
||||
assert!(item["created_at"].is_string());
|
||||
assert!(item["pending_control"].is_null());
|
||||
assert!(item["status_reason"].is_null());
|
||||
assert!(item["total_usd_micros"].is_null());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -9199,7 +9274,7 @@ timeout = "30s"
|
|||
let data = body["data"].as_array().expect("data should be array");
|
||||
let found = data
|
||||
.iter()
|
||||
.any(|i| i["id"].as_str() == Some(&run_id.to_string()));
|
||||
.any(|i| i["run_id"].as_str() == Some(&run_id.to_string()));
|
||||
assert!(!found, "removing run should not appear on the board");
|
||||
}
|
||||
|
||||
|
|
@ -9252,15 +9327,17 @@ timeout = "30s"
|
|||
|
||||
let paused_item = data
|
||||
.iter()
|
||||
.find(|i| i["id"].as_str() == Some(&paused_id.to_string()))
|
||||
.find(|i| i["run_id"].as_str() == Some(&paused_id.to_string()))
|
||||
.expect("paused run should be on board");
|
||||
assert_eq!(paused_item["status"].as_str().unwrap(), "waiting");
|
||||
assert_eq!(paused_item["status"].as_str().unwrap(), "paused");
|
||||
assert_eq!(paused_item["column"].as_str().unwrap(), "waiting");
|
||||
|
||||
let succeeded_item = data
|
||||
.iter()
|
||||
.find(|i| i["id"].as_str() == Some(&succeeded_id.to_string()))
|
||||
.find(|i| i["run_id"].as_str() == Some(&succeeded_id.to_string()))
|
||||
.expect("succeeded run should be on board");
|
||||
assert_eq!(succeeded_item["status"].as_str().unwrap(), "succeeded");
|
||||
assert_eq!(succeeded_item["column"].as_str().unwrap(), "succeeded");
|
||||
|
||||
// Verify columns are included in the response
|
||||
let columns = body["columns"].as_array().expect("columns should be array");
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ models/assistant-stage-turn.ts
|
|||
models/billed-token-counts.ts
|
||||
models/billing-by-model.ts
|
||||
models/billing-stage-ref.ts
|
||||
models/board-column-definition.ts
|
||||
models/board-column.ts
|
||||
models/check-run-status.ts
|
||||
models/check-run.ts
|
||||
|
|
@ -86,6 +87,7 @@ models/model.ts
|
|||
models/node-state.ts
|
||||
models/node-status-record.ts
|
||||
models/paginated-api-question-list.ts
|
||||
models/paginated-board-run-list.ts
|
||||
models/paginated-event-list.ts
|
||||
models/paginated-history-entry-list.ts
|
||||
models/paginated-model-list.ts
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
|
|||
// @ts-ignore
|
||||
import type { ErrorResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PaginatedBoardRunList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PaginatedRunList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PreflightResponse } from '../models';
|
||||
|
|
@ -212,10 +214,12 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
/**
|
||||
* Returns durable run summaries from the backing store, including runs persisted before the current server boot.
|
||||
* @summary List Runs
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRuns: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
listRuns: async (pageLimit?: number, pageOffset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/v1/runs`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
|
|
@ -234,6 +238,14 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (pageLimit !== undefined) {
|
||||
localVarQueryParameter['page[limit]'] = pageLimit;
|
||||
}
|
||||
|
||||
if (pageOffset !== undefined) {
|
||||
localVarQueryParameter['page[offset]'] = pageOffset;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
|
|
@ -586,7 +598,7 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listBoardRuns(pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunList>> {
|
||||
async listBoardRuns(pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedBoardRunList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listBoardRuns(pageLimit, pageOffset, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.listBoardRuns']?.[localVarOperationServerIndex]?.url;
|
||||
|
|
@ -595,11 +607,13 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
/**
|
||||
* Returns durable run summaries from the backing store, including runs persisted before the current server boot.
|
||||
* @summary List Runs
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listRuns(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<StoreRunSummary>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRuns(options);
|
||||
async listRuns(pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRuns(pageLimit, pageOffset, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.listRuns']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
|
|
@ -743,17 +757,19 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listBoardRuns(pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunList> {
|
||||
listBoardRuns(pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedBoardRunList> {
|
||||
return localVarFp.listBoardRuns(pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns durable run summaries from the backing store, including runs persisted before the current server boot.
|
||||
* @summary List Runs
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRuns(options?: RawAxiosRequestConfig): AxiosPromise<Array<StoreRunSummary>> {
|
||||
return localVarFp.listRuns(options).then((request) => request(axios, basePath));
|
||||
listRuns(pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunList> {
|
||||
return localVarFp.listRuns(pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Pauses a running run. Returns 409 if the run is not running.
|
||||
|
|
@ -881,11 +897,13 @@ export class RunsApi extends BaseAPI {
|
|||
/**
|
||||
* Returns durable run summaries from the backing store, including runs persisted before the current server boot.
|
||||
* @summary List Runs
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public listRuns(options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).listRuns(options).then((request) => request(this.axios, this.basePath));
|
||||
public listRuns(pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).listRuns(pageLimit, pageOffset, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface BoardColumnDefinition {
|
||||
'id': string;
|
||||
'name': string;
|
||||
}
|
||||
|
||||
|
|
@ -19,10 +19,11 @@
|
|||
*/
|
||||
|
||||
export const BoardColumn = {
|
||||
WORKING: 'working',
|
||||
INITIALIZING: 'initializing',
|
||||
REVIEW: 'review',
|
||||
MERGE: 'merge'
|
||||
RUNNING: 'running',
|
||||
WAITING: 'waiting',
|
||||
SUCCEEDED: 'succeeded',
|
||||
FAILED: 'failed'
|
||||
} as const;
|
||||
|
||||
export type BoardColumn = typeof BoardColumn[keyof typeof BoardColumn];
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export * from './billed-token-counts';
|
|||
export * from './billing-by-model';
|
||||
export * from './billing-stage-ref';
|
||||
export * from './board-column';
|
||||
export * from './board-column-definition';
|
||||
export * from './check-run';
|
||||
export * from './check-run-status';
|
||||
export * from './code-location';
|
||||
|
|
@ -67,6 +68,7 @@ export * from './model-test-result';
|
|||
export * from './node-state';
|
||||
export * from './node-status-record';
|
||||
export * from './paginated-api-question-list';
|
||||
export * from './paginated-board-run-list';
|
||||
export * from './paginated-event-list';
|
||||
export * from './paginated-history-entry-list';
|
||||
export * from './paginated-model-list';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { BoardColumnDefinition } from './board-column-definition';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { PaginationMeta } from './pagination-meta';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunListItem } from './run-list-item';
|
||||
|
||||
/**
|
||||
* Paginated list of board runs with shared canonical fields plus board metadata.
|
||||
*/
|
||||
export interface PaginatedBoardRunList {
|
||||
'columns': Array<BoardColumnDefinition>;
|
||||
'data': Array<RunListItem>;
|
||||
'meta': PaginationMeta;
|
||||
}
|
||||
|
||||
|
|
@ -18,13 +18,13 @@
|
|||
import type { PaginationMeta } from './pagination-meta';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunListItem } from './run-list-item';
|
||||
import type { StoreRunSummary } from './store-run-summary';
|
||||
|
||||
/**
|
||||
* Paginated list of runs.
|
||||
*/
|
||||
export interface PaginatedRunList {
|
||||
'data': Array<RunListItem>;
|
||||
'data': Array<StoreRunSummary>;
|
||||
'meta': PaginationMeta;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ import type { BoardColumn } from './board-column';
|
|||
import type { RepositoryReference } from './repository-reference';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunControlAction } from './run-control-action';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunPullRequest } from './run-pull-request';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
|
|
@ -30,28 +33,35 @@ import type { RunQuestion } from './run-question';
|
|||
import type { RunSandbox } from './run-sandbox';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunTimings } from './run-timings';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { WorkflowReference } from './workflow-reference';
|
||||
import type { StatusReason } from './status-reason';
|
||||
|
||||
/**
|
||||
* Summary of a run shown in the board view.
|
||||
* Canonical run summary shown in the board view, extended with board-specific metadata.
|
||||
*/
|
||||
export interface RunListItem {
|
||||
/**
|
||||
* Unique run identifier (ULID).
|
||||
*/
|
||||
'id': string;
|
||||
'run_id': string;
|
||||
'workflow_name'?: string | null;
|
||||
'workflow_slug'?: string | null;
|
||||
'goal': string;
|
||||
'repository': RepositoryReference;
|
||||
/**
|
||||
* Human-readable title describing the run\'s goal.
|
||||
*/
|
||||
'title': string;
|
||||
'workflow': WorkflowReference;
|
||||
'status': BoardColumn;
|
||||
'status': string;
|
||||
'labels': { [key: string]: string; };
|
||||
'host_repo_path'?: string | null;
|
||||
'start_time'?: string | null;
|
||||
'status_reason'?: StatusReason | null;
|
||||
'pending_control'?: RunControlAction | null;
|
||||
'duration_ms'?: number | null;
|
||||
'elapsed_secs'?: number | null;
|
||||
'total_usd_micros'?: number | null;
|
||||
'column': BoardColumn;
|
||||
'pull_request'?: RunPullRequest;
|
||||
'timings'?: RunTimings;
|
||||
'sandbox'?: RunSandbox;
|
||||
'question'?: RunQuestion;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RepositoryReference } from './repository-reference';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunControlAction } from './run-control-action';
|
||||
|
|
@ -24,14 +27,18 @@ export interface StoreRunSummary {
|
|||
'run_id': string;
|
||||
'workflow_name'?: string | null;
|
||||
'workflow_slug'?: string | null;
|
||||
'goal'?: string | null;
|
||||
'goal': string;
|
||||
'title': string;
|
||||
'labels': { [key: string]: string; };
|
||||
'host_repo_path'?: string | null;
|
||||
'repository': RepositoryReference;
|
||||
'start_time'?: string | null;
|
||||
'created_at': string;
|
||||
'status'?: string | null;
|
||||
'status_reason'?: string | null;
|
||||
'pending_control'?: RunControlAction | null;
|
||||
'duration_ms'?: number | null;
|
||||
'elapsed_secs'?: number | null;
|
||||
'total_usd_micros'?: number | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue