mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
refactor: simplify reviewed run cleanup
Reuse shared PR chip rendering, persist runtime stage handlers from events, and remove duplicated lifecycle/delete helpers found during review.
This commit is contained in:
parent
1a89974f65
commit
6719a3cdac
13 changed files with 134 additions and 154 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, type ReactNode } from "react";
|
||||
import { Fragment, useMemo, type ReactNode } from "react";
|
||||
import { Lexer, type Token } from "marked";
|
||||
|
||||
const CODE_CLASSNAME =
|
||||
|
|
@ -46,6 +46,6 @@ export function InlineMarkdown({
|
|||
content: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const tokens = Lexer.lexInline(content);
|
||||
return <span className={className}>{renderTokens(tokens)}</span>;
|
||||
const children = useMemo(() => renderTokens(Lexer.lexInline(content)), [content]);
|
||||
return <span className={className}>{children}</span>;
|
||||
}
|
||||
|
|
|
|||
40
apps/fabro-web/app/components/pull-request-chip.tsx
Normal file
40
apps/fabro-web/app/components/pull-request-chip.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { ReactNode } from "react";
|
||||
|
||||
import { GitPullRequestIcon } from "./icons";
|
||||
|
||||
export function PullRequestChip({
|
||||
number,
|
||||
url,
|
||||
className = "inline-flex items-center gap-1.5 font-mono text-xs text-fg-muted",
|
||||
iconClassName = "size-3",
|
||||
children,
|
||||
}: {
|
||||
number: number;
|
||||
url?: string;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const content = (
|
||||
<>
|
||||
<GitPullRequestIcon className={iconClassName} />
|
||||
#{number}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
if (url == null) {
|
||||
return <span className={className}>{content}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={`${className} hover:text-fg`}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
|||
|
||||
import { InlineMarkdown } from "../components/inline-markdown";
|
||||
import { InterviewDock } from "../components/interview-dock";
|
||||
import { GitPullRequestIcon } from "../components/icons";
|
||||
import { PullRequestChip } from "../components/pull-request-chip";
|
||||
import { SteerBar, type SteerBarHandle } from "../components/steer-bar";
|
||||
import { ErrorState } from "../components/state";
|
||||
import { useToast } from "../components/toast";
|
||||
|
|
@ -328,15 +328,11 @@ export default function RunDetail({ params }: { params: { id: string } }) {
|
|||
</Tooltip>
|
||||
)}
|
||||
{run.number != null && run.pullRequestUrl && (
|
||||
<a
|
||||
href={run.pullRequestUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-1.5 font-mono text-xs text-fg-muted hover:text-fg"
|
||||
>
|
||||
<GitPullRequestIcon className="size-3.5" />
|
||||
#{run.number}
|
||||
</a>
|
||||
<PullRequestChip
|
||||
number={run.number}
|
||||
url={run.pullRequestUrl}
|
||||
iconClassName="size-3.5"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ 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 { InlineMarkdown } from "../components/inline-markdown";
|
||||
import { PullRequestChip } from "../components/pull-request-chip";
|
||||
import { useToast } from "../components/toast";
|
||||
import { shouldRefreshBoardForEvent, useBoardEvents } from "../lib/board-events";
|
||||
import { useAuthConfig, useBoardsRuns, useSystemInfo } from "../lib/queries";
|
||||
|
|
@ -276,38 +276,29 @@ function PrCard({
|
|||
const lifecycleLabel = boardLifecycleStatusLabel(pr);
|
||||
|
||||
return (
|
||||
<Link to={`/runs/${pr.id}`} className="group block rounded-md border border-line bg-panel p-4 transition-all duration-200 hover:border-line-strong hover:shadow-lg hover:shadow-black/20">
|
||||
<div className="group rounded-md border border-line bg-panel p-4 transition-all duration-200 hover:border-line-strong hover:shadow-lg hover:shadow-black/20">
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
<span className="font-mono text-xs font-medium text-teal-500">
|
||||
<Link to={`/runs/${pr.id}`} className="font-mono text-xs font-medium text-teal-500">
|
||||
{pr.repo}
|
||||
</span>
|
||||
</Link>
|
||||
{lifecycleLabel != null && (
|
||||
<span className="rounded-full border border-line px-1.5 py-0.5 font-mono text-[11px] uppercase tracking-wide text-fg-muted">
|
||||
{lifecycleLabel}
|
||||
</span>
|
||||
)}
|
||||
{pr.number != null && (
|
||||
pr.pullRequestUrl ? (
|
||||
<a
|
||||
href={pr.pullRequestUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className={`ml-auto inline-flex items-center gap-1 font-mono text-xs ${iconColor} hover:text-fg`}
|
||||
>
|
||||
<GitPullRequestIcon className="size-3.5 shrink-0" />
|
||||
#{pr.number}
|
||||
</a>
|
||||
) : (
|
||||
<span className={`ml-auto inline-flex items-center gap-1 font-mono text-xs ${iconColor}`}>
|
||||
<GitPullRequestIcon className="size-3.5 shrink-0" />
|
||||
#{pr.number}
|
||||
</span>
|
||||
)
|
||||
<PullRequestChip
|
||||
number={pr.number}
|
||||
url={pr.pullRequestUrl}
|
||||
className={`ml-auto inline-flex items-center gap-1 font-mono text-xs ${iconColor}`}
|
||||
iconClassName="size-3.5 shrink-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm leading-snug text-fg-2">{pr.title}</p>
|
||||
<Link to={`/runs/${pr.id}`} className="block">
|
||||
<p className="text-sm leading-snug text-fg-2">{pr.title}</p>
|
||||
</Link>
|
||||
|
||||
{(pr.resources != null || pr.comments != null || pr.elapsed != null) && (
|
||||
<div className="mt-3 flex items-center gap-3 font-mono text-xs">
|
||||
|
|
@ -387,7 +378,7 @@ function PrCard({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -594,7 +585,8 @@ function RunRow({ run }: { run: RunWithStatus }) {
|
|||
const statusDisplay = columnStatusDisplay[run.status];
|
||||
|
||||
return (
|
||||
<Link to={`/runs/${run.id}`} className="grid items-center rounded-md border border-line bg-panel/80 px-4 py-3 transition-all duration-200 hover:border-line-strong hover:bg-panel" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
|
||||
<div className="grid items-center rounded-md border border-line bg-panel/80 px-4 py-3 transition-all duration-200 hover:border-line-strong hover:bg-panel" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
|
||||
<Link to={`/runs/${run.id}`} className="contents">
|
||||
<span className="flex items-center gap-2 pr-2">
|
||||
<span className={`size-1.5 shrink-0 rounded-full ${statusDisplay.dot}`} aria-hidden="true" />
|
||||
<span className={`font-mono text-xs ${statusDisplay.text}`}>{run.statusLabel}</span>
|
||||
|
|
@ -636,31 +628,16 @@ function RunRow({ run }: { run: RunWithStatus }) {
|
|||
{run.additions != null && <span className="text-mint">+{run.additions.toLocaleString()}</span>}
|
||||
{run.deletions != null && <span className="text-coral">-{run.deletions.toLocaleString()}</span>}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-fg-muted">
|
||||
{run.number != null && (
|
||||
run.pullRequestUrl ? (
|
||||
<a
|
||||
href={run.pullRequestUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="inline-flex items-center gap-1.5 hover:text-fg"
|
||||
>
|
||||
<GitPullRequestIcon className="size-3" />
|
||||
#{run.number}
|
||||
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<GitPullRequestIcon className="size-3" />
|
||||
#{run.number}
|
||||
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
|
||||
</>
|
||||
)
|
||||
<PullRequestChip number={run.number} url={run.pullRequestUrl}>
|
||||
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
|
||||
</PullRequestChip>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from "react";
|
||||
import { ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { Link, useParams } from "react-router";
|
||||
import { GitPullRequestIcon } from "../components/icons";
|
||||
import { InlineMarkdown } from "../components/inline-markdown";
|
||||
import { PullRequestChip } from "../components/pull-request-chip";
|
||||
import { ciConfig, columnForStatus, columnStatusDisplay, deriveCiStatus, mapRunSummaryToRunItem } from "../data/runs";
|
||||
import type { ColumnStatus, RunWithStatus } from "../data/runs";
|
||||
import { useWorkflowRuns } from "../lib/queries";
|
||||
|
|
@ -26,7 +26,8 @@ function mapWorkflowRuns(result: PaginatedRunList | null | undefined): RunWithSt
|
|||
function RunRow({ run }: { run: RunWithStatus }) {
|
||||
const colors = columnStatusDisplay[run.status];
|
||||
return (
|
||||
<Link to={`/runs/${run.id}`} className="grid items-center rounded-md border border-line bg-panel/80 px-4 py-3 transition-all duration-200 hover:border-line-strong hover:bg-panel" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
|
||||
<div className="grid items-center rounded-md border border-line bg-panel/80 px-4 py-3 transition-all duration-200 hover:border-line-strong hover:bg-panel" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
|
||||
<Link to={`/runs/${run.id}`} className="contents">
|
||||
<span className="flex items-center gap-2 pr-2">
|
||||
<span className={`size-2 shrink-0 rounded-full ${colors.dot}`} />
|
||||
<span className={`text-xs font-medium ${colors.text}`}>{run.statusLabel}</span>
|
||||
|
|
@ -52,31 +53,16 @@ function RunRow({ run }: { run: RunWithStatus }) {
|
|||
{run.additions != null && <span className="text-mint">+{run.additions.toLocaleString()}</span>}
|
||||
{run.deletions != null && <span className="text-coral">-{run.deletions.toLocaleString()}</span>}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-fg-muted">
|
||||
{run.number != null && (
|
||||
run.pullRequestUrl ? (
|
||||
<a
|
||||
href={run.pullRequestUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="inline-flex items-center gap-1.5 hover:text-fg"
|
||||
>
|
||||
<GitPullRequestIcon className="size-3" />
|
||||
#{run.number}
|
||||
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<GitPullRequestIcon className="size-3" />
|
||||
#{run.number}
|
||||
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
|
||||
</>
|
||||
)
|
||||
<PullRequestChip number={run.number} url={run.pullRequestUrl}>
|
||||
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
|
||||
</PullRequestChip>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -918,7 +918,7 @@ impl Sandbox for DaytonaSandbox {
|
|||
});
|
||||
return Err(err);
|
||||
}
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::StartCompleted {
|
||||
provider: "daytona".into(),
|
||||
duration_ms,
|
||||
|
|
@ -941,7 +941,7 @@ impl Sandbox for DaytonaSandbox {
|
|||
});
|
||||
return Err(err);
|
||||
}
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::StopCompleted {
|
||||
provider: "daytona".into(),
|
||||
duration_ms,
|
||||
|
|
@ -966,7 +966,7 @@ impl Sandbox for DaytonaSandbox {
|
|||
return Err(err);
|
||||
}
|
||||
}
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::DeleteCompleted {
|
||||
provider: "daytona".into(),
|
||||
duration_ms,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use bollard::image::CreateImageOptions;
|
|||
use bollard::models::HostConfig;
|
||||
use fabro_github::GitHubCredentials;
|
||||
use fabro_types::{CommandOutputStream, CommandTermination, RunId};
|
||||
use fabro_util::time::elapsed_ms;
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::{fs, time};
|
||||
|
|
@ -873,7 +874,7 @@ fn docker_not_found(error: &DockerError) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
fn docker_already_stopped(error: &DockerError) -> bool {
|
||||
fn docker_not_modified(error: &DockerError) -> bool {
|
||||
matches!(error, DockerError::DockerResponseServerError {
|
||||
status_code: 304,
|
||||
..
|
||||
|
|
@ -1148,7 +1149,7 @@ impl Sandbox for DockerSandbox {
|
|||
.start_container(&container_id, None::<StartContainerOptions<String>>)
|
||||
.await
|
||||
{
|
||||
if !docker_already_stopped(&e) {
|
||||
if !docker_not_modified(&e) {
|
||||
return self.start_error(crate::Error::context(
|
||||
format!(
|
||||
"Failed to start Docker container '{container_id}' with labels {labels:?}"
|
||||
|
|
@ -1170,7 +1171,7 @@ impl Sandbox for DockerSandbox {
|
|||
)));
|
||||
}
|
||||
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::StartCompleted {
|
||||
provider: "docker".into(),
|
||||
duration_ms,
|
||||
|
|
@ -1185,7 +1186,7 @@ impl Sandbox for DockerSandbox {
|
|||
let start = Instant::now();
|
||||
|
||||
let Some(container_id) = self.container_id.get().cloned() else {
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::StopCompleted {
|
||||
provider: "docker".into(),
|
||||
duration_ms,
|
||||
|
|
@ -1207,7 +1208,7 @@ impl Sandbox for DockerSandbox {
|
|||
.stop_container(&container_id, Some(stop_opts))
|
||||
.await
|
||||
{
|
||||
if !docker_not_found(&e) && !docker_already_stopped(&e) {
|
||||
if !docker_not_found(&e) && !docker_not_modified(&e) {
|
||||
return self.stop_error(crate::Error::context(
|
||||
format!(
|
||||
"Failed to stop Docker container '{container_id}' with labels {labels:?}"
|
||||
|
|
@ -1217,7 +1218,7 @@ impl Sandbox for DockerSandbox {
|
|||
}
|
||||
}
|
||||
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::StopCompleted {
|
||||
provider: "docker".into(),
|
||||
duration_ms,
|
||||
|
|
@ -1233,7 +1234,7 @@ impl Sandbox for DockerSandbox {
|
|||
let start = Instant::now();
|
||||
|
||||
let Some(container_id) = self.container_id.get().cloned() else {
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::DeleteCompleted {
|
||||
provider: "docker".into(),
|
||||
duration_ms,
|
||||
|
|
@ -1268,7 +1269,7 @@ impl Sandbox for DockerSandbox {
|
|||
}
|
||||
}
|
||||
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
self.emit(SandboxEvent::DeleteCompleted {
|
||||
provider: "docker".into(),
|
||||
duration_ms,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::time::Instant;
|
|||
use async_trait::async_trait;
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_types::{CommandOutputStream, CommandTermination};
|
||||
use fabro_util::time::elapsed_ms;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
|
@ -314,7 +315,7 @@ impl Sandbox for LocalSandbox {
|
|||
}
|
||||
};
|
||||
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
|
||||
let stdout_str = stdout_task.await.unwrap_or_default();
|
||||
let stderr_str = stderr_task.await.unwrap_or_default();
|
||||
|
|
@ -402,7 +403,7 @@ impl Sandbox for LocalSandbox {
|
|||
}
|
||||
};
|
||||
|
||||
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
let duration_ms = elapsed_ms(start);
|
||||
let stdout_bytes = stdout_task
|
||||
.await
|
||||
.map_err(|e| crate::Error::context("stdout stream task failed", e))??;
|
||||
|
|
|
|||
|
|
@ -26,17 +26,17 @@ pub use fabro_api::types::{
|
|||
ArtifactEntry, ArtifactListResponse, BillingByModel, BillingStageRef,
|
||||
CloseRunPullRequestResponse, CompletionContentPart, CompletionMessage, CompletionMessageRole,
|
||||
CompletionResponse, CompletionToolChoiceMode, CompletionUsage, CreateCompletionRequest,
|
||||
CreateRunPullRequestRequest, CreateSecretRequest, DeleteSecretRequest, DiskUsageResponse,
|
||||
DiskUsageRunRow, DiskUsageSummaryRow, ForkRequest, ForkResponse, MergeRunPullRequestRequest,
|
||||
MergeRunPullRequestResponse, ModelReference, PaginatedEventList, PaginatedRunList,
|
||||
PaginationMeta, PreflightResponse, PreviewUrlRequest, PreviewUrlResponse, PruneRunEntry,
|
||||
PruneRunsRequest, PruneRunsResponse, RenderWorkflowGraphDirection, RenderWorkflowGraphRequest,
|
||||
RewindRequest, RewindResponse, RunArtifactEntry, RunArtifactListResponse, RunBilling,
|
||||
RunBillingStage, RunBillingTotals, RunError, RunManifest, RunStage, RunStatusResponse,
|
||||
SandboxFileEntry, SandboxFileListResponse, SshAccessRequest, SshAccessResponse, StageHandler,
|
||||
StageState, StartRunRequest, SubmitAnswerRequest, SystemFeatures, SystemInfoResponse,
|
||||
SystemRepairRunIssue, SystemRepairRunsResponse, SystemRunCounts, TimelineEntryResponse,
|
||||
WriteBlobResponse,
|
||||
CreateRunPullRequestRequest, CreateSecretRequest, DeleteRunResponse, DeleteRunSandbox,
|
||||
DeleteSecretRequest, DiskUsageResponse, DiskUsageRunRow, DiskUsageSummaryRow, ForkRequest,
|
||||
ForkResponse, MergeRunPullRequestRequest, MergeRunPullRequestResponse, ModelReference,
|
||||
PaginatedEventList, PaginatedRunList, PaginationMeta, PreflightResponse, PreviewUrlRequest,
|
||||
PreviewUrlResponse, PruneRunEntry, PruneRunsRequest, PruneRunsResponse,
|
||||
RenderWorkflowGraphDirection, RenderWorkflowGraphRequest, RewindRequest, RewindResponse,
|
||||
RunArtifactEntry, RunArtifactListResponse, RunBilling, RunBillingStage, RunBillingTotals,
|
||||
RunError, RunManifest, RunStage, RunStatusResponse, SandboxFileEntry, SandboxFileListResponse,
|
||||
SshAccessRequest, SshAccessResponse, StageHandler, StageState, StartRunRequest,
|
||||
SubmitAnswerRequest, SystemFeatures, SystemInfoResponse, SystemRepairRunIssue,
|
||||
SystemRepairRunsResponse, SystemRunCounts, TimelineEntryResponse, WriteBlobResponse,
|
||||
};
|
||||
use fabro_auth::{
|
||||
CredentialSource, VaultCredentialSource, auth_issue_message, parse_credential_secret,
|
||||
|
|
@ -96,7 +96,6 @@ use fabro_workflow::run_lookup::{
|
|||
};
|
||||
use fabro_workflow::run_status::{FailureReason, RunStatus, SuccessReason};
|
||||
use fabro_workflow::{Error as WorkflowError, operations, pull_request};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::fs;
|
||||
|
|
@ -1591,19 +1590,6 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
|
||||
const MAX_PAGE_OFFSET: u32 = 1_000_000;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DeleteRunResponse {
|
||||
deleted: bool,
|
||||
sandbox_preserved: bool,
|
||||
sandbox: DeleteRunSandboxResponse,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DeleteRunSandboxResponse {
|
||||
provider: String,
|
||||
identifier: String,
|
||||
}
|
||||
|
||||
enum DeleteRunOutcome {
|
||||
NoContent,
|
||||
Preserved(DeleteRunResponse),
|
||||
|
|
@ -1723,7 +1709,7 @@ async fn delete_run_sandbox_resource(
|
|||
return Ok(DeleteRunOutcome::Preserved(DeleteRunResponse {
|
||||
deleted: true,
|
||||
sandbox_preserved: true,
|
||||
sandbox: DeleteRunSandboxResponse {
|
||||
sandbox: DeleteRunSandbox {
|
||||
provider: record.provider,
|
||||
identifier,
|
||||
},
|
||||
|
|
@ -1771,15 +1757,7 @@ async fn reject_active_delete_without_force(
|
|||
.ok()
|
||||
.and_then(|runs| runs.get(run_id).map(|managed_run| managed_run.status));
|
||||
if let Some(status) = managed_status {
|
||||
if matches!(
|
||||
status,
|
||||
RunStatus::Submitted
|
||||
| RunStatus::Queued
|
||||
| RunStatus::Starting
|
||||
| RunStatus::Running
|
||||
| RunStatus::Blocked { .. }
|
||||
| RunStatus::Paused { .. }
|
||||
) {
|
||||
if status.requires_force_to_delete() {
|
||||
return Err(ApiError::new(
|
||||
StatusCode::CONFLICT,
|
||||
active_run_delete_message(*run_id, status),
|
||||
|
|
@ -1790,23 +1768,11 @@ async fn reject_active_delete_without_force(
|
|||
}
|
||||
|
||||
match state.store.runs().find(run_id).await {
|
||||
Ok(Some(summary))
|
||||
if matches!(
|
||||
summary.status,
|
||||
RunStatus::Submitted
|
||||
| RunStatus::Queued
|
||||
| RunStatus::Starting
|
||||
| RunStatus::Running
|
||||
| RunStatus::Blocked { .. }
|
||||
| RunStatus::Paused { .. }
|
||||
) =>
|
||||
{
|
||||
Err(ApiError::new(
|
||||
StatusCode::CONFLICT,
|
||||
active_run_delete_message(*run_id, summary.status),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
Ok(Some(summary)) if summary.status.requires_force_to_delete() => Err(ApiError::new(
|
||||
StatusCode::CONFLICT,
|
||||
active_run_delete_message(*run_id, summary.status),
|
||||
)
|
||||
.into_response()),
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
Err(ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@ async fn list_run_stages(
|
|||
let stages = projection
|
||||
.iter_stages()
|
||||
.map(|(stage_id, stage)| {
|
||||
let handler = StageHandler::from_handler_type(
|
||||
graph
|
||||
.and_then(|g| g.nodes.get(stage_id.node_id()))
|
||||
.and_then(|n| n.handler_type()),
|
||||
);
|
||||
let handler = stage.handler.unwrap_or_else(|| {
|
||||
StageHandler::from_handler_type(
|
||||
graph
|
||||
.and_then(|g| g.nodes.get(stage_id.node_id()))
|
||||
.and_then(|n| n.handler_type()),
|
||||
)
|
||||
});
|
||||
run_stage_from_stage_id(
|
||||
stage_id,
|
||||
stage_id.node_id().to_string(),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use fabro_types::{
|
|||
BilledModelUsage, Checkpoint, CommandTermination, Conclusion, EventBody, FailureSignature,
|
||||
InterviewQuestionRecord, Outcome, PendingInterviewRecord, PullRequestRecord, RunControlAction,
|
||||
RunEvent, RunId, RunProjection, RunSpec, RunStatus, RunSummary, SandboxRecord, StageCompletion,
|
||||
StageId, StageOutcome, StageProjection, StageState, StartRecord, TerminalStatus,
|
||||
StageHandler, StageId, StageOutcome, StageProjection, StageState, StartRecord, TerminalStatus,
|
||||
first_event_seq,
|
||||
};
|
||||
use fabro_util::error::render_with_causes;
|
||||
|
|
@ -292,7 +292,7 @@ impl RunProjectionReducer for RunProjection {
|
|||
EventBody::InterviewInterrupted(props) if !props.question_id.is_empty() => {
|
||||
self.pending_interviews.remove(&props.question_id);
|
||||
}
|
||||
EventBody::StageStarted(_) => {
|
||||
EventBody::StageStarted(props) => {
|
||||
let Some(stage_id) = stored.stage_id.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
|
@ -301,7 +301,10 @@ impl RunProjectionReducer for RunProjection {
|
|||
stage_id.visit(),
|
||||
first_event_seq(event.seq),
|
||||
);
|
||||
stage.begin_attempt(ts);
|
||||
stage.begin_attempt(
|
||||
ts,
|
||||
StageHandler::from_handler_type(Some(&props.handler_type)),
|
||||
);
|
||||
}
|
||||
EventBody::StageRetrying(_) => {
|
||||
let Some(stage) = stage_at_stored_or_current_visit(self, stored, event.seq) else {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
|
|||
use crate::{
|
||||
BilledModelUsage, Checkpoint, Conclusion, DiffSummary, InterviewQuestionRecord,
|
||||
InvalidTransition, PullRequestRecord, Retro, RunControlAction, RunId, RunSpec, RunStatus,
|
||||
SandboxRecord, StageCompletion, StageId, StageState, StartRecord,
|
||||
SandboxRecord, StageCompletion, StageHandler, StageId, StageState, StartRecord,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -62,6 +62,8 @@ pub struct StageProjection {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub handler: Option<StageHandler>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub duration_ms: Option<u64>,
|
||||
/// Server-internal billing usage for the latest attempt; not part of the
|
||||
/// wire contract because `BilledModelUsage` is not modeled in OpenAPI.
|
||||
|
|
@ -99,6 +101,7 @@ impl StageProjection {
|
|||
live_streaming: None,
|
||||
termination: None,
|
||||
started_at: None,
|
||||
handler: None,
|
||||
state: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -142,9 +145,10 @@ impl StageProjection {
|
|||
/// per-attempt field so prior-attempt data does not leak, then record
|
||||
/// `started_at` and `state = Running`. Preserves `first_event_seq`
|
||||
/// (identity / sort key).
|
||||
pub fn begin_attempt(&mut self, started_at: DateTime<Utc>) {
|
||||
pub fn begin_attempt(&mut self, started_at: DateTime<Utc>, handler: StageHandler) {
|
||||
*self = Self::new(self.first_event_seq);
|
||||
self.started_at = Some(started_at);
|
||||
self.handler = Some(handler);
|
||||
self.state = Some(StageState::Running);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ impl RunStatus {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn requires_force_to_delete(self) -> bool {
|
||||
self.is_active() && !matches!(self, Self::Removing)
|
||||
}
|
||||
|
||||
pub fn blocked_reason(self) -> Option<BlockedReason> {
|
||||
match self {
|
||||
Self::Blocked { blocked_reason } => Some(blocked_reason),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue