mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
feat(runs): add parent run links (#271)
## Summary
Adds orchestration-only parent links between runs without merging them
into fork or rewind lineage. Runs can now be created under a parent,
linked to a different parent, or unlinked through event-sourced
mutations that rebuild summaries and projections from the run event
stream.
## Changes
- Adds optional `parent_id` to run manifests, public run summaries, run
projections, `run.created`, OpenAPI, and the generated TypeScript API
client.
- Adds `PUT /api/v1/runs/{id}/parent` and `DELETE
/api/v1/runs/{id}/parent` for mutable parent links across any run state,
including terminal or archived runs.
- Records `run.parent.linked` and `run.parent.unlinked` events with
actor metadata and previous/current parent IDs.
- Validates parent changes in the API path: parent must exist for new
links, self-parenting is rejected, cycles are rejected, and same-parent
or already-root operations are idempotent no-ops.
- Adds `parent_id` filtering to run listing while preserving dangling
historical parent references after parent deletion.
## Validation
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo build -p fabro-api`
- `cargo check -p fabro-workflow -p fabro-store -p fabro-server`
- `cargo nextest run -p fabro-types -p fabro-store`
- `cargo nextest run -p fabro-server --features test-support
create_run_can_set_parent_and_list_children
link_relink_and_unlink_parent_are_idempotent
parent_link_validation_rejects_missing_self_and_cycles
deleting_parent_leaves_child_parent_id_as_historical_reference`
- `cargo nextest run -p fabro-api
run_summary_json_matches_openapi_shape`
- `cd lib/packages/fabro-api-client && bun run typecheck`
Known unrelated broad-suite blocker: `cargo nextest run -p fabro-server
--features test-support get_graph_returns_svg` currently returns 500
because the render subprocess emits test-harness output instead of SVG.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, medium reasoning) via
[Codex](https://openai.com/codex)
This commit is contained in:
parent
ae55bded81
commit
2296ba6ea8
51 changed files with 1161 additions and 44 deletions
|
|
@ -823,6 +823,7 @@ paths:
|
|||
- $ref: "#/components/parameters/PageLimit"
|
||||
- $ref: "#/components/parameters/PageOffset"
|
||||
- $ref: "#/components/parameters/IncludeArchived"
|
||||
- $ref: "#/components/parameters/ParentRunId"
|
||||
responses:
|
||||
"200":
|
||||
description: Paginated durable run summaries
|
||||
|
|
@ -1078,6 +1079,69 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/{id}/parent:
|
||||
put:
|
||||
operationId: linkRunParent
|
||||
tags: [Runs]
|
||||
summary: Link Run Parent
|
||||
description: Links a run under an orchestration parent. Parent links are mutable for all run states, including archived and terminal runs.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RunId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UpdateRunParentRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated durable run summary
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Run"
|
||||
"400":
|
||||
description: Self-parent or cycle rejected
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"404":
|
||||
description: Child or parent run not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
delete:
|
||||
operationId: unlinkRunParent
|
||||
tags: [Runs]
|
||||
summary: Unlink Run Parent
|
||||
description: Removes a run's orchestration parent. Already-root runs are returned unchanged.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RunId"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated durable run summary
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Run"
|
||||
"404":
|
||||
description: Run not found
|
||||
headers:
|
||||
x-request-id:
|
||||
$ref: "#/components/headers/XRequestId"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/{id}/cancel:
|
||||
post:
|
||||
operationId: cancelRun
|
||||
|
|
@ -3771,6 +3835,15 @@ components:
|
|||
default: false
|
||||
example: false
|
||||
|
||||
ParentRunId:
|
||||
name: parent_id
|
||||
in: query
|
||||
required: false
|
||||
description: Return only runs currently linked to this orchestration parent.
|
||||
schema:
|
||||
type: string
|
||||
example: 01JNQVR7M0EJ5GKAT2SC4ERS1Z
|
||||
|
||||
ForceRunDelete:
|
||||
name: force
|
||||
in: query
|
||||
|
|
@ -5062,6 +5135,10 @@ components:
|
|||
type: ["string", "null"]
|
||||
description: Optional pre-generated run ID to use instead of allocating a new ULID.
|
||||
example: "01HV6D7S5YF4Z4B2M7K4N0Q6T9"
|
||||
parent_id:
|
||||
type: ["string", "null"]
|
||||
description: Optional orchestration parent run ID. Fork and rewind lineage use separate fields and should not set this value.
|
||||
example: "01HV6D7S5YF4Z4B2M7K4N0Q6T8"
|
||||
title:
|
||||
type: ["string", "null"]
|
||||
maxLength: 100
|
||||
|
|
@ -6484,6 +6561,15 @@ components:
|
|||
- $ref: "#/components/schemas/ForkSourceRef"
|
||||
- type: "null"
|
||||
|
||||
UpdateRunParentRequest:
|
||||
type: object
|
||||
required:
|
||||
- parent_id
|
||||
properties:
|
||||
parent_id:
|
||||
type: string
|
||||
description: Existing orchestration parent run ID.
|
||||
|
||||
StartRecord:
|
||||
description: Metadata captured when execution starts.
|
||||
type: object
|
||||
|
|
@ -6595,6 +6681,9 @@ components:
|
|||
title:
|
||||
type: string
|
||||
description: Resolved run title from the event log.
|
||||
parent_id:
|
||||
type: ["string", "null"]
|
||||
description: Current orchestration parent run ID, if linked.
|
||||
spec:
|
||||
$ref: "#/components/schemas/RunSpec"
|
||||
web_url:
|
||||
|
|
@ -6675,6 +6764,9 @@ components:
|
|||
properties:
|
||||
id:
|
||||
type: string
|
||||
parent_id:
|
||||
type: ["string", "null"]
|
||||
description: Current orchestration parent run ID, if linked.
|
||||
title:
|
||||
type: string
|
||||
goal:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ fn run_summary_json_matches_openapi_shape() {
|
|||
let archived_at = Utc.with_ymd_and_hms(2026, 4, 20, 12, 1, 0).unwrap();
|
||||
let summary = RunSummary {
|
||||
id: run_id,
|
||||
parent_id: None,
|
||||
title: "API title".to_string(),
|
||||
goal: String::new(),
|
||||
workflow: WorkflowRef {
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ pub fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManifest> {
|
|||
cwd: input.cwd.display().to_string(),
|
||||
git,
|
||||
goal,
|
||||
parent_id: None,
|
||||
run_id: input.run_id.map(|run_id| run_id.to_string()),
|
||||
title: None,
|
||||
target: types::ManifestTarget {
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ mod tests {
|
|||
let created_at = Utc.with_ymd_and_hms(2026, 5, 11, 12, 0, seconds).unwrap();
|
||||
Run {
|
||||
id: id.parse().expect("test run id should parse"),
|
||||
parent_id: None,
|
||||
title: "test".to_string(),
|
||||
goal: "test".to_string(),
|
||||
workflow: WorkflowRef {
|
||||
|
|
|
|||
|
|
@ -1056,6 +1056,7 @@ mod runs {
|
|||
let duration_ms = elapsed_secs.and_then(duration_ms_from_secs);
|
||||
RunSummary {
|
||||
id: run_id,
|
||||
parent_id: None,
|
||||
title: fabro_types::infer_run_title(goal),
|
||||
goal: goal.into(),
|
||||
workflow: WorkflowRef {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ pub(crate) struct PreparedManifest {
|
|||
pub git: Option<types::GitContext>,
|
||||
pub root_source: String,
|
||||
pub run_id: Option<RunId>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub title: Option<String>,
|
||||
pub settings: WorkflowSettings,
|
||||
pub target_path: ManifestPath,
|
||||
|
|
@ -155,6 +156,12 @@ pub(crate) fn prepare_manifest(
|
|||
.map(str::parse::<RunId>)
|
||||
.transpose()
|
||||
.context("invalid run ID")?,
|
||||
parent_id: manifest
|
||||
.parent_id
|
||||
.as_deref()
|
||||
.map(str::parse::<RunId>)
|
||||
.transpose()
|
||||
.context("invalid parent run ID")?,
|
||||
title,
|
||||
settings: settings.clone(),
|
||||
target_path,
|
||||
|
|
@ -196,6 +203,7 @@ pub(crate) fn create_run_input(
|
|||
title: prepared.title,
|
||||
git: prepared.git,
|
||||
fork_source_ref: None,
|
||||
parent_id: prepared.parent_id,
|
||||
provenance: None,
|
||||
configured_providers,
|
||||
web_url,
|
||||
|
|
@ -1325,6 +1333,7 @@ mod tests {
|
|||
cwd: "/tmp/project".to_string(),
|
||||
git: None,
|
||||
goal: None,
|
||||
parent_id: None,
|
||||
run_id: None,
|
||||
title: None,
|
||||
target: types::ManifestTarget {
|
||||
|
|
|
|||
|
|
@ -525,6 +525,7 @@ pub struct AppState {
|
|||
/// proceed in parallel. See `crate::run_files` for semantics.
|
||||
pub(crate) files_in_flight: FilesInFlight,
|
||||
pull_request_create_locks: PullRequestCreateLocks,
|
||||
parent_link_lock: AsyncMutex<()>,
|
||||
|
||||
pub(crate) vault: Arc<AsyncRwLock<Vault>>,
|
||||
pub(super) server_secrets: ServerSecrets,
|
||||
|
|
@ -1590,6 +1591,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
global_event_tx,
|
||||
files_in_flight: new_files_in_flight(),
|
||||
pull_request_create_locks: Arc::new(Mutex::new(HashMap::new())),
|
||||
parent_link_lock: AsyncMutex::new(()),
|
||||
vault,
|
||||
server_secrets,
|
||||
llm_source,
|
||||
|
|
|
|||
|
|
@ -441,6 +441,7 @@ mod stage_events_tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::HashSet;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -5,13 +6,14 @@ use std::sync::Arc;
|
|||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{HeaderMap, StatusCode, header};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::routing::{get, post, put};
|
||||
use axum::{Json, Router};
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use bytes::Bytes;
|
||||
use fabro_api::types::{
|
||||
BoardColumn, BoardColumnDefinition, RunManifest, SubmitAnswerRequest, UpdateRunRequest,
|
||||
BoardColumn, BoardColumnDefinition, RunManifest, SubmitAnswerRequest, UpdateRunParentRequest,
|
||||
UpdateRunRequest,
|
||||
};
|
||||
use fabro_config::Storage;
|
||||
use fabro_interview::AnswerSubmission;
|
||||
|
|
@ -56,6 +58,10 @@ pub(super) fn routes() -> Router<Arc<AppState>> {
|
|||
"/runs/{id}",
|
||||
get(get_run_status).patch(update_run).delete(delete_run),
|
||||
)
|
||||
.route(
|
||||
"/runs/{id}/parent",
|
||||
put(link_run_parent).delete(unlink_run_parent),
|
||||
)
|
||||
.route("/runs/{id}/questions", get(get_questions))
|
||||
.route("/runs/{id}/questions/{qid}/answer", post(submit_answer))
|
||||
.route("/runs/{id}/state", get(get_run_state))
|
||||
|
|
@ -78,6 +84,8 @@ struct ListRunsParams {
|
|||
offset: u32,
|
||||
#[serde(default)]
|
||||
include_archived: bool,
|
||||
#[serde(default)]
|
||||
parent_id: Option<RunId>,
|
||||
}
|
||||
|
||||
impl ListRunsParams {
|
||||
|
|
@ -156,7 +164,10 @@ async fn list_board_runs(
|
|||
) -> Response {
|
||||
let entries = match state
|
||||
.store
|
||||
.list_cached_runs(&fabro_store::ListRunsQuery::default())
|
||||
.list_cached_runs(&fabro_store::ListRunsQuery {
|
||||
parent_id: params.parent_id,
|
||||
..fabro_store::ListRunsQuery::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(runs) => runs,
|
||||
|
|
@ -195,6 +206,140 @@ async fn list_board_runs(
|
|||
.into_response()
|
||||
}
|
||||
|
||||
async fn link_run_parent(
|
||||
subject: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<UpdateRunParentRequest>,
|
||||
) -> Response {
|
||||
let child_id = match parse_run_id_path(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let parent_id = match req.parent_id.parse::<RunId>() {
|
||||
Ok(parent_id) => parent_id,
|
||||
Err(err) => {
|
||||
return ApiError::bad_request(format!("invalid parent run ID: {err}")).into_response();
|
||||
}
|
||||
};
|
||||
let _parent_link_guard = state.parent_link_lock.lock().await;
|
||||
let child = match state.store.get_cached_summary(&child_id).await {
|
||||
Ok(Some(summary)) => summary,
|
||||
Ok(None) => return ApiError::not_found("Run not found.").into_response(),
|
||||
Err(err) => {
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if parent_id == child_id {
|
||||
return ApiError::bad_request("A run cannot be its own parent.").into_response();
|
||||
}
|
||||
if let Err(err) = validate_parent_link(&state, child_id, parent_id).await {
|
||||
return err.into_response();
|
||||
}
|
||||
if child.parent_id == Some(parent_id) {
|
||||
return (StatusCode::OK, Json(child)).into_response();
|
||||
}
|
||||
|
||||
let Ok(run_store) = state.store.open_run(&child_id).await else {
|
||||
return ApiError::not_found("Run not found.").into_response();
|
||||
};
|
||||
if let Err(err) = workflow_event::append_event(
|
||||
&run_store,
|
||||
&child_id,
|
||||
&workflow_event::Event::RunParentLinked {
|
||||
previous_parent_id: child.parent_id,
|
||||
parent_id,
|
||||
actor: Some(Principal::User(subject.0)),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response();
|
||||
}
|
||||
updated_run_response(&state, &child_id).await
|
||||
}
|
||||
|
||||
async fn unlink_run_parent(
|
||||
subject: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
let child_id = match parse_run_id_path(&id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return err.into_response(),
|
||||
};
|
||||
let _parent_link_guard = state.parent_link_lock.lock().await;
|
||||
let child = match state.store.get_cached_summary(&child_id).await {
|
||||
Ok(Some(summary)) => summary,
|
||||
Ok(None) => return ApiError::not_found("Run not found.").into_response(),
|
||||
Err(err) => {
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let Some(previous_parent_id) = child.parent_id else {
|
||||
return (StatusCode::OK, Json(child)).into_response();
|
||||
};
|
||||
|
||||
let Ok(run_store) = state.store.open_run(&child_id).await else {
|
||||
return ApiError::not_found("Run not found.").into_response();
|
||||
};
|
||||
if let Err(err) = workflow_event::append_event(
|
||||
&run_store,
|
||||
&child_id,
|
||||
&workflow_event::Event::RunParentUnlinked {
|
||||
previous_parent_id,
|
||||
actor: Some(Principal::User(subject.0)),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response();
|
||||
}
|
||||
updated_run_response(&state, &child_id).await
|
||||
}
|
||||
|
||||
async fn validate_parent_link(
|
||||
state: &AppState,
|
||||
child_id: RunId,
|
||||
parent_id: RunId,
|
||||
) -> Result<(), ApiError> {
|
||||
let mut cursor = Some(parent_id);
|
||||
let mut visited = HashSet::new();
|
||||
while let Some(current_id) = cursor {
|
||||
if current_id == child_id {
|
||||
return Err(ApiError::bad_request("Parent link would create a cycle."));
|
||||
}
|
||||
if !visited.insert(current_id) {
|
||||
return Err(ApiError::bad_request("Parent link would create a cycle."));
|
||||
}
|
||||
let summary = state
|
||||
.store
|
||||
.get_cached_summary(¤t_id)
|
||||
.await
|
||||
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
let Some(summary) = summary else {
|
||||
if current_id == parent_id {
|
||||
return Err(ApiError::not_found("Parent run not found."));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
cursor = summary.parent_id;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn updated_run_response(state: &AppState, run_id: &RunId) -> Response {
|
||||
match state.store.get_cached_summary(run_id).await {
|
||||
Ok(Some(summary)) => (StatusCode::OK, Json(summary)).into_response(),
|
||||
Ok(None) => ApiError::not_found("Run not found.").into_response(),
|
||||
Err(err) => {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_runs(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -202,7 +347,10 @@ async fn list_runs(
|
|||
) -> Response {
|
||||
match state
|
||||
.store
|
||||
.list_cached_runs(&fabro_store::ListRunsQuery::default())
|
||||
.list_cached_runs(&fabro_store::ListRunsQuery {
|
||||
parent_id: params.parent_id,
|
||||
..fabro_store::ListRunsQuery::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(entries) => {
|
||||
|
|
@ -399,6 +547,14 @@ async fn create_run(
|
|||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let run_id = prepared.run_id.unwrap_or_else(RunId::new);
|
||||
if let Some(parent_id) = prepared.parent_id {
|
||||
if parent_id == run_id {
|
||||
return ApiError::bad_request("A run cannot be its own parent.").into_response();
|
||||
}
|
||||
if let Err(err) = validate_parent_link(&state, run_id, parent_id).await {
|
||||
return err.into_response();
|
||||
}
|
||||
}
|
||||
info!(run_id = %run_id, "Run created");
|
||||
|
||||
let web_url = state.run_web_url(&run_id);
|
||||
|
|
|
|||
|
|
@ -2454,6 +2454,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -2801,6 +2802,7 @@ async fn list_run_stages_distinguishes_visits() {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
workflow_event::Event::RunStarting,
|
||||
|
|
@ -3781,6 +3783,7 @@ async fn create_completed_run_ready_for_pull_request(
|
|||
manifest_blob: None,
|
||||
git,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
workflow_event::Event::WorkflowRunStarted {
|
||||
|
|
@ -8262,6 +8265,7 @@ async fn delete_run_with_preserved_sandbox_returns_handoff() {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
workflow_event::Event::RunSubmitted {
|
||||
|
|
@ -8328,6 +8332,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
workflow_event::Event::RunSubmitted {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ async fn append_completed_run_with_final_patch(
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -3,10 +3,55 @@ use axum::http::{Request, StatusCode};
|
|||
use tower::ServiceExt;
|
||||
|
||||
use crate::helpers::{
|
||||
MINIMAL_DOT, api, body_json, minimal_manifest_json, response_json, settings_from_toml,
|
||||
test_app_state_with_options,
|
||||
MINIMAL_DOT, api, body_json, minimal_manifest_json, response_json, response_status,
|
||||
settings_from_toml, test_app_state_with_options,
|
||||
};
|
||||
|
||||
async fn create_run(app: &axum::Router, mut manifest: serde_json::Value) -> serde_json::Value {
|
||||
manifest
|
||||
.as_object_mut()
|
||||
.expect("manifest should be an object")
|
||||
.entry("configs")
|
||||
.or_insert_with(|| serde_json::json!([]));
|
||||
let request = Request::builder()
|
||||
.method("POST")
|
||||
.uri(api("/runs"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&manifest).expect("manifest should serialize"),
|
||||
))
|
||||
.expect("create run request should build");
|
||||
response_json(
|
||||
app.clone().oneshot(request).await.unwrap(),
|
||||
StatusCode::CREATED,
|
||||
"POST /api/v1/runs",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn request_json(
|
||||
app: &axum::Router,
|
||||
method: &str,
|
||||
path: String,
|
||||
body: serde_json::Value,
|
||||
expected: StatusCode,
|
||||
) -> serde_json::Value {
|
||||
let request = Request::builder()
|
||||
.method(method)
|
||||
.uri(api(&path))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&body).expect("request body should serialize"),
|
||||
))
|
||||
.expect("JSON request should build");
|
||||
response_json(
|
||||
app.clone().oneshot(request).await.unwrap(),
|
||||
expected,
|
||||
format!("{method} /api/v1{path}"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retrieve_run_settings_returns_dense_snapshot() {
|
||||
let storage_dir = tempfile::tempdir().unwrap();
|
||||
|
|
@ -94,3 +139,221 @@ session_sandboxes = true
|
|||
assert!(body.pointer("/features").is_none());
|
||||
assert!(body.pointer("/server").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_run_can_set_parent_and_list_children() {
|
||||
let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
|
||||
let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
|
||||
let parent_id = parent["id"].as_str().unwrap();
|
||||
let mut child_manifest = minimal_manifest_json(MINIMAL_DOT);
|
||||
child_manifest["parent_id"] = serde_json::json!(parent_id);
|
||||
|
||||
let child = create_run(&app, child_manifest).await;
|
||||
let child_id = child["id"].as_str().unwrap();
|
||||
|
||||
assert_eq!(child["parent_id"], parent_id);
|
||||
let list_request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs?parent_id={parent_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let list = response_json(
|
||||
app.clone().oneshot(list_request).await.unwrap(),
|
||||
StatusCode::OK,
|
||||
"GET /api/v1/runs?parent_id",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(list["data"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(list["data"][0]["id"], child_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn link_relink_and_unlink_parent_are_idempotent() {
|
||||
let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
|
||||
let parent_1 = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
|
||||
let parent_2 = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
|
||||
let child = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
|
||||
let parent_1_id = parent_1["id"].as_str().unwrap();
|
||||
let parent_2_id = parent_2["id"].as_str().unwrap();
|
||||
let child_id = child["id"].as_str().unwrap();
|
||||
|
||||
let linked = request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{child_id}/parent"),
|
||||
serde_json::json!({ "parent_id": parent_1_id }),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(linked["parent_id"], parent_1_id);
|
||||
|
||||
let linked_again = request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{child_id}/parent"),
|
||||
serde_json::json!({ "parent_id": parent_1_id }),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(linked_again["parent_id"], parent_1_id);
|
||||
|
||||
let relinked = request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{child_id}/parent"),
|
||||
serde_json::json!({ "parent_id": parent_2_id }),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(relinked["parent_id"], parent_2_id);
|
||||
|
||||
let events_request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{child_id}/events")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let events = response_json(
|
||||
app.clone().oneshot(events_request).await.unwrap(),
|
||||
StatusCode::OK,
|
||||
format!("GET /api/v1/runs/{child_id}/events"),
|
||||
)
|
||||
.await;
|
||||
let event_names = events["data"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|event| event["event"].as_str().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(event_names, vec![
|
||||
"run.created",
|
||||
"run.submitted",
|
||||
"run.parent.linked",
|
||||
"run.parent.linked"
|
||||
]);
|
||||
|
||||
let unlink_request = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(api(&format!("/runs/{child_id}/parent")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let unlinked = response_json(
|
||||
app.clone().oneshot(unlink_request).await.unwrap(),
|
||||
StatusCode::OK,
|
||||
format!("DELETE /api/v1/runs/{child_id}/parent"),
|
||||
)
|
||||
.await;
|
||||
assert!(unlinked["parent_id"].is_null());
|
||||
|
||||
let unlink_again = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(api(&format!("/runs/{child_id}/parent")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let unlinked_again = response_json(
|
||||
app.clone().oneshot(unlink_again).await.unwrap(),
|
||||
StatusCode::OK,
|
||||
format!("DELETE /api/v1/runs/{child_id}/parent"),
|
||||
)
|
||||
.await;
|
||||
assert!(unlinked_again["parent_id"].is_null());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_parent_leaves_child_parent_id_as_historical_reference() {
|
||||
let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
|
||||
let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
|
||||
let parent_id = parent["id"].as_str().unwrap();
|
||||
let mut child_manifest = minimal_manifest_json(MINIMAL_DOT);
|
||||
child_manifest["parent_id"] = serde_json::json!(parent_id);
|
||||
let child = create_run(&app, child_manifest).await;
|
||||
let child_id = child["id"].as_str().unwrap();
|
||||
|
||||
let delete_request = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(api(&format!("/runs/{parent_id}?force=true")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
crate::helpers::checked_response_in(
|
||||
app.clone().oneshot(delete_request).await.unwrap(),
|
||||
&[StatusCode::OK, StatusCode::NO_CONTENT],
|
||||
format!("DELETE /api/v1/runs/{parent_id}?force=true"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let get_child = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{child_id}")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let child_after_delete = response_json(
|
||||
app.clone().oneshot(get_child).await.unwrap(),
|
||||
StatusCode::OK,
|
||||
format!("GET /api/v1/runs/{child_id}"),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(child_after_delete["parent_id"], parent_id);
|
||||
|
||||
request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{child_id}/parent"),
|
||||
serde_json::json!({ "parent_id": parent_id }),
|
||||
StatusCode::NOT_FOUND,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parent_link_validation_rejects_missing_self_and_cycles() {
|
||||
let app = fabro_server::test_support::build_test_router(crate::helpers::test_app_state());
|
||||
let parent = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
|
||||
let child = create_run(&app, minimal_manifest_json(MINIMAL_DOT)).await;
|
||||
let parent_id = parent["id"].as_str().unwrap();
|
||||
let child_id = child["id"].as_str().unwrap();
|
||||
|
||||
request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{child_id}/parent"),
|
||||
serde_json::json!({ "parent_id": parent_id }),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
|
||||
request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{parent_id}/parent"),
|
||||
serde_json::json!({ "parent_id": child_id }),
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
.await;
|
||||
request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{child_id}/parent"),
|
||||
serde_json::json!({ "parent_id": child_id }),
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
.await;
|
||||
request_json(
|
||||
&app,
|
||||
"PUT",
|
||||
format!("/runs/{child_id}/parent"),
|
||||
serde_json::json!({ "parent_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" }),
|
||||
StatusCode::NOT_FOUND,
|
||||
)
|
||||
.await;
|
||||
|
||||
let missing_child = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(api("/runs/01ARZ3NDEKTSV4RRFFQ69G5FAV/parent"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
response_status(
|
||||
app.oneshot(missing_child).await.unwrap(),
|
||||
StatusCode::NOT_FOUND,
|
||||
"DELETE /api/v1/runs/{missing}/parent",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ pub use types::EventPayload;
|
|||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct ListRunsQuery {
|
||||
pub start: Option<DateTime<Utc>>,
|
||||
pub end: Option<DateTime<Utc>>,
|
||||
pub start: Option<DateTime<Utc>>,
|
||||
pub end: Option<DateTime<Utc>>,
|
||||
pub parent_id: Option<fabro_types::RunId>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,6 +160,12 @@ impl RunProjectionReducer for RunProjection {
|
|||
EventBody::RunSupersededBy(props) => {
|
||||
self.superseded_by = Some(props.new_run_id);
|
||||
}
|
||||
EventBody::RunParentLinked(props) => {
|
||||
self.parent_id = Some(props.parent_id);
|
||||
}
|
||||
EventBody::RunParentUnlinked(_props) => {
|
||||
self.parent_id = None;
|
||||
}
|
||||
EventBody::RunArchived(_props) => {
|
||||
if self.archived_at.is_some() {
|
||||
return Ok(());
|
||||
|
|
@ -524,6 +530,7 @@ fn projection_from_created(event: &EventEnvelope) -> Result<RunProjection> {
|
|||
};
|
||||
|
||||
let mut projection = RunProjection::new(title, spec, stored.ts);
|
||||
projection.parent_id = props.parent_id;
|
||||
projection.web_url.clone_from(&props.web_url);
|
||||
projection.sandbox = Some(planned_sandbox(&projection.spec.settings.run.sandbox));
|
||||
Ok(projection)
|
||||
|
|
@ -671,6 +678,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> RunSummary
|
|||
|
||||
RunSummary {
|
||||
id: *run_id,
|
||||
parent_id: state.parent_id,
|
||||
title: state.title().into_owned(),
|
||||
goal,
|
||||
workflow: WorkflowRef {
|
||||
|
|
|
|||
|
|
@ -533,6 +533,32 @@ mod tests {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
async fn append_created_with_parent(
|
||||
run: &RunDatabase,
|
||||
label: &str,
|
||||
created_at: DateTime<Utc>,
|
||||
parent_id: RunId,
|
||||
) {
|
||||
let run_spec = sample_run_spec(label);
|
||||
run.append_event(&event_payload(
|
||||
label,
|
||||
&created_at.to_rfc3339(),
|
||||
"run.created",
|
||||
&serde_json::json!({
|
||||
"settings": run_spec.settings,
|
||||
"graph": run_spec.graph,
|
||||
"workflow_slug": run_spec.workflow_slug,
|
||||
"source_directory": run_spec.source_directory,
|
||||
"run_dir": format!("/tmp/{label}"),
|
||||
"git": run_spec.git,
|
||||
"labels": run_spec.labels,
|
||||
"parent_id": parent_id,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn append_completed(run: &RunDatabase, label: &str, created_at: DateTime<Utc>) {
|
||||
append_running(run, label, created_at).await;
|
||||
run.append_event(&event_payload(
|
||||
|
|
@ -673,6 +699,143 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parent_id_is_projected_from_created_and_parent_events() {
|
||||
let (_object_store, store) = make_store();
|
||||
let parent_1 = store.create_run(&test_run_id("run-1")).await.unwrap();
|
||||
let parent_2 = store.create_run(&test_run_id("run-2")).await.unwrap();
|
||||
let child = store.create_run(&test_run_id("run-3")).await.unwrap();
|
||||
append_created(&parent_1, "run-1", dt("2026-03-27T12:00:00Z")).await;
|
||||
append_created(&parent_2, "run-2", dt("2026-03-27T12:00:10Z")).await;
|
||||
append_created_with_parent(
|
||||
&child,
|
||||
"run-3",
|
||||
dt("2026-03-27T12:00:20Z"),
|
||||
test_run_id("run-1"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial = store.open_run(&test_run_id("run-3")).await.unwrap();
|
||||
assert_eq!(
|
||||
initial.state().await.unwrap().parent_id,
|
||||
Some(test_run_id("run-1"))
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.get_cached_summary(&test_run_id("run-3"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.parent_id,
|
||||
Some(test_run_id("run-1"))
|
||||
);
|
||||
|
||||
child
|
||||
.append_event(&event_payload(
|
||||
"run-3",
|
||||
"2026-03-27T12:00:21Z",
|
||||
"run.parent.linked",
|
||||
&serde_json::json!({
|
||||
"previous_parent_id": test_run_id("run-1"),
|
||||
"parent_id": test_run_id("run-2"),
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.get_cached_summary(&test_run_id("run-3"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.parent_id,
|
||||
Some(test_run_id("run-2"))
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.list_runs(&ListRunsQuery {
|
||||
parent_id: Some(test_run_id("run-1")),
|
||||
..ListRunsQuery::default()
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.list_runs(&ListRunsQuery {
|
||||
parent_id: Some(test_run_id("run-2")),
|
||||
..ListRunsQuery::default()
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|summary| summary.id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![test_run_id("run-3")]
|
||||
);
|
||||
|
||||
child
|
||||
.append_event(&event_payload(
|
||||
"run-3",
|
||||
"2026-03-27T12:00:22Z",
|
||||
"run.parent.unlinked",
|
||||
&serde_json::json!({
|
||||
"previous_parent_id": test_run_id("run-2"),
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store
|
||||
.get_cached_summary(&test_run_id("run-3"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.parent_id,
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.list_runs(&ListRunsQuery {
|
||||
parent_id: Some(test_run_id("run-2")),
|
||||
..ListRunsQuery::default()
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_runs_filters_by_parent_id() {
|
||||
let (_object_store, store) = make_store();
|
||||
let parent = store.create_run(&test_run_id("run-1")).await.unwrap();
|
||||
let child = store.create_run(&test_run_id("run-2")).await.unwrap();
|
||||
let unrelated = store.create_run(&test_run_id("run-3")).await.unwrap();
|
||||
append_created(&parent, "run-1", dt("2026-03-27T12:00:00Z")).await;
|
||||
append_created_with_parent(
|
||||
&child,
|
||||
"run-2",
|
||||
dt("2026-03-27T12:00:10Z"),
|
||||
test_run_id("run-1"),
|
||||
)
|
||||
.await;
|
||||
append_created(&unrelated, "run-3", dt("2026-03-27T12:00:20Z")).await;
|
||||
|
||||
let summaries = store
|
||||
.list_runs(&ListRunsQuery {
|
||||
parent_id: Some(test_run_id("run-1")),
|
||||
..ListRunsQuery::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(summaries.len(), 1);
|
||||
assert_eq!(summaries[0].id, test_run_id("run-2"));
|
||||
assert_eq!(summaries[0].parent_id, Some(test_run_id("run-1")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn control_effect_events_clear_pending_control_and_update_status() {
|
||||
let (_object_store, store) = make_store();
|
||||
|
|
@ -831,8 +994,9 @@ mod tests {
|
|||
|
||||
let filtered = reopened
|
||||
.list_cached_runs(&ListRunsQuery {
|
||||
start: Some(test_run_id("run-2").created_at()),
|
||||
end: Some(test_run_id("run-2").created_at() + chrono::Duration::seconds(1)),
|
||||
start: Some(test_run_id("run-2").created_at()),
|
||||
end: Some(test_run_id("run-2").created_at() + chrono::Duration::seconds(1)),
|
||||
parent_id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::sync::Arc;
|
||||
|
||||
use fabro_types::{RunId, RunProjection, RunSummary};
|
||||
|
|
@ -29,24 +29,83 @@ impl CachedRunProjection {
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct RunProjectionCache {
|
||||
entries: Mutex<HashMap<RunId, CachedRunProjection>>,
|
||||
state: Mutex<RunProjectionCacheState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RunProjectionCacheState {
|
||||
entries: HashMap<RunId, CachedRunProjection>,
|
||||
children_by_parent: HashMap<RunId, BTreeSet<RunId>>,
|
||||
}
|
||||
|
||||
impl RunProjectionCacheState {
|
||||
fn replace_all(&mut self, entries: Vec<CachedRunProjection>) {
|
||||
self.entries.clear();
|
||||
self.children_by_parent.clear();
|
||||
for entry in entries {
|
||||
self.insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(&mut self, entry: CachedRunProjection) {
|
||||
let run_id = entry.run_id;
|
||||
let parent_id = entry.summary.parent_id;
|
||||
if let Some(previous) = self.entries.insert(run_id, entry) {
|
||||
self.remove_parent_index(&previous);
|
||||
}
|
||||
if let Some(parent_id) = parent_id {
|
||||
self.children_by_parent
|
||||
.entry(parent_id)
|
||||
.or_default()
|
||||
.insert(run_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, run_id: &RunId) {
|
||||
if let Some(entry) = self.entries.remove(run_id) {
|
||||
self.remove_parent_index(&entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_parent_index(&mut self, entry: &CachedRunProjection) {
|
||||
let Some(parent_id) = entry.summary.parent_id else {
|
||||
return;
|
||||
};
|
||||
let Some(children) = self.children_by_parent.get_mut(&parent_id) else {
|
||||
return;
|
||||
};
|
||||
children.remove(&entry.run_id);
|
||||
if children.is_empty() {
|
||||
self.children_by_parent.remove(&parent_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RunProjectionCache {
|
||||
pub(crate) async fn replace_all(&self, entries: Vec<CachedRunProjection>) {
|
||||
let mut cache = self.entries.lock().await;
|
||||
cache.clear();
|
||||
cache.extend(entries.into_iter().map(|entry| (entry.run_id, entry)));
|
||||
self.state.lock().await.replace_all(entries);
|
||||
}
|
||||
|
||||
pub(crate) async fn replace(&self, entry: CachedRunProjection) {
|
||||
self.entries.lock().await.insert(entry.run_id, entry);
|
||||
self.state.lock().await.insert(entry);
|
||||
}
|
||||
|
||||
pub(crate) async fn list(&self, query: &ListRunsQuery) -> Vec<CachedRunProjection> {
|
||||
let cache = self.entries.lock().await;
|
||||
let mut entries = cache
|
||||
.values()
|
||||
let entries = {
|
||||
let state = self.state.lock().await;
|
||||
match query.parent_id {
|
||||
Some(parent_id) => state
|
||||
.children_by_parent
|
||||
.get(&parent_id)
|
||||
.into_iter()
|
||||
.flat_map(|children| children.iter())
|
||||
.filter_map(|run_id| state.entries.get(run_id).cloned())
|
||||
.collect::<Vec<_>>(),
|
||||
None => state.entries.values().cloned().collect::<Vec<_>>(),
|
||||
}
|
||||
};
|
||||
let mut entries = entries
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
let created_at = entry.run_id.created_at();
|
||||
if query.start.is_some_and(|start| created_at < start) {
|
||||
|
|
@ -57,7 +116,6 @@ impl RunProjectionCache {
|
|||
}
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_by(|left, right| {
|
||||
right
|
||||
|
|
@ -70,26 +128,26 @@ impl RunProjectionCache {
|
|||
}
|
||||
|
||||
pub(crate) async fn get(&self, run_id: &RunId) -> Option<CachedRunProjection> {
|
||||
self.entries.lock().await.get(run_id).cloned()
|
||||
self.state.lock().await.entries.get(run_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_summary(&self, run_id: &RunId) -> Option<RunSummary> {
|
||||
self.entries
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.entries
|
||||
.get(run_id)
|
||||
.map(|entry| entry.summary.clone())
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_event(&self, run_id: &RunId, event: &EventEnvelope) -> Result<()> {
|
||||
let mut cache = self.entries.lock().await;
|
||||
let Some(entry) = cache.get(run_id) else {
|
||||
let mut state = self.state.lock().await;
|
||||
let Some(entry) = state.entries.get(run_id).cloned() else {
|
||||
if event.seq == 1 {
|
||||
let projection = RunProjection::apply_events(std::slice::from_ref(event))?;
|
||||
cache.insert(
|
||||
*run_id,
|
||||
CachedRunProjection::from_projection(*run_id, projection, event.seq),
|
||||
);
|
||||
state.insert(CachedRunProjection::from_projection(
|
||||
*run_id, projection, event.seq,
|
||||
));
|
||||
} else {
|
||||
return Err(Error::InvalidEvent(format!(
|
||||
"projection cache cannot initialize run {run_id} from event seq {}",
|
||||
|
|
@ -111,14 +169,13 @@ impl RunProjectionCache {
|
|||
|
||||
let mut projection = (*entry.projection).clone();
|
||||
projection.apply_event(event)?;
|
||||
cache.insert(
|
||||
*run_id,
|
||||
CachedRunProjection::from_projection(*run_id, projection, event.seq),
|
||||
);
|
||||
state.insert(CachedRunProjection::from_projection(
|
||||
*run_id, projection, event.seq,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove(&self, run_id: &RunId) {
|
||||
self.entries.lock().await.remove(run_id);
|
||||
self.state.lock().await.remove(run_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,8 +139,9 @@ mod tests {
|
|||
assert_eq!(
|
||||
index
|
||||
.list(&ListRunsQuery {
|
||||
start: Some(second.created_at()),
|
||||
end: Some(second.created_at() + ChronoDuration::seconds(1)),
|
||||
start: Some(second.created_at()),
|
||||
end: Some(second.created_at() + ChronoDuration::seconds(1)),
|
||||
parent_id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap(),
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ pub enum EventBody {
|
|||
RunUnarchived(RunUnarchivedProps),
|
||||
#[serde(rename = "run.title.updated")]
|
||||
RunTitleUpdated(RunTitleUpdatedProps),
|
||||
#[serde(rename = "run.parent.linked")]
|
||||
RunParentLinked(RunParentLinkedProps),
|
||||
#[serde(rename = "run.parent.unlinked")]
|
||||
RunParentUnlinked(RunParentUnlinkedProps),
|
||||
#[serde(rename = "run.completed")]
|
||||
RunCompleted(RunCompletedProps),
|
||||
#[serde(rename = "run.failed")]
|
||||
|
|
@ -396,6 +400,8 @@ impl EventBody {
|
|||
Self::RunArchived(_) => "run.archived",
|
||||
Self::RunUnarchived(_) => "run.unarchived",
|
||||
Self::RunTitleUpdated(_) => "run.title.updated",
|
||||
Self::RunParentLinked(_) => "run.parent.linked",
|
||||
Self::RunParentUnlinked(_) => "run.parent.unlinked",
|
||||
Self::RunCompleted(_) => "run.completed",
|
||||
Self::RunFailed(_) => "run.failed",
|
||||
Self::RunNotice(_) => "run.notice",
|
||||
|
|
@ -549,6 +555,8 @@ fn is_known_event_name(event: &str) -> bool {
|
|||
| "run.superseded_by"
|
||||
| "run.archived"
|
||||
| "run.unarchived"
|
||||
| "run.parent.linked"
|
||||
| "run.parent.unlinked"
|
||||
| "run.completed"
|
||||
| "run.failed"
|
||||
| "run.notice"
|
||||
|
|
|
|||
|
|
@ -37,9 +37,23 @@ pub struct RunCreatedProps {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<RunId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub web_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunParentLinkedProps {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub previous_parent_id: Option<RunId>,
|
||||
pub parent_id: RunId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunParentUnlinkedProps {
|
||||
pub previous_parent_id: RunId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunStartedProps {
|
||||
pub name: String,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ use crate::{
|
|||
pub struct RunProjection {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<RunId>,
|
||||
pub spec: RunSpec,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub web_url: Option<String>,
|
||||
|
|
@ -156,6 +158,7 @@ impl RunProjection {
|
|||
pub fn new(title: String, spec: RunSpec, created_at: DateTime<Utc>) -> Self {
|
||||
Self {
|
||||
title,
|
||||
parent_id: None,
|
||||
spec,
|
||||
web_url: None,
|
||||
start: None,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ use crate::{
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Run {
|
||||
pub id: RunId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<RunId>,
|
||||
pub title: String,
|
||||
pub goal: String,
|
||||
pub workflow: WorkflowRef,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ use std::collections::BTreeMap;
|
|||
|
||||
use fabro_types::graph::Graph;
|
||||
use fabro_types::run::{DirtyStatus, ForkSourceRef, GitContext, PreRunPushOutcome};
|
||||
use fabro_types::run_event::run::RunCreatedProps;
|
||||
use fabro_types::run_event::run::{RunCreatedProps, RunParentLinkedProps, RunParentUnlinkedProps};
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::RunGoal;
|
||||
use fabro_types::{WorkflowSettings, fixtures};
|
||||
use fabro_types::{EventBody, WorkflowSettings, fixtures};
|
||||
|
||||
fn templated_settings() -> WorkflowSettings {
|
||||
let mut settings = WorkflowSettings::default();
|
||||
|
|
@ -39,6 +39,7 @@ fn run_created_props_round_trip_templated_settings() {
|
|||
source_run_id: fixtures::RUN_2,
|
||||
checkpoint_sha: "def456".to_string(),
|
||||
}),
|
||||
parent_id: Some(fixtures::RUN_2),
|
||||
web_url: Some("http://localhost:3000/runs/01JNQVR7M0EJ5GKAT2SC4ERS1Z".to_string()),
|
||||
};
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ fn run_created_props_round_trip_templated_settings() {
|
|||
json["web_url"],
|
||||
"http://localhost:3000/runs/01JNQVR7M0EJ5GKAT2SC4ERS1Z"
|
||||
);
|
||||
assert_eq!(json["parent_id"], fixtures::RUN_2.to_string());
|
||||
|
||||
let round_trip: RunCreatedProps =
|
||||
serde_json::from_value(json.clone()).expect("props should deserialize");
|
||||
|
|
@ -88,6 +90,7 @@ fn run_created_props_omits_web_url_when_absent() {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
};
|
||||
|
||||
|
|
@ -96,8 +99,46 @@ fn run_created_props_omits_web_url_when_absent() {
|
|||
json.get("web_url").is_none(),
|
||||
"web_url must be omitted when None, got {json}"
|
||||
);
|
||||
assert!(
|
||||
json.get("parent_id").is_none(),
|
||||
"parent_id must be omitted when None, got {json}"
|
||||
);
|
||||
|
||||
let round_trip: RunCreatedProps =
|
||||
serde_json::from_value(json.clone()).expect("props should deserialize");
|
||||
assert_eq!(round_trip.web_url, None);
|
||||
assert_eq!(round_trip.parent_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_parent_events_round_trip_parent_ids() {
|
||||
let linked = EventBody::RunParentLinked(RunParentLinkedProps {
|
||||
previous_parent_id: None,
|
||||
parent_id: fixtures::RUN_2,
|
||||
});
|
||||
let linked_json = serde_json::to_value(&linked).expect("linked event should serialize");
|
||||
assert_eq!(linked_json["event"], "run.parent.linked");
|
||||
assert_eq!(
|
||||
linked_json["properties"]["parent_id"],
|
||||
fixtures::RUN_2.to_string()
|
||||
);
|
||||
|
||||
let linked_round_trip: EventBody =
|
||||
serde_json::from_value(linked_json).expect("linked event should deserialize");
|
||||
assert_eq!(linked_round_trip.event_name(), "run.parent.linked");
|
||||
|
||||
let unlinked = EventBody::RunParentUnlinked(RunParentUnlinkedProps {
|
||||
previous_parent_id: fixtures::RUN_2,
|
||||
});
|
||||
let unlinked_json = serde_json::to_value(&unlinked).expect("unlinked event should serialize");
|
||||
assert_eq!(unlinked_json["event"], "run.parent.unlinked");
|
||||
assert_eq!(
|
||||
unlinked_json["properties"]["previous_parent_id"],
|
||||
fixtures::RUN_2.to_string()
|
||||
);
|
||||
assert!(unlinked_json["properties"].get("parent_id").is_none());
|
||||
|
||||
let unlinked_round_trip: EventBody =
|
||||
serde_json::from_value(unlinked_json).expect("unlinked event should deserialize");
|
||||
assert_eq!(unlinked_round_trip.event_name(), "run.parent.unlinked");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
manifest_blob,
|
||||
git,
|
||||
fork_source_ref,
|
||||
parent_id,
|
||||
web_url,
|
||||
..
|
||||
} => EventBody::RunCreated(fabro_types::RunCreatedProps {
|
||||
|
|
@ -57,6 +58,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
manifest_blob: *manifest_blob,
|
||||
git: git.clone(),
|
||||
fork_source_ref: fork_source_ref.clone(),
|
||||
parent_id: *parent_id,
|
||||
web_url: web_url.clone(),
|
||||
}),
|
||||
Event::WorkflowRunStarted {
|
||||
|
|
@ -143,6 +145,19 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
title: title.clone(),
|
||||
})
|
||||
}
|
||||
Event::RunParentLinked {
|
||||
previous_parent_id,
|
||||
parent_id,
|
||||
..
|
||||
} => EventBody::RunParentLinked(fabro_types::RunParentLinkedProps {
|
||||
previous_parent_id: *previous_parent_id,
|
||||
parent_id: *parent_id,
|
||||
}),
|
||||
Event::RunParentUnlinked {
|
||||
previous_parent_id, ..
|
||||
} => EventBody::RunParentUnlinked(fabro_types::RunParentUnlinkedProps {
|
||||
previous_parent_id: *previous_parent_id,
|
||||
}),
|
||||
Event::WorkflowRunCompleted {
|
||||
duration_ms,
|
||||
artifact_count,
|
||||
|
|
@ -2251,6 +2266,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
});
|
||||
let actor = stored.actor.as_ref().expect("actor set");
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ pub enum Event {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
fork_source_ref: Option<ForkSourceRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
parent_id: Option<RunId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
web_url: Option<String>,
|
||||
},
|
||||
WorkflowRunStarted {
|
||||
|
|
@ -115,6 +117,18 @@ pub enum Event {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
actor: Option<Principal>,
|
||||
},
|
||||
RunParentLinked {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
previous_parent_id: Option<RunId>,
|
||||
parent_id: RunId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
actor: Option<Principal>,
|
||||
},
|
||||
RunParentUnlinked {
|
||||
previous_parent_id: RunId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
actor: Option<Principal>,
|
||||
},
|
||||
WorkflowRunCompleted {
|
||||
duration_ms: u64,
|
||||
artifact_count: usize,
|
||||
|
|
@ -810,6 +824,19 @@ impl Event {
|
|||
Self::RunTitleUpdated { title, actor } => {
|
||||
info!(title, ?actor, "Run title updated");
|
||||
}
|
||||
Self::RunParentLinked {
|
||||
previous_parent_id,
|
||||
parent_id,
|
||||
actor,
|
||||
} => {
|
||||
info!(?previous_parent_id, %parent_id, ?actor, "Run parent linked");
|
||||
}
|
||||
Self::RunParentUnlinked {
|
||||
previous_parent_id,
|
||||
actor,
|
||||
} => {
|
||||
info!(%previous_parent_id, ?actor, "Run parent unlinked");
|
||||
}
|
||||
Self::WorkflowRunCompleted {
|
||||
duration_ms,
|
||||
artifact_count,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ pub fn event_name(event: &Event) -> &'static str {
|
|||
Event::RunArchived { .. } => "run.archived",
|
||||
Event::RunUnarchived { .. } => "run.unarchived",
|
||||
Event::RunTitleUpdated { .. } => "run.title.updated",
|
||||
Event::RunParentLinked { .. } => "run.parent.linked",
|
||||
Event::RunParentUnlinked { .. } => "run.parent.unlinked",
|
||||
Event::WorkflowRunCompleted { .. } => "run.completed",
|
||||
Event::WorkflowRunFailed { .. } => "run.failed",
|
||||
Event::RunNotice { .. } => "run.notice",
|
||||
|
|
|
|||
|
|
@ -247,6 +247,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ fn stored_event_fields_for_variant(event: &Event) -> StoredEventFields {
|
|||
| Event::RunArchived { actor }
|
||||
| Event::RunUnarchived { actor, .. }
|
||||
| Event::RunTitleUpdated { actor, .. }
|
||||
| Event::RunParentLinked { actor, .. }
|
||||
| Event::RunParentUnlinked { actor, .. }
|
||||
| Event::InterviewCompleted { actor, .. }
|
||||
| Event::AgentSteerBuffered { actor, .. } => StoredEventFields {
|
||||
actor: actor.clone(),
|
||||
|
|
|
|||
|
|
@ -472,6 +472,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -482,6 +482,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -706,6 +706,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -729,6 +729,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub struct CreateRunInput {
|
|||
pub title: Option<String>,
|
||||
pub git: Option<GitContext>,
|
||||
pub fork_source_ref: Option<ForkSourceRef>,
|
||||
pub parent_id: Option<RunId>,
|
||||
pub provenance: Option<RunProvenance>,
|
||||
pub configured_providers: Vec<ProviderId>,
|
||||
/// Public URL where this run can be viewed in the web UI, when the server
|
||||
|
|
@ -109,6 +110,7 @@ pub async fn create(
|
|||
title,
|
||||
git,
|
||||
fork_source_ref,
|
||||
parent_id,
|
||||
provenance,
|
||||
configured_providers,
|
||||
web_url,
|
||||
|
|
@ -169,6 +171,7 @@ pub async fn create(
|
|||
submitted_manifest_bytes.as_deref(),
|
||||
accepted_definition.as_ref(),
|
||||
title,
|
||||
parent_id,
|
||||
web_url,
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -189,6 +192,7 @@ async fn persist_created_run(
|
|||
submitted_manifest_bytes: Option<&[u8]>,
|
||||
accepted_definition: Option<&RunDefinition>,
|
||||
explicit_title: Option<String>,
|
||||
parent_id: Option<RunId>,
|
||||
web_url: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let record = persisted.run_spec();
|
||||
|
|
@ -242,6 +246,7 @@ async fn persist_created_run(
|
|||
manifest_blob,
|
||||
git: record.git.clone(),
|
||||
fork_source_ref: record.fork_source_ref.clone(),
|
||||
parent_id,
|
||||
web_url,
|
||||
},
|
||||
record.run_id.created_at(),
|
||||
|
|
@ -871,6 +876,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -916,6 +922,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -983,6 +990,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1093,6 +1101,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1137,6 +1146,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1203,6 +1213,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1248,6 +1259,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: Some(fabro_types::RunProvenance {
|
||||
server: Some(fabro_types::RunServerProvenance {
|
||||
version: "0.9.0".to_string(),
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ async fn persist_forked_run(
|
|||
manifest_blob: spec.manifest_blob,
|
||||
git: spec.git.clone(),
|
||||
fork_source_ref: spec.fork_source_ref.clone(),
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -384,6 +385,7 @@ mod tests {
|
|||
push_outcome: fabro_types::PreRunPushOutcome::NotAttempted,
|
||||
}),
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1306,6 +1306,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
@ -1497,6 +1498,7 @@ mod tests {
|
|||
title: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
provenance: None,
|
||||
configured_providers: Vec::new(),
|
||||
web_url: None,
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ async fn seed_created_and_starting(
|
|||
manifest_blob: None,
|
||||
git: run_options.pre_run_git.clone(),
|
||||
fork_source_ref: run_options.fork_source_ref.clone(),
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -741,6 +741,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: record.git.clone(),
|
||||
fork_source_ref: record.fork_source_ref.clone(),
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1164,6 +1164,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: run_spec.git.clone(),
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -1231,6 +1232,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: run_spec.git.clone(),
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -1587,6 +1589,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -1706,6 +1709,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
@ -1876,6 +1880,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -485,6 +485,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: run_spec.git.clone(),
|
||||
fork_source_ref: run_spec.fork_source_ref.clone(),
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ mod tests {
|
|||
manifest_blob: None,
|
||||
git: None,
|
||||
fork_source_ref: None,
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ async fn initialized(
|
|||
manifest_blob: None,
|
||||
git: run_options.pre_run_git.clone(),
|
||||
fork_source_ref: run_options.fork_source_ref.clone(),
|
||||
parent_id: None,
|
||||
web_url: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ models/system-repair-runs-response.ts
|
|||
models/system-run-counts.ts
|
||||
models/timeline-entry-response.ts
|
||||
models/tls-mode.ts
|
||||
models/update-run-parent-request.ts
|
||||
models/update-run-request.ts
|
||||
models/user-response.ts
|
||||
models/validate-response.ts
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ import type { StartRunRequest } from '../models';
|
|||
// @ts-ignore
|
||||
import type { TimelineEntryResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { UpdateRunParentRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { UpdateRunRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { ValidateResponse } from '../models';
|
||||
|
|
@ -446,6 +448,51 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Links a run under an orchestration parent. Parent links are mutable for all run states, including archived and terminal runs.
|
||||
* @summary Link Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {UpdateRunParentRequest} updateRunParentRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
linkRunParent: async (id: string, updateRunParentRequest: UpdateRunParentRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('linkRunParent', 'id', id)
|
||||
// verify required parameter 'updateRunParentRequest' is not null or undefined
|
||||
assertParamExists('linkRunParent', 'updateRunParentRequest', updateRunParentRequest)
|
||||
const localVarPath = `/api/v1/runs/{id}/parent`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// 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: 'PUT', ...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(updateRunParentRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Links or replaces the GitHub pull request association for a run without modifying the remote pull request.
|
||||
* @summary Link Run Pull Request
|
||||
|
|
@ -548,10 +595,11 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {boolean} [includeArchived] Whether to include archived runs in the response. Defaults to `false`.
|
||||
* @param {string} [parentId] Return only runs currently linked to this orchestration parent.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRuns: async (pageLimit?: number, pageOffset?: number, includeArchived?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
listRuns: async (pageLimit?: number, pageOffset?: number, includeArchived?: boolean, parentId?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/v1/runs`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
|
|
@ -582,6 +630,10 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
localVarQueryParameter['include_archived'] = includeArchived;
|
||||
}
|
||||
|
||||
if (parentId !== undefined) {
|
||||
localVarQueryParameter['parent_id'] = parentId;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
|
|
@ -1054,6 +1106,46 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Removes a run\'s orchestration parent. Already-root runs are returned unchanged.
|
||||
* @summary Unlink Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
unlinkRunParent: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('unlinkRunParent', 'id', id)
|
||||
const localVarPath = `/api/v1/runs/{id}/parent`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// 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: 'DELETE', ...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['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Removes Fabro\'s stored pull request association for a run without modifying the remote pull request.
|
||||
* @summary Unlink Run Pull Request
|
||||
|
|
@ -1349,6 +1441,20 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['RunsApi.getRunTimeline']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Links a run under an orchestration parent. Parent links are mutable for all run states, including archived and terminal runs.
|
||||
* @summary Link Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {UpdateRunParentRequest} updateRunParentRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async linkRunParent(id: string, updateRunParentRequest: UpdateRunParentRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Run>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.linkRunParent(id, updateRunParentRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.linkRunParent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Links or replaces the GitHub pull request association for a run without modifying the remote pull request.
|
||||
* @summary Link Run Pull Request
|
||||
|
|
@ -1384,11 +1490,12 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {boolean} [includeArchived] Whether to include archived runs in the response. Defaults to `false`.
|
||||
* @param {string} [parentId] Return only runs currently linked to this orchestration parent.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listRuns(pageLimit?: number, pageOffset?: number, includeArchived?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRuns(pageLimit, pageOffset, includeArchived, options);
|
||||
async listRuns(pageLimit?: number, pageOffset?: number, includeArchived?: boolean, parentId?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRuns(pageLimit, pageOffset, includeArchived, parentId, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.listRuns']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
|
|
@ -1540,6 +1647,19 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['RunsApi.unarchiveRun']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Removes a run\'s orchestration parent. Already-root runs are returned unchanged.
|
||||
* @summary Unlink Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async unlinkRunParent(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Run>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.unlinkRunParent(id, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.unlinkRunParent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Removes Fabro\'s stored pull request association for a run without modifying the remote pull request.
|
||||
* @summary Unlink Run Pull Request
|
||||
|
|
@ -1695,6 +1815,17 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
getRunTimeline(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<TimelineEntryResponse>> {
|
||||
return localVarFp.getRunTimeline(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Links a run under an orchestration parent. Parent links are mutable for all run states, including archived and terminal runs.
|
||||
* @summary Link Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {UpdateRunParentRequest} updateRunParentRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
linkRunParent(id: string, updateRunParentRequest: UpdateRunParentRequest, options?: RawAxiosRequestConfig): AxiosPromise<Run> {
|
||||
return localVarFp.linkRunParent(id, updateRunParentRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Links or replaces the GitHub pull request association for a run without modifying the remote pull request.
|
||||
* @summary Link Run Pull Request
|
||||
|
|
@ -1724,11 +1855,12 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {boolean} [includeArchived] Whether to include archived runs in the response. Defaults to `false`.
|
||||
* @param {string} [parentId] Return only runs currently linked to this orchestration parent.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRuns(pageLimit?: number, pageOffset?: number, includeArchived?: boolean, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunList> {
|
||||
return localVarFp.listRuns(pageLimit, pageOffset, includeArchived, options).then((request) => request(axios, basePath));
|
||||
listRuns(pageLimit?: number, pageOffset?: number, includeArchived?: boolean, parentId?: string, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunList> {
|
||||
return localVarFp.listRuns(pageLimit, pageOffset, includeArchived, parentId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Merges the stored pull request for a run on GitHub.
|
||||
|
|
@ -1844,6 +1976,16 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
unarchiveRun(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Run> {
|
||||
return localVarFp.unarchiveRun(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Removes a run\'s orchestration parent. Already-root runs are returned unchanged.
|
||||
* @summary Unlink Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
unlinkRunParent(id: string, options?: RawAxiosRequestConfig): AxiosPromise<Run> {
|
||||
return localVarFp.unlinkRunParent(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Removes Fabro\'s stored pull request association for a run without modifying the remote pull request.
|
||||
* @summary Unlink Run Pull Request
|
||||
|
|
@ -1994,6 +2136,18 @@ export class RunsApi extends BaseAPI {
|
|||
return RunsApiFp(this.configuration).getRunTimeline(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a run under an orchestration parent. Parent links are mutable for all run states, including archived and terminal runs.
|
||||
* @summary Link Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {UpdateRunParentRequest} updateRunParentRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public linkRunParent(id: string, updateRunParentRequest: UpdateRunParentRequest, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).linkRunParent(id, updateRunParentRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Links or replaces the GitHub pull request association for a run without modifying the remote pull request.
|
||||
* @summary Link Run Pull Request
|
||||
|
|
@ -2025,11 +2179,12 @@ export class RunsApi extends BaseAPI {
|
|||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {boolean} [includeArchived] Whether to include archived runs in the response. Defaults to `false`.
|
||||
* @param {string} [parentId] Return only runs currently linked to this orchestration parent.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public listRuns(pageLimit?: number, pageOffset?: number, includeArchived?: boolean, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).listRuns(pageLimit, pageOffset, includeArchived, options).then((request) => request(this.axios, this.basePath));
|
||||
public listRuns(pageLimit?: number, pageOffset?: number, includeArchived?: boolean, parentId?: string, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).listRuns(pageLimit, pageOffset, includeArchived, parentId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2157,6 +2312,17 @@ export class RunsApi extends BaseAPI {
|
|||
return RunsApiFp(this.configuration).unarchiveRun(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a run\'s orchestration parent. Already-root runs are returned unchanged.
|
||||
* @summary Unlink Run Parent
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public unlinkRunParent(id: string, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).unlinkRunParent(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes Fabro\'s stored pull request association for a run without modifying the remote pull request.
|
||||
* @summary Unlink Run Pull Request
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@ export * from './system-repair-runs-response';
|
|||
export * from './system-run-counts';
|
||||
export * from './timeline-entry-response';
|
||||
export * from './tls-mode';
|
||||
export * from './update-run-parent-request';
|
||||
export * from './update-run-request';
|
||||
export * from './user-response';
|
||||
export * from './validate-response';
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ export interface RunManifest {
|
|||
* Optional pre-generated run ID to use instead of allocating a new ULID.
|
||||
*/
|
||||
'run_id'?: string | null;
|
||||
/**
|
||||
* Optional orchestration parent run ID. Fork and rewind lineage use separate fields and should not set this value.
|
||||
*/
|
||||
'parent_id'?: string | null;
|
||||
/**
|
||||
* Optional explicit run title. The server trims leading/trailing whitespace, rejects blank values, rejects control characters and newline characters, and requires at most 100 characters.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ export interface RunProjection {
|
|||
* Resolved run title from the event log.
|
||||
*/
|
||||
'title'?: string;
|
||||
/**
|
||||
* Current orchestration parent run ID, if linked.
|
||||
*/
|
||||
'parent_id'?: string | null;
|
||||
'spec': RunSpec;
|
||||
/**
|
||||
* Absolute web UI URL for this run when server web settings are configured.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ import type { WorkflowRef } from './workflow-ref';
|
|||
*/
|
||||
export interface Run {
|
||||
'id': string;
|
||||
/**
|
||||
* Current orchestration parent run ID, if linked.
|
||||
*/
|
||||
'parent_id'?: string | null;
|
||||
'title': string;
|
||||
'goal': string;
|
||||
'workflow': WorkflowRef;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface UpdateRunParentRequest {
|
||||
/**
|
||||
* Existing orchestration parent run ID.
|
||||
*/
|
||||
'parent_id': string;
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue