feat(workflows): M5 — SDK workflow::start + admin run API + pic CLI
The user-facing surface for the durable DAG engine. A workflow run can now be
started three ways, all resolving the workflow by name in the caller's app
(`cx.app_id` / the resolved app — never a passed-in arg, the isolation
boundary), seeding a run the orchestrator advances.
SDK seam (shared):
- `WorkflowService` trait + `NoopWorkflowService` + `WorkflowError` in
`shared::workflow` (mirrors `InvokeService`); added to `Services` as a
noop-defaulted `with_workflow` builder (all `Services::new` call sites
unchanged).
- `WorkflowServiceImpl` (manager-core): resolves + seeds a run; authenticated
callers gated on `AppInvoke` (anonymous skips, script-as-gate), the same gate
`invoke()` uses.
- `executor-core::sdk::workflow` — the Rhai bridge `workflow::start(name, input)`
/ `workflow::start(name)`, registered in `register_all`. Returns the run id.
Admin API (manager-core `workflows_api`, mounted in the binary):
- `GET /apps/{app}/workflows` — definitions (AppRead)
- `POST /apps/{app}/workflows/{name}/runs` — start a run (AppInvoke)
- `GET /apps/{app}/workflows/{name}/runs` — run history (AppRead)
- `GET /apps/{app}/workflow-runs/{run_id}` — one run + its steps (AppRead)
`app_id` scopes every query; a foreign run 404s.
CLI (`pic workflows`): `ls` · `run <name> [--input JSON]` · `runs <name>` ·
`run-status <run_id>` — all `--app`-scoped; run definitions stay declarative
(`[[workflows]]` → `pic apply`).
Also: `list_runs_for_workflow` repo reader.
Tests: `workflow_service_start_seeds_a_run` (DB-gated — the SDK/API-shared
entry) + `workflow_run_executes_end_to_end` CLI journey (apply → `pic workflows
run` → poll `run-status` → succeeded, through the real binary + orchestrator).
fmt + clippy -D warnings clean, 449 manager-core lib tests, 14 orchestrator DB
tests, 18 executor-core lib tests, 3 workflow CLI journeys green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -120,3 +120,4 @@ pub use users::{
|
||||
pub use validator::{ScriptValidator, ValidatedScript, ValidationError};
|
||||
pub use vars::{NoopVarsService, VarsError, VarsService};
|
||||
pub use version::{API_VERSION, PRODUCT_VERSION, SDK_VERSION, WIRE_VERSION};
|
||||
pub use workflow::{NoopWorkflowService, WorkflowError, WorkflowService};
|
||||
|
||||
@@ -26,8 +26,9 @@ use crate::{
|
||||
NoopEmailService, NoopEventEmitter, NoopFilesService, NoopGroupDocsService,
|
||||
NoopGroupFilesService, NoopGroupKvService, NoopGroupPubsubService, NoopGroupQueueService,
|
||||
NoopHttpService, NoopInvokeService, NoopKvService, NoopModuleSource, NoopPubsubService,
|
||||
NoopQueueService, NoopSecretsService, NoopUsersService, NoopVarsService, PubsubService,
|
||||
QueueService, SecretsService, ServiceEventEmitter, UsersService, VarsService,
|
||||
NoopQueueService, NoopSecretsService, NoopUsersService, NoopVarsService, NoopWorkflowService,
|
||||
PubsubService, QueueService, SecretsService, ServiceEventEmitter, UsersService, VarsService,
|
||||
WorkflowService,
|
||||
};
|
||||
|
||||
/// SDK service bundle. See module docs for the lifecycle and the v1.1.x
|
||||
@@ -146,6 +147,11 @@ pub struct Services {
|
||||
/// drained by competing per-descendant consumers. Wired via
|
||||
/// [`Services::with_group_queue`]; defaults to `NoopGroupQueueService`.
|
||||
pub group_queue: Arc<dyn GroupQueueService>,
|
||||
|
||||
/// v1.2 Workflows (M5). Scripts get `workflow::start(name, input)` — start a
|
||||
/// run of a named workflow in the caller's app. Wired via
|
||||
/// [`Services::with_workflow`]; defaults to `NoopWorkflowService`.
|
||||
pub workflow: Arc<dyn WorkflowService>,
|
||||
}
|
||||
|
||||
impl Services {
|
||||
@@ -193,9 +199,18 @@ impl Services {
|
||||
group_files: Arc::new(NoopGroupFilesService),
|
||||
group_pubsub: Arc::new(NoopGroupPubsubService),
|
||||
group_queue: Arc::new(NoopGroupQueueService),
|
||||
workflow: Arc::new(NoopWorkflowService),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the v1.2 Workflows service (picloud binary wires the Postgres-backed
|
||||
/// impl; tests leave the noop default).
|
||||
#[must_use]
|
||||
pub fn with_workflow(mut self, workflow: Arc<dyn WorkflowService>) -> Self {
|
||||
self.workflow = workflow;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the §11.6 shared group-KV service (the picloud binary wires the
|
||||
/// Postgres-backed impl; tests leave the noop default).
|
||||
#[must_use]
|
||||
|
||||
@@ -7,10 +7,60 @@
|
||||
//! resolver (`workflow_template`), and the durable orchestrator all live
|
||||
//! server-side in `manager-core`, the only crate that touches Postgres.
|
||||
//!
|
||||
//! The `WorkflowService` trait (the injected SDK seam for `workflow::start`)
|
||||
//! is added here in M5.
|
||||
//! The `WorkflowService` trait is the injected SDK seam for `workflow::start`
|
||||
//! (M5): a script (or the admin API) starts a run of a named workflow in the
|
||||
//! caller's app. Same-app only — the owning workflow resolves from `cx.app_id`,
|
||||
//! never a script-passed arg (the isolation boundary), mirroring `InvokeService`.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{SdkCallCx, WorkflowRunId};
|
||||
|
||||
/// Backs the Rhai `workflow::start(name, input)` call and the admin run API.
|
||||
#[async_trait]
|
||||
pub trait WorkflowService: Send + Sync {
|
||||
/// Start a run of the workflow named `name` in `cx.app_id`, seeded with
|
||||
/// `input`. Returns the new run id. The durable orchestrator advances it.
|
||||
async fn start(
|
||||
&self,
|
||||
cx: &SdkCallCx,
|
||||
name: &str,
|
||||
input: serde_json::Value,
|
||||
) -> Result<WorkflowRunId, WorkflowError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WorkflowError {
|
||||
#[error("workflow {0:?} not found")]
|
||||
NotFound(String),
|
||||
#[error("workflow {0:?} is disabled")]
|
||||
Disabled(String),
|
||||
/// The authenticated caller lacks `AppInvoke` on the app.
|
||||
#[error("forbidden")]
|
||||
Forbidden,
|
||||
#[error("workflow backend error: {0}")]
|
||||
Backend(String),
|
||||
}
|
||||
|
||||
/// Default no-op: every call errors so a misconfigured bundle can't silently
|
||||
/// no-op a `workflow::start`.
|
||||
pub struct NoopWorkflowService;
|
||||
|
||||
#[async_trait]
|
||||
impl WorkflowService for NoopWorkflowService {
|
||||
async fn start(
|
||||
&self,
|
||||
_cx: &SdkCallCx,
|
||||
_name: &str,
|
||||
_input: serde_json::Value,
|
||||
) -> Result<WorkflowRunId, WorkflowError> {
|
||||
Err(WorkflowError::Backend(
|
||||
"workflow service is not configured".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A workflow's DAG: an ordered list of steps connected by `depends_on` edges.
|
||||
/// Serialized to the `workflows.definition` JSONB column.
|
||||
|
||||
Reference in New Issue
Block a user