Skip to main content

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}