fix(workflows): close review gaps — pull round-trip, dup-names, reclaim budget
Adversarial review of the v1.2 Workflows track surfaced two HIGH footguns and several correctness/hardening gaps. Fixes: HIGH - pull round-trip: `pic pull` dropped `[[workflows]]`, so a later `apply --prune` silently deleted them. The list endpoint now returns the full `definition`; pull rebuilds the manifest block (inverse of workflow_to_wire). - case-colliding names: two workflows differing only by case collided in the reconcile diff (keyed by lower(name)), silently dropping one. Rejected up front in validate_bundle_for. MEDIUM - reclaim retry budget: a crashed attempt (no outcome) consumed the retry budget. reclaim_stale_steps now decrements `attempt` (floored) and clears `next_attempt_at`, so a crash no longer counts as a failed try. - on_error/backoff: typed the manifest fields against the shared enums so a bad value fails at TOML parse with a clear message, not an opaque 500. - dedupe depends_on: a repeated dependency inflated the Kahn in-degree past the single decrement, reporting a spurious cycle for a valid DAG. Count distinct. - canceled child: a canceled sub-workflow resolved the parent step with a message naming the cause instead of a generic "failed"; documented that a run-level cancel op is not yet supported. LOW - list_run_steps is now app-scoped at the query (JOIN workflow_runs) rather than relying on caller pre-verification. - partial failure is surfaced: a run that succeeds with on_error=continue failures records them in the run's `error` field. - admin-started runs log the root_execution_id against the principal. - documented the deliberate when(missing→false) vs template(missing→fail) asymmetry; corrected the claim's atomicity comment. Tests: unit (dedupe-deps), DB-gated reclaim-budget assertion, and two new CLI journeys (duplicate-name rejection, pull→plan clean round-trip). fmt + clippy -D warnings clean; 450 lib + 14 orchestrator DB + 5 workflow journeys pass; schema snapshot unchanged (no migration). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -651,9 +651,11 @@ pub struct ManifestWorkflowStep {
|
||||
pub when: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub retry: Option<ManifestWorkflowRetry>,
|
||||
/// `fail` (default) or `continue`.
|
||||
/// `fail` (default) or `continue`. Typed against the shared enum so a typo
|
||||
/// (`on_error = "retry"`) fails at TOML parse with an "unknown variant"
|
||||
/// message pointing at this step, rather than an opaque server-side error.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub on_error: Option<String>,
|
||||
pub on_error: Option<picloud_shared::OnError>,
|
||||
}
|
||||
|
||||
/// Per-step retry policy in the manifest (mirrors the server `WorkflowRetry`).
|
||||
@@ -661,9 +663,10 @@ pub struct ManifestWorkflowStep {
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ManifestWorkflowRetry {
|
||||
pub max_attempts: u32,
|
||||
/// `constant` | `linear` | `exponential` (default).
|
||||
/// `constant` | `linear` | `exponential` (default). Typed against the shared
|
||||
/// enum so a bad value fails at TOML parse, not opaquely on the server.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub backoff: Option<String>,
|
||||
pub backoff: Option<picloud_shared::WorkflowBackoff>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_ms: Option<u32>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user