mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add POST /api/v1/runs/delete batch delete endpoint (#382)
## Summary
Adds a fail-soft batch delete endpoint (`POST /api/v1/runs/delete`) that
mirrors the existing archive/unarchive batch pattern, processes 1–250
run IDs independently, and returns per-item outcomes with an aggregate
summary. Existing `DELETE /api/v1/runs/{id}` behavior is unchanged.
### Plan Summary
- **OpenAPI-first**: new
`BatchDeleteRunsRequest/Response/Result/Summary` schemas added to the
spec; Rust (`fabro-api`) and TypeScript (`fabro-api-client`) clients
regenerated.
- **Delete internals refactored**: `DeleteRunOutcome` gains `Deleted`
and `AlreadyAbsent` variants (replacing the old `NoContent`);
`delete_run_internal` and its helpers now return `Result<_, ApiError>`
instead of `Result<_, Response>`, enabling both the single-delete
handler and the new batch handler to reuse the same logic.
- **Batch handler**: `batch_delete_runs` in `lifecycle.rs` validates the
request (reusing the generalized `validate_batch_run_ids`), loops over
IDs, and assembles `BatchDeleteRunsResult` items mapping
`ApiError::status()` to outcome strings (`conflict`, `error`).
- **Web helper**: `deleteRuns` added to `run-actions.ts` alongside
`archiveRuns`/`unarchiveRuns`, with the same `as unknown as` cast needed
for the openapi-generator `Set<string>` quirk.
- **Tests**: six new server integration tests cover ordered results,
mixed outcomes without rollback, force deletion, sandbox preservation
handoff, pre-mutation validation rejection, and auth gating.
### Key design decisions
**`POST /runs/delete` not `DELETE /runs`** — JSON request bodies on
`DELETE` are poorly supported by proxies and HTTP clients; the existing
batch lifecycle endpoints already use JSON-body `POST` actions.
**`already_absent` counts as success** — consistent with single-delete
semantics where `204` means "deleted or already absent"; callers doing
cleanup don't need to special-case missing IDs.
**`force` is batch-wide** — callers needing mixed force behavior issue
separate requests; this keeps the request schema simple.
**`SandboxDeleteOutcome` internal enum** — introduced alongside
`DeleteRunOutcome` to cleanly separate the sandbox-layer result
(absent/cleaned/preserved) from the top-level outcome that callers see,
avoiding a leaky intermediate type.
### Fabro Details
<details>
<summary>Ran 8 stages in 41m 52s for $13.37</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 3s | – | 0 |
| preflight_lint | 2m 17s | – | 0 |
| implement | 15m 12s | $8.10 | 0 |
| simplify_opus | 8m 54s | $3.30 | 0 |
| simplify_gpt | 3m 54s | $1.97 | 0 |
| verify | 9m 0s | – | 0 |
| **Total** | **41m 52s** | **$13.37** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
This commit is contained in:
parent
fd5d932346
commit
04e169ef79
15 changed files with 987 additions and 135 deletions
|
|
@ -1,6 +1,11 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import type { AxiosAdapter } from "axios";
|
||||
import type { BatchRunLifecycleResponse, Run, RunStatus } from "@qltysh/fabro-api-client";
|
||||
import type {
|
||||
BatchDeleteRunsResponse,
|
||||
BatchRunLifecycleResponse,
|
||||
Run,
|
||||
RunStatus,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
|
||||
import {
|
||||
archiveRun,
|
||||
|
|
@ -11,6 +16,7 @@ import {
|
|||
canRetry,
|
||||
canUnarchive,
|
||||
cancelRun,
|
||||
deleteRuns,
|
||||
isTerminalCancelledRun,
|
||||
mapError,
|
||||
retryRun,
|
||||
|
|
@ -118,6 +124,20 @@ function batchResponse(
|
|||
};
|
||||
}
|
||||
|
||||
function batchDeleteResponse(
|
||||
results: BatchDeleteRunsResponse["results"],
|
||||
): BatchDeleteRunsResponse {
|
||||
const succeeded = results.filter((result) => result.ok).length;
|
||||
return {
|
||||
results,
|
||||
summary: {
|
||||
requested: results.length,
|
||||
succeeded,
|
||||
failed: results.length - succeeded,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function requestJsonBody(request: CapturedRequest): unknown {
|
||||
return typeof request.data === "string" ? JSON.parse(request.data) : request.data;
|
||||
}
|
||||
|
|
@ -229,6 +249,72 @@ describe("run lifecycle actions", () => {
|
|||
expect(result.results[1]?.error?.status).toBe("404");
|
||||
});
|
||||
|
||||
test("deleteRuns sends one batch request and parses results", async () => {
|
||||
const stub = stubGeneratedAxiosOnce({
|
||||
status: 200,
|
||||
body: batchDeleteResponse([
|
||||
{
|
||||
run_id: "run-1",
|
||||
ok: true,
|
||||
outcome: "deleted",
|
||||
},
|
||||
{
|
||||
run_id: "run-missing",
|
||||
ok: true,
|
||||
outcome: "already_absent",
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
const result = await deleteRuns(["run-1", "run-missing"]);
|
||||
|
||||
expect(stub.requests).toHaveLength(1);
|
||||
expect(stub.requests[0]?.method?.toUpperCase()).toBe("POST");
|
||||
expect(stub.requests[0]?.url).toBe("/api/v1/runs/delete");
|
||||
expect(requestJsonBody(stub.requests[0]!)).toEqual({
|
||||
run_ids: ["run-1", "run-missing"],
|
||||
force: false,
|
||||
});
|
||||
expect(result.summary).toEqual({ requested: 2, succeeded: 2, failed: 0 });
|
||||
expect(result.results.map((entry) => entry.outcome)).toEqual(["deleted", "already_absent"]);
|
||||
});
|
||||
|
||||
test("deleteRuns sends force when requested", async () => {
|
||||
const stub = stubGeneratedAxiosOnce({
|
||||
status: 200,
|
||||
body: batchDeleteResponse([
|
||||
{
|
||||
run_id: "run-1",
|
||||
ok: true,
|
||||
outcome: "deleted",
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
await deleteRuns(["run-1"], true);
|
||||
|
||||
expect(stub.requests).toHaveLength(1);
|
||||
expect(requestJsonBody(stub.requests[0]!)).toEqual({
|
||||
run_ids: ["run-1"],
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("deleteRuns preserves request-level error envelopes", async () => {
|
||||
stubGeneratedAxiosOnce({
|
||||
status: 400,
|
||||
body: {
|
||||
errors: [{ status: "400", title: "Bad Request", detail: "run_ids must contain at least one run ID." }],
|
||||
},
|
||||
});
|
||||
|
||||
const error = await expectLifecycleError(deleteRuns([]));
|
||||
expect(error).toEqual({
|
||||
status: 400,
|
||||
errors: [{ status: "400", title: "Bad Request", detail: "run_ids must contain at least one run ID." }],
|
||||
});
|
||||
});
|
||||
|
||||
test("batch lifecycle helpers preserve request-level error envelopes", async () => {
|
||||
stubGeneratedAxiosOnce({
|
||||
status: 400,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type {
|
||||
BatchDeleteRunsRequest,
|
||||
BatchDeleteRunsResponse,
|
||||
BatchRunLifecycleRequest,
|
||||
BatchRunLifecycleResponse,
|
||||
ErrorResponseEntry,
|
||||
|
|
@ -77,6 +79,22 @@ export async function unarchiveRuns(
|
|||
return batchRunLifecycleAction(runIds, "unarchive", request);
|
||||
}
|
||||
|
||||
export async function deleteRuns(
|
||||
runIds: string[],
|
||||
force = false,
|
||||
request?: Request,
|
||||
): Promise<BatchDeleteRunsResponse> {
|
||||
try {
|
||||
// See `batchRunLifecycleAction` for the `as unknown as` rationale:
|
||||
// openapi-generator types `uniqueItems` arrays as `Set<T>` while the wire
|
||||
// contract is a JSON array.
|
||||
const body = { run_ids: runIds, force } as unknown as BatchDeleteRunsRequest;
|
||||
return await apiData(() => runsApi.batchDeleteRuns(body, requestSignalOptions(request)));
|
||||
} catch (error) {
|
||||
throw lifecycleActionErrorFromError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function retryRun(id: string, request?: Request): Promise<Run> {
|
||||
return runLifecycleAction(id, "retry", request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1196,6 +1196,57 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/delete:
|
||||
post:
|
||||
operationId: batchDeleteRuns
|
||||
tags: [Runs]
|
||||
summary: Delete Runs
|
||||
description: >
|
||||
Deletes up to 250 runs in one fail-soft, non-transactional request.
|
||||
Each run is processed independently. A valid batch returns `200` even
|
||||
when some items fail; inspect `results` and `summary` for per-run
|
||||
outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BatchDeleteRunsRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Batch processed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BatchDeleteRunsResponse"
|
||||
"400":
|
||||
description: Invalid batch request
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"401":
|
||||
description: Not authenticated
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"500":
|
||||
description: Request-level server error
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/unarchive:
|
||||
post:
|
||||
operationId: batchUnarchiveRuns
|
||||
|
|
@ -5378,6 +5429,96 @@ components:
|
|||
minimum: 0
|
||||
description: Number of item results with `ok=false`.
|
||||
|
||||
BatchDeleteRunsRequest:
|
||||
description: Run IDs to delete as one bounded fail-soft batch.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- run_ids
|
||||
properties:
|
||||
run_ids:
|
||||
type: array
|
||||
description: Run IDs to process, in result order.
|
||||
minItems: 1
|
||||
maxItems: 250
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
example: 01HZX6M29F1CD5YYMHT1F5D7WQ
|
||||
force:
|
||||
type: boolean
|
||||
description: Whether to force deletion of active runs. Defaults to `false`.
|
||||
default: false
|
||||
|
||||
BatchDeleteRunsResponse:
|
||||
description: Per-run results for a fail-soft batch delete request.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- results
|
||||
- summary
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
description: Results ordered exactly like the request `run_ids`.
|
||||
items:
|
||||
$ref: "#/components/schemas/BatchDeleteRunsResult"
|
||||
summary:
|
||||
$ref: "#/components/schemas/BatchDeleteRunsSummary"
|
||||
|
||||
BatchDeleteRunsResult:
|
||||
description: Result for one run in a batch delete request.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- run_id
|
||||
- ok
|
||||
- outcome
|
||||
properties:
|
||||
run_id:
|
||||
type: string
|
||||
description: Run ID from the request item.
|
||||
ok:
|
||||
type: boolean
|
||||
description: Whether this item succeeded.
|
||||
outcome:
|
||||
type: string
|
||||
enum:
|
||||
- deleted
|
||||
- already_absent
|
||||
- sandbox_preserved
|
||||
- conflict
|
||||
- error
|
||||
description: Machine-readable item outcome.
|
||||
sandbox:
|
||||
$ref: "#/components/schemas/DeleteRunSandbox"
|
||||
description: Sandbox handoff details when `outcome` is `sandbox_preserved`.
|
||||
error:
|
||||
$ref: "#/components/schemas/ErrorResponseEntry"
|
||||
description: Structured item error for failed items.
|
||||
|
||||
BatchDeleteRunsSummary:
|
||||
description: Aggregate counts for a batch delete request.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- requested
|
||||
- succeeded
|
||||
- failed
|
||||
properties:
|
||||
requested:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Number of requested run IDs.
|
||||
succeeded:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Number of item results with `ok=true`.
|
||||
failed:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Number of item results with `ok=false`.
|
||||
|
||||
PairId:
|
||||
type: string
|
||||
description: Durable run pair identifier.
|
||||
|
|
|
|||
|
|
@ -23,25 +23,26 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|||
use bytes::Bytes;
|
||||
pub use fabro_api::types::{
|
||||
AggregateBilling, AggregateBillingTotals, ApiQuestion, AppendEventResponse, ArtifactEntry,
|
||||
ArtifactListResponse, BatchRunLifecycleRequest, BatchRunLifecycleResponse,
|
||||
BatchRunLifecycleResult, BatchRunLifecycleResultOutcome, BatchRunLifecycleSummary,
|
||||
BillingByModel, BillingStageRef, CloseRunPullRequestResponse, CompletionContentPart,
|
||||
CompletionMessage, CompletionMessageRole, CompletionResponse, CompletionToolChoiceMode,
|
||||
CompletionUsage, CreateCompletionRequest, CreateRunPullRequestRequest, CreateSecretRequest,
|
||||
DeleteRunResponse, DeleteRunSandbox, DeleteSecretRequest, DenyRunRequest, DiskUsageResponse,
|
||||
DiskUsageRunRow, DiskUsageSummaryRow, ErrorResponseEntry, ForkRequest, ForkResponse,
|
||||
LinkRunPullRequestRequest, MergeRunPullRequestRequest, MergeRunPullRequestResponse,
|
||||
ModelReference, PaginatedEventList, PaginatedRunList, PaginationMeta, PreflightResponse,
|
||||
PreviewUrlRequest, PreviewUrlResponse, Provider, ProviderList, PruneRunEntry, PruneRunsRequest,
|
||||
PruneRunsResponse, RenderWorkflowGraphDirection, RenderWorkflowGraphRequest, RewindRequest,
|
||||
RewindResponse, Run, RunArtifactEntry, RunArtifactListResponse, RunBilling, RunBillingStage,
|
||||
RunBillingTotals, RunError, RunManifest, RunStage, SandboxDetails, SandboxFileEntry,
|
||||
SandboxFileListResponse, SandboxService, SandboxServiceListResponse, SshAccessRequest,
|
||||
SshAccessResponse, StageHandler, StageState, StartRunRequest, SubmitAnswerRequest,
|
||||
SystemCpuResourceScope, SystemCpuResources, SystemDiskResourceScope, SystemDiskResources,
|
||||
SystemInfoResponse, SystemMemoryResourceScope, SystemMemoryResources, SystemRepairRunIssue,
|
||||
SystemRepairRunsResponse, SystemResourcesResponse, SystemRunCounts, TimelineEntryResponse,
|
||||
VncPreviewResponse, WriteBlobResponse,
|
||||
ArtifactListResponse, BatchDeleteRunsRequest, BatchDeleteRunsResponse, BatchDeleteRunsResult,
|
||||
BatchDeleteRunsResultOutcome, BatchDeleteRunsSummary, BatchRunLifecycleRequest,
|
||||
BatchRunLifecycleResponse, BatchRunLifecycleResult, BatchRunLifecycleResultOutcome,
|
||||
BatchRunLifecycleSummary, BillingByModel, BillingStageRef, CloseRunPullRequestResponse,
|
||||
CompletionContentPart, CompletionMessage, CompletionMessageRole, CompletionResponse,
|
||||
CompletionToolChoiceMode, CompletionUsage, CreateCompletionRequest,
|
||||
CreateRunPullRequestRequest, CreateSecretRequest, DeleteRunResponse, DeleteRunSandbox,
|
||||
DeleteSecretRequest, DenyRunRequest, DiskUsageResponse, DiskUsageRunRow, DiskUsageSummaryRow,
|
||||
ErrorResponseEntry, ForkRequest, ForkResponse, LinkRunPullRequestRequest,
|
||||
MergeRunPullRequestRequest, MergeRunPullRequestResponse, ModelReference, PaginatedEventList,
|
||||
PaginatedRunList, PaginationMeta, PreflightResponse, PreviewUrlRequest, PreviewUrlResponse,
|
||||
Provider, ProviderList, PruneRunEntry, PruneRunsRequest, PruneRunsResponse,
|
||||
RenderWorkflowGraphDirection, RenderWorkflowGraphRequest, RewindRequest, RewindResponse, Run,
|
||||
RunArtifactEntry, RunArtifactListResponse, RunBilling, RunBillingStage, RunBillingTotals,
|
||||
RunError, RunManifest, RunStage, SandboxDetails, SandboxFileEntry, SandboxFileListResponse,
|
||||
SandboxService, SandboxServiceListResponse, SshAccessRequest, SshAccessResponse, StageHandler,
|
||||
StageState, StartRunRequest, SubmitAnswerRequest, SystemCpuResourceScope, SystemCpuResources,
|
||||
SystemDiskResourceScope, SystemDiskResources, SystemInfoResponse, SystemMemoryResourceScope,
|
||||
SystemMemoryResources, SystemRepairRunIssue, SystemRepairRunsResponse, SystemResourcesResponse,
|
||||
SystemRunCounts, TimelineEntryResponse, VncPreviewResponse, WriteBlobResponse,
|
||||
};
|
||||
use fabro_auth::{CredentialSource, VaultCredentialSource, auth_issue_message};
|
||||
#[cfg(test)]
|
||||
|
|
@ -2175,17 +2176,27 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
const MAX_PAGE_OFFSET: u32 = 1_000_000;
|
||||
|
||||
enum DeleteRunOutcome {
|
||||
NoContent,
|
||||
Deleted,
|
||||
AlreadyAbsent,
|
||||
Preserved(DeleteRunResponse),
|
||||
}
|
||||
|
||||
enum SandboxDeleteOutcome {
|
||||
/// The durable run store did not exist; nothing to delete.
|
||||
Absent,
|
||||
/// The sandbox resource was cleaned up (or there was none to clean).
|
||||
Cleaned,
|
||||
/// Sandbox is being handed off to the operator instead of deleted.
|
||||
Preserved(DeleteRunResponse),
|
||||
}
|
||||
|
||||
async fn delete_run_internal(
|
||||
state: &Arc<AppState>,
|
||||
state: &AppState,
|
||||
id: RunId,
|
||||
force: bool,
|
||||
) -> Result<DeleteRunOutcome, Response> {
|
||||
) -> Result<DeleteRunOutcome, ApiError> {
|
||||
if !force {
|
||||
reject_active_delete_without_force(state.as_ref(), &id).await?;
|
||||
reject_active_delete_without_force(state, &id).await?;
|
||||
}
|
||||
|
||||
let mut managed_run = if let Ok(mut runs) = state.runs.lock() {
|
||||
|
|
@ -2193,8 +2204,9 @@ async fn delete_run_internal(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let had_managed_run = managed_run.is_some();
|
||||
let durable_status = if managed_run.is_some() {
|
||||
load_durable_run_status(state.as_ref(), &id).await
|
||||
load_durable_run_status(state, &id).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -2232,29 +2244,32 @@ async fn delete_run_internal(
|
|||
|
||||
if let Some(mut managed_run) = managed_run {
|
||||
if let Some(run_dir) = managed_run.run_dir.take() {
|
||||
remove_run_dir(&run_dir).map_err(|err| {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
})?;
|
||||
remove_run_dir(&run_dir)
|
||||
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
}
|
||||
} else {
|
||||
let storage = Storage::new(state.server_storage_dir());
|
||||
let run_dir = storage.run_scratch(&id).root().to_path_buf();
|
||||
remove_run_dir(&run_dir).map_err(|err| {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
})?;
|
||||
remove_run_dir(&run_dir)
|
||||
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
}
|
||||
|
||||
state.store.delete_run(&id).await.map_err(|err| {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
})?;
|
||||
state
|
||||
.store
|
||||
.delete_run(&id)
|
||||
.await
|
||||
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
state
|
||||
.artifact_store
|
||||
.delete_for_run(&id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
})?;
|
||||
Ok(delete_outcome)
|
||||
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
match delete_outcome {
|
||||
SandboxDeleteOutcome::Preserved(response) => Ok(DeleteRunOutcome::Preserved(response)),
|
||||
SandboxDeleteOutcome::Cleaned => Ok(DeleteRunOutcome::Deleted),
|
||||
SandboxDeleteOutcome::Absent if had_managed_run => Ok(DeleteRunOutcome::Deleted),
|
||||
SandboxDeleteOutcome::Absent => Ok(DeleteRunOutcome::AlreadyAbsent),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_durable_run_status(state: &AppState, id: &RunId) -> Option<RunStatus> {
|
||||
|
|
@ -2264,12 +2279,12 @@ async fn load_durable_run_status(state: &AppState, id: &RunId) -> Option<RunStat
|
|||
}
|
||||
|
||||
async fn delete_run_sandbox_resource(
|
||||
state: &Arc<AppState>,
|
||||
state: &AppState,
|
||||
id: RunId,
|
||||
force: bool,
|
||||
) -> Result<DeleteRunOutcome, Response> {
|
||||
) -> Result<SandboxDeleteOutcome, ApiError> {
|
||||
let Ok(run_store) = state.store.open_run(&id).await else {
|
||||
return Ok(DeleteRunOutcome::NoContent);
|
||||
return Ok(SandboxDeleteOutcome::Absent);
|
||||
};
|
||||
let projection = match run_store.state().await {
|
||||
Ok(projection) => projection,
|
||||
|
|
@ -2279,12 +2294,13 @@ async fn delete_run_sandbox_resource(
|
|||
error = %render_with_causes(&err.to_string(), &collect_causes(&err)),
|
||||
"Skipping sandbox provider delete because run projection cannot be loaded"
|
||||
);
|
||||
return Ok(DeleteRunOutcome::NoContent);
|
||||
return Ok(SandboxDeleteOutcome::Cleaned);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
|
||||
);
|
||||
return Err(ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let delete_started = matches!(projection.status, RunStatus::Removing);
|
||||
|
|
@ -2292,9 +2308,7 @@ async fn delete_run_sandbox_resource(
|
|||
if !delete_started && can_mark_removing {
|
||||
workflow_event::append_event(&run_store, &id, &workflow_event::Event::RunRemoving)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
})?;
|
||||
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
}
|
||||
|
||||
let preserve = projection
|
||||
|
|
@ -2305,19 +2319,18 @@ async fn delete_run_sandbox_resource(
|
|||
.lifecycle
|
||||
.preserve;
|
||||
let Some(record) = projection.sandbox else {
|
||||
return Ok(DeleteRunOutcome::NoContent);
|
||||
return Ok(SandboxDeleteOutcome::Cleaned);
|
||||
};
|
||||
let Some(runtime) = record.runtime.as_ref() else {
|
||||
return Ok(SandboxDeleteOutcome::Cleaned);
|
||||
};
|
||||
if preserve {
|
||||
return Ok(DeleteRunOutcome::Preserved(DeleteRunResponse {
|
||||
return Ok(SandboxDeleteOutcome::Preserved(DeleteRunResponse {
|
||||
deleted: true,
|
||||
sandbox_preserved: true,
|
||||
sandbox: DeleteRunSandbox {
|
||||
provider: record.provider,
|
||||
id: record
|
||||
.runtime
|
||||
.as_ref()
|
||||
.map(|runtime| runtime.id.clone())
|
||||
.unwrap_or_default(),
|
||||
id: runtime.id.clone(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
|
@ -2331,11 +2344,11 @@ async fn delete_run_sandbox_resource(
|
|||
error = %render_with_causes(&err.to_string(), &collect_causes(err.as_ref())),
|
||||
"Skipping sandbox provider delete during run deletion"
|
||||
);
|
||||
return Ok(DeleteRunOutcome::NoContent);
|
||||
return Ok(SandboxDeleteOutcome::Cleaned);
|
||||
}
|
||||
Err(err) => {
|
||||
let detail = render_with_causes(&err.to_string(), &collect_causes(err.as_ref()));
|
||||
return Err(ApiError::new(StatusCode::CONFLICT, detail).into_response());
|
||||
return Err(ApiError::new(StatusCode::CONFLICT, detail));
|
||||
}
|
||||
};
|
||||
if let Err(err) = sandbox.delete().await {
|
||||
|
|
@ -2345,18 +2358,21 @@ async fn delete_run_sandbox_resource(
|
|||
error = %err.display_with_causes(),
|
||||
"Skipping failed sandbox provider delete during run deletion"
|
||||
);
|
||||
return Ok(DeleteRunOutcome::NoContent);
|
||||
return Ok(SandboxDeleteOutcome::Cleaned);
|
||||
}
|
||||
return Err(ApiError::new(StatusCode::CONFLICT, err.display_with_causes()).into_response());
|
||||
return Err(ApiError::new(
|
||||
StatusCode::CONFLICT,
|
||||
err.display_with_causes(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(DeleteRunOutcome::NoContent)
|
||||
Ok(SandboxDeleteOutcome::Cleaned)
|
||||
}
|
||||
|
||||
async fn reject_active_delete_without_force(
|
||||
state: &AppState,
|
||||
run_id: &RunId,
|
||||
) -> Result<(), Response> {
|
||||
) -> Result<(), ApiError> {
|
||||
let managed_status = state
|
||||
.runs
|
||||
.lock()
|
||||
|
|
@ -2367,8 +2383,7 @@ async fn reject_active_delete_without_force(
|
|||
return Err(ApiError::new(
|
||||
StatusCode::CONFLICT,
|
||||
active_run_delete_message(*run_id, status),
|
||||
)
|
||||
.into_response());
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -2378,13 +2393,13 @@ async fn reject_active_delete_without_force(
|
|||
Err(ApiError::new(
|
||||
StatusCode::CONFLICT,
|
||||
active_run_delete_message(*run_id, summary.lifecycle.status),
|
||||
)
|
||||
.into_response())
|
||||
))
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
Err(ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())
|
||||
}
|
||||
Err(err) => Err(ApiError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,18 @@ use std::sync::Arc;
|
|||
use chrono::Utc;
|
||||
|
||||
use super::super::{
|
||||
ApiError, AppState, AskFabroReadiness, BatchRunLifecycleRequest, BatchRunLifecycleResponse,
|
||||
BatchRunLifecycleResult, BatchRunLifecycleResultOutcome, BatchRunLifecycleSummary,
|
||||
ApiError, AppState, AskFabroReadiness, BatchDeleteRunsRequest, BatchDeleteRunsResponse,
|
||||
BatchDeleteRunsResult, BatchDeleteRunsResultOutcome, BatchDeleteRunsSummary,
|
||||
BatchRunLifecycleRequest, BatchRunLifecycleResponse, BatchRunLifecycleResult,
|
||||
BatchRunLifecycleResultOutcome, BatchRunLifecycleSummary, DeleteRunOutcome, DeleteRunSandbox,
|
||||
DenyRunRequest, FailureReason, ForkRequest, ForkResponse, HeaderMap, IntoResponse, Json, Path,
|
||||
PendingReason, Principal, RequireRunScopedOrRunTools, RequiredUser, Response, RewindRequest,
|
||||
RewindResponse, Router, RunAnswerTransport, RunControlAction, RunExecutionMode, RunId,
|
||||
RunRunnableSource, RunStatus, StartRunRequest, State, StatusCode, Storage,
|
||||
TimelineEntryResponse, WORKER_CANCEL_GRACE, WorkflowError, append_control_request,
|
||||
clear_live_run_state, durable_run_status, get, load_pending_control, managed_run, operations,
|
||||
parse_run_id_path, persist_cancelled_run_status, post, reject_if_archived, sleep,
|
||||
update_live_run_from_event, workflow_event,
|
||||
clear_live_run_state, delete_run_internal, durable_run_status, get, load_pending_control,
|
||||
managed_run, operations, parse_run_id_path, persist_cancelled_run_status, post,
|
||||
reject_if_archived, sleep, update_live_run_from_event, workflow_event,
|
||||
};
|
||||
use super::runs::run_provenance;
|
||||
|
||||
|
|
@ -26,6 +28,7 @@ pub(super) fn routes() -> Router<Arc<AppState>> {
|
|||
.route("/runs/{id}/pause", post(pause_run))
|
||||
.route("/runs/{id}/unpause", post(unpause_run))
|
||||
.route("/runs/archive", post(batch_archive_runs))
|
||||
.route("/runs/delete", post(batch_delete_runs))
|
||||
.route("/runs/unarchive", post(batch_unarchive_runs))
|
||||
.route("/runs/{id}/archive", post(archive_run))
|
||||
.route("/runs/{id}/rewind", post(rewind_run))
|
||||
|
|
@ -717,6 +720,38 @@ async fn batch_unarchive_runs(
|
|||
.await
|
||||
}
|
||||
|
||||
async fn batch_delete_runs(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<BatchDeleteRunsRequest>,
|
||||
) -> Response {
|
||||
let force = request.force;
|
||||
let ids = match validate_batch_run_ids(request.run_ids) {
|
||||
Ok(ids) => ids,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
|
||||
let mut results = Vec::with_capacity(ids.len());
|
||||
for id in ids {
|
||||
results.push(batch_delete_run_item(state.as_ref(), id, force).await);
|
||||
}
|
||||
|
||||
let requested = results.len() as u64;
|
||||
let succeeded = results.iter().filter(|result| result.ok).count() as u64;
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(BatchDeleteRunsResponse {
|
||||
results,
|
||||
summary: BatchDeleteRunsSummary {
|
||||
requested,
|
||||
succeeded,
|
||||
failed: requested - succeeded,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn rewind_run(
|
||||
subject: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -909,7 +944,7 @@ enum ArchiveAction {
|
|||
Unarchive,
|
||||
}
|
||||
|
||||
const MAX_BATCH_RUN_LIFECYCLE_IDS: usize = 250;
|
||||
const MAX_BATCH_RUN_IDS: usize = 250;
|
||||
|
||||
async fn batch_run_archive_action(
|
||||
state: Arc<AppState>,
|
||||
|
|
@ -917,7 +952,7 @@ async fn batch_run_archive_action(
|
|||
request: BatchRunLifecycleRequest,
|
||||
action: ArchiveAction,
|
||||
) -> Response {
|
||||
let ids = match validate_batch_run_ids(request) {
|
||||
let ids = match validate_batch_run_ids(request.run_ids) {
|
||||
Ok(ids) => ids,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
|
|
@ -949,21 +984,21 @@ async fn batch_run_archive_action(
|
|||
.into_response()
|
||||
}
|
||||
|
||||
fn validate_batch_run_ids(request: BatchRunLifecycleRequest) -> Result<Vec<RunId>, ApiError> {
|
||||
if request.run_ids.is_empty() {
|
||||
fn validate_batch_run_ids(run_ids: Vec<String>) -> Result<Vec<RunId>, ApiError> {
|
||||
if run_ids.is_empty() {
|
||||
return Err(ApiError::bad_request(
|
||||
"run_ids must contain at least one run ID.",
|
||||
));
|
||||
}
|
||||
if request.run_ids.len() > MAX_BATCH_RUN_LIFECYCLE_IDS {
|
||||
if run_ids.len() > MAX_BATCH_RUN_IDS {
|
||||
return Err(ApiError::bad_request(format!(
|
||||
"run_ids must contain no more than {MAX_BATCH_RUN_LIFECYCLE_IDS} run IDs.",
|
||||
"run_ids must contain no more than {MAX_BATCH_RUN_IDS} run IDs.",
|
||||
)));
|
||||
}
|
||||
|
||||
let mut seen = HashSet::with_capacity(request.run_ids.len());
|
||||
let mut ids = Vec::with_capacity(request.run_ids.len());
|
||||
for raw in request.run_ids {
|
||||
let mut seen = HashSet::with_capacity(run_ids.len());
|
||||
let mut ids = Vec::with_capacity(run_ids.len());
|
||||
for raw in run_ids {
|
||||
let id = raw.parse::<RunId>().map_err(|_| {
|
||||
ApiError::bad_request(format!("run_ids contains invalid run ID: {raw}"))
|
||||
})?;
|
||||
|
|
@ -977,6 +1012,49 @@ fn validate_batch_run_ids(request: BatchRunLifecycleRequest) -> Result<Vec<RunId
|
|||
Ok(ids)
|
||||
}
|
||||
|
||||
async fn batch_delete_run_item(state: &AppState, id: RunId, force: bool) -> BatchDeleteRunsResult {
|
||||
match delete_run_internal(state, id, force).await {
|
||||
Ok(DeleteRunOutcome::Deleted) => {
|
||||
batch_delete_success(id, BatchDeleteRunsResultOutcome::Deleted, None)
|
||||
}
|
||||
Ok(DeleteRunOutcome::AlreadyAbsent) => {
|
||||
batch_delete_success(id, BatchDeleteRunsResultOutcome::AlreadyAbsent, None)
|
||||
}
|
||||
Ok(DeleteRunOutcome::Preserved(response)) => batch_delete_success(
|
||||
id,
|
||||
BatchDeleteRunsResultOutcome::SandboxPreserved,
|
||||
Some(response.sandbox),
|
||||
),
|
||||
Err(error) => {
|
||||
let outcome = match error.status() {
|
||||
StatusCode::CONFLICT => BatchDeleteRunsResultOutcome::Conflict,
|
||||
_ => BatchDeleteRunsResultOutcome::Error,
|
||||
};
|
||||
BatchDeleteRunsResult {
|
||||
run_id: id.to_string(),
|
||||
ok: false,
|
||||
outcome,
|
||||
sandbox: None,
|
||||
error: Some(error.into_response_entry()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn batch_delete_success(
|
||||
id: RunId,
|
||||
outcome: BatchDeleteRunsResultOutcome,
|
||||
sandbox: Option<DeleteRunSandbox>,
|
||||
) -> BatchDeleteRunsResult {
|
||||
BatchDeleteRunsResult {
|
||||
run_id: id.to_string(),
|
||||
ok: true,
|
||||
outcome,
|
||||
sandbox,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn batch_run_archive_item(
|
||||
state: &AppState,
|
||||
readiness: &AskFabroReadiness,
|
||||
|
|
|
|||
|
|
@ -32,10 +32,11 @@ use tokio::fs;
|
|||
use tracing::info;
|
||||
|
||||
use super::super::{
|
||||
AppState, ListResponse, PaginationParams, RunExecutionMode, answer_from_request,
|
||||
api_question_from_pending_interview, default_page_limit, delete_run_internal,
|
||||
load_pending_interview, managed_run, paginate_items, parse_run_id_path, parse_stage_id_path,
|
||||
reject_if_archived, resolve_interp_string, submit_pending_interview_answer, workflow_event,
|
||||
AppState, DeleteRunOutcome, ListResponse, PaginationParams, RunExecutionMode,
|
||||
answer_from_request, api_question_from_pending_interview, default_page_limit,
|
||||
delete_run_internal, load_pending_interview, managed_run, paginate_items, parse_run_id_path,
|
||||
parse_stage_id_path, reject_if_archived, resolve_interp_string,
|
||||
submit_pending_interview_answer, workflow_event,
|
||||
};
|
||||
use crate::error::ApiError;
|
||||
use crate::principal_middleware::{
|
||||
|
|
@ -508,12 +509,14 @@ async fn delete_run(
|
|||
Err(response) => return response,
|
||||
};
|
||||
|
||||
match delete_run_internal(&state, id, query.force).await {
|
||||
Ok(super::super::DeleteRunOutcome::NoContent) => StatusCode::NO_CONTENT.into_response(),
|
||||
Ok(super::super::DeleteRunOutcome::Preserved(response)) => {
|
||||
match delete_run_internal(state.as_ref(), id, query.force).await {
|
||||
Ok(DeleteRunOutcome::Deleted | DeleteRunOutcome::AlreadyAbsent) => {
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
Ok(DeleteRunOutcome::Preserved(response)) => {
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
Err(response) => response,
|
||||
Err(error) => error.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -207,8 +207,8 @@ async fn prune_runs(
|
|||
}
|
||||
|
||||
for run_id in &prune_plan.run_ids {
|
||||
if let Err(response) = delete_run_internal(&state, *run_id, true).await {
|
||||
return response;
|
||||
if let Err(error) = delete_run_internal(state.as_ref(), *run_id, true).await {
|
||||
return error.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3725,7 +3725,9 @@ async fn delete_terminal_managed_run_does_not_send_cancel_signal() {
|
|||
.expect("runs lock poisoned")
|
||||
.insert(run_id, run);
|
||||
|
||||
delete_run_internal(&state, run_id, true).await.unwrap();
|
||||
delete_run_internal(state.as_ref(), run_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!cancel_token.is_cancelled());
|
||||
}
|
||||
|
|
@ -10557,12 +10559,64 @@ async fn create_running_run(state: &Arc<AppState>, run_id: RunId) {
|
|||
.await;
|
||||
}
|
||||
|
||||
async fn create_preserved_local_sandbox_run(state: &Arc<AppState>, run_id: RunId) {
|
||||
let mut settings = fabro_types::WorkflowSettings::default();
|
||||
settings.run.environment.lifecycle.preserve = true;
|
||||
let graph = Graph::new("test");
|
||||
|
||||
create_durable_run_with_events(state, run_id, &[
|
||||
workflow_event::Event::RunCreated {
|
||||
run_id,
|
||||
title: None,
|
||||
settings: serde_json::to_value(settings).unwrap(),
|
||||
graph: serde_json::to_value(graph).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: std::collections::BTreeMap::default(),
|
||||
run_dir: "/tmp/fabro-run".to_string(),
|
||||
source_directory: Some("/tmp/fabro-run".to_string()),
|
||||
workflow_slug: Some("test".to_string()),
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
workflow_event::Event::RunSubmitted {
|
||||
definition_blob: None,
|
||||
},
|
||||
workflow_event::Event::SandboxInitialized {
|
||||
provider: SandboxProvider::Local,
|
||||
id: "sandbox-preserve-1".to_string(),
|
||||
working_directory: "/tmp/fabro-preserved-sandbox".to_string(),
|
||||
repo_cloned: None,
|
||||
clone_origin_url: None,
|
||||
clone_branch: None,
|
||||
workspace_root: None,
|
||||
repos_root: None,
|
||||
primary_repo_path: None,
|
||||
primary_repo_link: None,
|
||||
},
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
fn batch_lifecycle_body(run_ids: &[RunId]) -> serde_json::Value {
|
||||
json!({
|
||||
"run_ids": run_ids.iter().map(ToString::to_string).collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
fn batch_delete_body(run_ids: &[RunId], force: bool) -> serde_json::Value {
|
||||
json!({
|
||||
"run_ids": run_ids.iter().map(ToString::to_string).collect::<Vec<_>>(),
|
||||
"force": force,
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_batch_result(result: &serde_json::Value, run_id: RunId, ok: bool, outcome: &str) {
|
||||
assert_eq!(result["run_id"], run_id.to_string());
|
||||
assert_eq!(result["ok"], ok);
|
||||
|
|
@ -10588,6 +10642,23 @@ fn assert_batch_result(result: &serde_json::Value, run_id: RunId, ok: bool, outc
|
|||
}
|
||||
}
|
||||
|
||||
fn assert_batch_delete_result(result: &serde_json::Value, run_id: RunId, ok: bool, outcome: &str) {
|
||||
assert_eq!(result["run_id"], run_id.to_string());
|
||||
assert_eq!(result["ok"], ok);
|
||||
assert_eq!(result["outcome"], outcome);
|
||||
if ok {
|
||||
assert!(
|
||||
result["error"].is_null(),
|
||||
"successful delete result should omit error: {result}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
result["error"].is_object(),
|
||||
"failed delete result should include error: {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_archive_and_unarchive_updates_listing_visibility() {
|
||||
let state = test_app_state();
|
||||
|
|
@ -10852,6 +10923,249 @@ async fn batch_lifecycle_requires_user_authentication() {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_removes_runs_and_reports_ordered_results() {
|
||||
let state = test_app_state();
|
||||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let first_id = RunId::new();
|
||||
let second_id = RunId::new();
|
||||
create_succeeded_run(&state, first_id).await;
|
||||
create_succeeded_run(&state, second_id).await;
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(json_request(
|
||||
Method::POST,
|
||||
"/runs/delete",
|
||||
&batch_delete_body(&[first_id, second_id], false),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
assert_eq!(body["summary"]["requested"], 2);
|
||||
assert_eq!(body["summary"]["succeeded"], 2);
|
||||
assert_eq!(body["summary"]["failed"], 0);
|
||||
let results = body["results"].as_array().unwrap();
|
||||
assert_batch_delete_result(&results[0], first_id, true, "deleted");
|
||||
assert_batch_delete_result(&results[1], second_id, true, "deleted");
|
||||
|
||||
for run_id in [first_id, second_id] {
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{run_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(response, StatusCode::NOT_FOUND).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_reports_mixed_results_without_rollback() {
|
||||
let state = test_app_state();
|
||||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let terminal_id = RunId::new();
|
||||
let running_id = RunId::new();
|
||||
let missing_id = RunId::new();
|
||||
create_succeeded_run(&state, terminal_id).await;
|
||||
create_running_run(&state, running_id).await;
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(json_request(
|
||||
Method::POST,
|
||||
"/runs/delete",
|
||||
&batch_delete_body(&[terminal_id, running_id, missing_id], false),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
assert_eq!(body["summary"]["requested"], 3);
|
||||
assert_eq!(body["summary"]["succeeded"], 2);
|
||||
assert_eq!(body["summary"]["failed"], 1);
|
||||
let results = body["results"].as_array().unwrap();
|
||||
assert_batch_delete_result(&results[0], terminal_id, true, "deleted");
|
||||
assert_batch_delete_result(&results[1], running_id, false, "conflict");
|
||||
assert_eq!(results[1]["error"]["status"], "409");
|
||||
assert_batch_delete_result(&results[2], missing_id, true, "already_absent");
|
||||
|
||||
let deleted_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{terminal_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(deleted_response, StatusCode::NOT_FOUND).await;
|
||||
|
||||
let running_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{running_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(running_response, StatusCode::OK).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_force_removes_active_runs() {
|
||||
let state = test_app_state();
|
||||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let run_id = RunId::new();
|
||||
create_running_run(&state, run_id).await;
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(json_request(
|
||||
Method::POST,
|
||||
"/runs/delete",
|
||||
&batch_delete_body(&[run_id], true),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
assert_eq!(body["summary"]["requested"], 1);
|
||||
assert_eq!(body["summary"]["succeeded"], 1);
|
||||
assert_eq!(body["summary"]["failed"], 0);
|
||||
let results = body["results"].as_array().unwrap();
|
||||
assert_batch_delete_result(&results[0], run_id, true, "deleted");
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{run_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(response, StatusCode::NOT_FOUND).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_with_preserved_sandbox_returns_handoff() {
|
||||
let state = test_app_state();
|
||||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let run_id = RunId::new();
|
||||
create_preserved_local_sandbox_run(&state, run_id).await;
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(json_request(
|
||||
Method::POST,
|
||||
"/runs/delete",
|
||||
&batch_delete_body(&[run_id], true),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
assert_eq!(body["summary"]["requested"], 1);
|
||||
assert_eq!(body["summary"]["succeeded"], 1);
|
||||
assert_eq!(body["summary"]["failed"], 0);
|
||||
let results = body["results"].as_array().unwrap();
|
||||
assert_batch_delete_result(&results[0], run_id, true, "sandbox_preserved");
|
||||
assert_eq!(results[0]["sandbox"]["provider"], "local");
|
||||
assert_eq!(results[0]["sandbox"]["id"], "sandbox-preserve-1");
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{run_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(response, StatusCode::NOT_FOUND).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_rejects_invalid_requests_before_mutating_runs() {
|
||||
let state = test_app_state();
|
||||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let run_id = RunId::new();
|
||||
create_succeeded_run(&state, run_id).await;
|
||||
let too_many_ids = (0..251)
|
||||
.map(|_| RunId::new().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let invalid_requests = [
|
||||
json!({ "run_ids": [], "force": false }),
|
||||
json!({ "run_ids": [run_id.to_string(), run_id.to_string()], "force": false }),
|
||||
json!({ "run_ids": ["not-a-run-id"], "force": false }),
|
||||
json!({ "run_ids": too_many_ids, "force": false }),
|
||||
];
|
||||
|
||||
for body in invalid_requests {
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(json_request(Method::POST, "/runs/delete", &body))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(response, StatusCode::BAD_REQUEST).await;
|
||||
}
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{run_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(response, StatusCode::OK).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_delete_requires_user_authentication() {
|
||||
let (_state, app) = jwt_auth_app();
|
||||
let user_jwt = issue_test_user_jwt();
|
||||
let run_id = create_run_with_bearer(&app, &user_jwt).await;
|
||||
let worker_token = issue_test_worker_token(&run_id);
|
||||
let body = batch_delete_body(&[run_id], false);
|
||||
|
||||
let unauthenticated = app
|
||||
.clone()
|
||||
.oneshot(json_request(Method::POST, "/runs/delete", &body))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_status!(unauthenticated, StatusCode::UNAUTHORIZED).await;
|
||||
|
||||
let worker_response = app
|
||||
.oneshot(json_bearer_request(
|
||||
Method::POST,
|
||||
"/runs/delete",
|
||||
&worker_token,
|
||||
&body,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(
|
||||
worker_response.status(),
|
||||
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN
|
||||
),
|
||||
"/runs/delete unexpectedly accepted worker token with status {}",
|
||||
worker_response.status()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn archive_unknown_run_returns_not_found() {
|
||||
let app = test_app_with();
|
||||
|
|
@ -10968,48 +11282,7 @@ async fn delete_run_with_preserved_sandbox_returns_handoff() {
|
|||
let state = test_app_state();
|
||||
let app = crate::test_support::build_test_router(Arc::clone(&state));
|
||||
let run_id = RunId::new();
|
||||
let mut settings = fabro_types::WorkflowSettings::default();
|
||||
settings.run.environment.lifecycle.preserve = true;
|
||||
let graph = Graph::new("test");
|
||||
|
||||
create_durable_run_with_events(&state, run_id, &[
|
||||
workflow_event::Event::RunCreated {
|
||||
run_id,
|
||||
title: None,
|
||||
settings: serde_json::to_value(settings).unwrap(),
|
||||
graph: serde_json::to_value(graph).unwrap(),
|
||||
workflow_source: None,
|
||||
workflow_config: None,
|
||||
labels: std::collections::BTreeMap::default(),
|
||||
run_dir: "/tmp/fabro-run".to_string(),
|
||||
source_directory: Some("/tmp/fabro-run".to_string()),
|
||||
workflow_slug: Some("test".to_string()),
|
||||
db_prefix: None,
|
||||
provenance: None,
|
||||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
retried_from: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
workflow_event::Event::RunSubmitted {
|
||||
definition_blob: None,
|
||||
},
|
||||
workflow_event::Event::SandboxInitialized {
|
||||
provider: SandboxProvider::Local,
|
||||
id: "sandbox-preserve-1".to_string(),
|
||||
working_directory: "/tmp/fabro-preserved-sandbox".to_string(),
|
||||
repo_cloned: None,
|
||||
clone_origin_url: None,
|
||||
clone_branch: None,
|
||||
workspace_root: None,
|
||||
repos_root: None,
|
||||
primary_repo_path: None,
|
||||
primary_repo_link: None,
|
||||
},
|
||||
])
|
||||
.await;
|
||||
create_preserved_local_sandbox_run(&state, run_id).await;
|
||||
|
||||
let req = Request::builder()
|
||||
.method("DELETE")
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ models/auth-session-user.ts
|
|||
models/auth-session.ts
|
||||
models/auth-sessions-response.ts
|
||||
models/automation-ref.ts
|
||||
models/batch-delete-runs-request.ts
|
||||
models/batch-delete-runs-response.ts
|
||||
models/batch-delete-runs-result.ts
|
||||
models/batch-delete-runs-summary.ts
|
||||
models/batch-run-lifecycle-request.ts
|
||||
models/batch-run-lifecycle-response.ts
|
||||
models/batch-run-lifecycle-result.ts
|
||||
|
|
|
|||
79
lib/packages/fabro-api-client/src/api/runs-api.ts
generated
79
lib/packages/fabro-api-client/src/api/runs-api.ts
generated
|
|
@ -22,6 +22,10 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
|
|||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||
// @ts-ignore
|
||||
import type { BatchDeleteRunsRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { BatchDeleteRunsResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { BatchRunLifecycleRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { BatchRunLifecycleResponse } from '../models';
|
||||
|
|
@ -201,6 +205,47 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Deletes up to 250 runs in one fail-soft, non-transactional request. Each run is processed independently. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Delete Runs
|
||||
* @param {BatchDeleteRunsRequest} batchDeleteRunsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
batchDeleteRuns: async (batchDeleteRunsRequest: BatchDeleteRunsRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'batchDeleteRunsRequest' is not null or undefined
|
||||
assertParamExists('batchDeleteRuns', 'batchDeleteRunsRequest', batchDeleteRunsRequest)
|
||||
const localVarPath = `/api/v1/runs/delete`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(batchDeleteRunsRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Restores up to 250 archived runs in one fail-soft, non-transactional request. Each run is processed independently and successful items emit the same per-run unarchive events as `POST /api/v1/runs/{id}/unarchive`. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Unarchive Runs
|
||||
|
|
@ -1535,6 +1580,19 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['RunsApi.batchArchiveRuns']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Deletes up to 250 runs in one fail-soft, non-transactional request. Each run is processed independently. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Delete Runs
|
||||
* @param {BatchDeleteRunsRequest} batchDeleteRunsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async batchDeleteRuns(batchDeleteRunsRequest: BatchDeleteRunsRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<BatchDeleteRunsResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.batchDeleteRuns(batchDeleteRunsRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.batchDeleteRuns']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Restores up to 250 archived runs in one fail-soft, non-transactional request. Each run is processed independently and successful items emit the same per-run unarchive events as `POST /api/v1/runs/{id}/unarchive`. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Unarchive Runs
|
||||
|
|
@ -1981,6 +2039,16 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
batchArchiveRuns(batchRunLifecycleRequest: BatchRunLifecycleRequest, options?: RawAxiosRequestConfig): AxiosPromise<BatchRunLifecycleResponse> {
|
||||
return localVarFp.batchArchiveRuns(batchRunLifecycleRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Deletes up to 250 runs in one fail-soft, non-transactional request. Each run is processed independently. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Delete Runs
|
||||
* @param {BatchDeleteRunsRequest} batchDeleteRunsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
batchDeleteRuns(batchDeleteRunsRequest: BatchDeleteRunsRequest, options?: RawAxiosRequestConfig): AxiosPromise<BatchDeleteRunsResponse> {
|
||||
return localVarFp.batchDeleteRuns(batchDeleteRunsRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Restores up to 250 archived runs in one fail-soft, non-transactional request. Each run is processed independently and successful items emit the same per-run unarchive events as `POST /api/v1/runs/{id}/unarchive`. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Unarchive Runs
|
||||
|
|
@ -2338,6 +2406,17 @@ export class RunsApi extends BaseAPI {
|
|||
return RunsApiFp(this.configuration).batchArchiveRuns(batchRunLifecycleRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes up to 250 runs in one fail-soft, non-transactional request. Each run is processed independently. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Delete Runs
|
||||
* @param {BatchDeleteRunsRequest} batchDeleteRunsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public batchDeleteRuns(batchDeleteRunsRequest: BatchDeleteRunsRequest, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).batchDeleteRuns(batchDeleteRunsRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores up to 250 archived runs in one fail-soft, non-transactional request. Each run is processed independently and successful items emit the same per-run unarchive events as `POST /api/v1/runs/{id}/unarchive`. A valid batch returns `200` even when some items fail; inspect `results` and `summary` for per-run outcomes. Invalid request bodies are rejected before mutating any run.
|
||||
* @summary Unarchive Runs
|
||||
|
|
|
|||
29
lib/packages/fabro-api-client/src/models/batch-delete-runs-request.ts
generated
Normal file
29
lib/packages/fabro-api-client/src/models/batch-delete-runs-request.ts
generated
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Run IDs to delete as one bounded fail-soft batch.
|
||||
*/
|
||||
export interface BatchDeleteRunsRequest {
|
||||
/**
|
||||
* Run IDs to process, in result order.
|
||||
*/
|
||||
'run_ids': Set<string>;
|
||||
/**
|
||||
* Whether to force deletion of active runs. Defaults to `false`.
|
||||
*/
|
||||
'force'?: boolean;
|
||||
}
|
||||
32
lib/packages/fabro-api-client/src/models/batch-delete-runs-response.ts
generated
Normal file
32
lib/packages/fabro-api-client/src/models/batch-delete-runs-response.ts
generated
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* 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 { BatchDeleteRunsResult } from './batch-delete-runs-result';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { BatchDeleteRunsSummary } from './batch-delete-runs-summary';
|
||||
|
||||
/**
|
||||
* Per-run results for a fail-soft batch delete request.
|
||||
*/
|
||||
export interface BatchDeleteRunsResponse {
|
||||
/**
|
||||
* Results ordered exactly like the request `run_ids`.
|
||||
*/
|
||||
'results': Array<BatchDeleteRunsResult>;
|
||||
'summary': BatchDeleteRunsSummary;
|
||||
}
|
||||
57
lib/packages/fabro-api-client/src/models/batch-delete-runs-result.ts
generated
Normal file
57
lib/packages/fabro-api-client/src/models/batch-delete-runs-result.ts
generated
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* 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 { DeleteRunSandbox } from './delete-run-sandbox';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { ErrorResponseEntry } from './error-response-entry';
|
||||
|
||||
/**
|
||||
* Result for one run in a batch delete request.
|
||||
*/
|
||||
export interface BatchDeleteRunsResult {
|
||||
/**
|
||||
* Run ID from the request item.
|
||||
*/
|
||||
'run_id': string;
|
||||
/**
|
||||
* Whether this item succeeded.
|
||||
*/
|
||||
'ok': boolean;
|
||||
/**
|
||||
* Machine-readable item outcome.
|
||||
*/
|
||||
'outcome': BatchDeleteRunsResultOutcomeEnum;
|
||||
/**
|
||||
* Sandbox handoff details when `outcome` is `sandbox_preserved`.
|
||||
*/
|
||||
'sandbox'?: DeleteRunSandbox;
|
||||
/**
|
||||
* Structured item error for failed items.
|
||||
*/
|
||||
'error'?: ErrorResponseEntry;
|
||||
}
|
||||
|
||||
export const BatchDeleteRunsResultOutcomeEnum = {
|
||||
DELETED: 'deleted',
|
||||
ALREADY_ABSENT: 'already_absent',
|
||||
SANDBOX_PRESERVED: 'sandbox_preserved',
|
||||
CONFLICT: 'conflict',
|
||||
ERROR: 'error'
|
||||
} as const;
|
||||
|
||||
export type BatchDeleteRunsResultOutcomeEnum = typeof BatchDeleteRunsResultOutcomeEnum[keyof typeof BatchDeleteRunsResultOutcomeEnum];
|
||||
33
lib/packages/fabro-api-client/src/models/batch-delete-runs-summary.ts
generated
Normal file
33
lib/packages/fabro-api-client/src/models/batch-delete-runs-summary.ts
generated
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Aggregate counts for a batch delete request.
|
||||
*/
|
||||
export interface BatchDeleteRunsSummary {
|
||||
/**
|
||||
* Number of requested run IDs.
|
||||
*/
|
||||
'requested': number;
|
||||
/**
|
||||
* Number of item results with `ok=true`.
|
||||
*/
|
||||
'succeeded': number;
|
||||
/**
|
||||
* Number of item results with `ok=false`.
|
||||
*/
|
||||
'failed': number;
|
||||
}
|
||||
|
|
@ -22,6 +22,10 @@ export * from './auth-session';
|
|||
export * from './auth-session-user';
|
||||
export * from './auth-sessions-response';
|
||||
export * from './automation-ref';
|
||||
export * from './batch-delete-runs-request';
|
||||
export * from './batch-delete-runs-response';
|
||||
export * from './batch-delete-runs-result';
|
||||
export * from './batch-delete-runs-summary';
|
||||
export * from './batch-run-lifecycle-request';
|
||||
export * from './batch-run-lifecycle-response';
|
||||
export * from './batch-run-lifecycle-result';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue