fix(workflows): close re-review gaps in the pull round-trip + partial-failure UI

A re-review of eaf5ace surfaced three follow-ups, all in the fixes themselves:

- pull vs an old server: a returned empty `definition` can only mean a server
  predating the definition-in-list field (a real zero-step workflow is rejected
  at apply). Emitting a stepless `[[workflows]]` block would just fail the next
  apply, so `wire_workflow_to_manifest` now warns and skips it instead.
- base_ms/backoff defaults: the pull mapper keyed its default-omission off a
  hardcoded `500` literal. `default_base_ms` is now `pub` + re-exported, and the
  mapper omits whatever equals `default_base_ms()` / `WorkflowBackoff::default()`
  — no drift if a default ever changes.
- dashboard: a run that succeeds with an `on_error = continue` step failure now
  carries a partial-failure note in `run.error` (from the compute_advance
  change); the run-detail view rendered it as a red failure, contradicting the
  green "succeeded" badge. It now renders as a `.notice` (warning) for a
  succeeded run, `.error` only for a failed one.

Verified: fmt + clippy -D warnings clean; 450 lib tests; 5 workflow CLI
journeys (incl. the pull→plan round-trip); dashboard npm run check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-12 19:08:21 +02:00
parent eaf5ace30f
commit aeebdd2f3f
4 changed files with 40 additions and 18 deletions

View File

@@ -253,7 +253,10 @@ pub async fn run(app_ident: &str, dir: &Path, force: bool, mode: OutputMode) ->
},
vars: manifest_vars,
suppress: crate::manifest::ManifestSuppress::default(),
workflows: workflows.iter().map(wire_workflow_to_manifest).collect(),
workflows: workflows
.iter()
.filter_map(wire_workflow_to_manifest)
.collect(),
};
std::fs::write(&manifest_path, manifest.to_toml()?)
@@ -280,8 +283,21 @@ fn trigger_count(t: &ManifestTriggers) -> usize {
/// (the inverse of `plan::workflow_to_wire`). Without this a `pic pull` would
/// drop the app's workflows, and a subsequent `pic apply --prune` would delete
/// them from the server.
fn wire_workflow_to_manifest(w: &crate::client::WorkflowDto) -> ManifestWorkflow {
ManifestWorkflow {
///
/// Returns `None` (with a warning) when the definition came back empty. A
/// zero-step workflow is rejected at apply, so an empty `definition` can only
/// mean the server predates the definition-in-list field (version skew) —
/// emitting a stepless block would just fail the next apply, so skip it.
fn wire_workflow_to_manifest(w: &crate::client::WorkflowDto) -> Option<ManifestWorkflow> {
if w.definition.steps.is_empty() {
eprintln!(
"warning: skipping workflow `{}` — the server did not return its \
definition (upgrade the server to round-trip workflows via pull)",
w.name
);
return None;
}
Some(ManifestWorkflow {
name: w.name.clone(),
enabled: w.enabled,
steps: w
@@ -290,7 +306,7 @@ fn wire_workflow_to_manifest(w: &crate::client::WorkflowDto) -> ManifestWorkflow
.iter()
.map(wire_step_to_manifest)
.collect(),
}
})
}
fn wire_step_to_manifest(s: &picloud_shared::WorkflowStepDef) -> ManifestWorkflowStep {
@@ -321,16 +337,10 @@ fn wire_step_to_manifest(s: &picloud_shared::WorkflowStepDef) -> ManifestWorkflo
when: s.when.clone(),
retry: s.retry.as_ref().map(|r| ManifestWorkflowRetry {
max_attempts: r.max_attempts,
// Exponential is the default → omit; likewise the default base_ms.
backoff: match r.backoff {
picloud_shared::WorkflowBackoff::Exponential => None,
other => Some(other),
},
base_ms: if r.base_ms == 500 {
None
} else {
Some(r.base_ms)
},
// Omit whatever equals the server default (so the round-trip is a
// no-op), keyed off the shared defaults rather than local literals.
backoff: (r.backoff != picloud_shared::WorkflowBackoff::default()).then_some(r.backoff),
base_ms: (r.base_ms != picloud_shared::default_base_ms()).then_some(r.base_ms),
}),
// Fail is the default → omit; Continue is explicit.
on_error: match s.on_error {