ironflow_engine/context/steps/skip.rs
1//! Explicitly skipped step for [`WorkflowContext`].
2
3use chrono::Utc;
4use serde_json::json;
5use tracing::info;
6
7use ironflow_store::models::{
8 NewStep, NewStepDependency, StepKind, StepStatus, StepUpdate, step_trace_id,
9};
10
11use crate::context::WorkflowContext;
12use crate::error::EngineError;
13use crate::plan::{ConditionResult, lock_plan};
14
15impl WorkflowContext {
16 /// Record a step as explicitly skipped.
17 ///
18 /// Use this inside an `if`/`else` branch when a step should not execute
19 /// but must still appear in the DAG and timeline with its reason.
20 ///
21 /// The step is created directly in [`StepStatus::Skipped`] state and the
22 /// reason is stored in the output as `{"reason": "..."}`.
23 ///
24 /// # Errors
25 ///
26 /// Returns [`EngineError`] if the store fails.
27 ///
28 /// # Examples
29 ///
30 /// ```no_run
31 /// use ironflow_engine::context::WorkflowContext;
32 /// use ironflow_engine::error::EngineError;
33 ///
34 /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
35 /// let tests_passed = false;
36 /// if tests_passed {
37 /// // ctx.shell("deploy", ...).await?;
38 /// } else {
39 /// ctx.skip("deploy", "tests failed").await?;
40 /// }
41 /// # Ok(())
42 /// # }
43 /// ```
44 pub async fn skip(&mut self, name: &str, reason: &str) -> Result<(), EngineError> {
45 // Plan mode: the skip and its reason become the step's condition.
46 if let Some(plan) = self.plan().cloned() {
47 self.position += 1;
48 let mut recorder = lock_plan(&plan);
49 recorder.set_condition(ConditionResult::Skipped {
50 reason: reason.to_string(),
51 });
52 if recorder.record(
53 name,
54 StepKind::Custom("skip".to_string()),
55 &self.workflow_name,
56 None,
57 ) {
58 recorder.set_last(vec![name.to_string()]);
59 }
60 return Ok(());
61 }
62
63 let position = self.position;
64 self.position += 1;
65
66 let trace_id = step_trace_id(self.run_id, name, position);
67 let step = self
68 .store
69 .create_step(NewStep {
70 run_id: self.run_id,
71 trace_id,
72 name: name.to_string(),
73 kind: StepKind::Custom("skip".to_string()),
74 position,
75 input: None,
76 is_error_handler: false,
77 })
78 .await?;
79
80 if !self.last_step_ids.is_empty() {
81 let deps: Vec<NewStepDependency> = self
82 .last_step_ids
83 .iter()
84 .map(|&depends_on| NewStepDependency {
85 step_id: step.id,
86 depends_on,
87 })
88 .collect();
89 self.store.create_step_dependencies(deps).await?;
90 }
91
92 let now = Utc::now();
93 self.store
94 .update_step(
95 step.id,
96 StepUpdate {
97 status: Some(StepStatus::Skipped),
98 output: Some(json!({"reason": reason})),
99 completed_at: Some(now),
100 ..StepUpdate::default()
101 },
102 )
103 .await?;
104
105 self.last_step_ids = vec![step.id];
106
107 info!(
108 run_id = %self.run_id,
109 step = %name,
110 reason,
111 "step skipped"
112 );
113
114 Ok(())
115 }
116}