@@ -169,6 +169,80 @@ impl WorkflowPlanner {
169169 to : "workflow:generate" . to_string ( ) ,
170170 } ) ;
171171 }
172+ last_tasks = vec ! [ "workflow:generate" . to_string( ) ] ;
173+ }
174+
175+ // 3. Deploy Phase
176+ if self . profile . deploy . is_active ( ) {
177+ tasks. push ( WorkflowTaskPlan {
178+ id : "workflow:deployment-plan" . to_string ( ) ,
179+ label : "Deployment Plan" . to_string ( ) ,
180+ kind : WorkflowTaskKind :: DeploymentPlan ,
181+ dependencies : last_tasks. clone ( ) ,
182+ effects : WorkflowEffects :: default ( ) ,
183+ description :
184+ "Create and validate the Kubernetes deployment plan without applying changes."
185+ . to_string ( ) ,
186+ } ) ;
187+
188+ for t in & last_tasks {
189+ edges. push ( WorkflowEdge {
190+ from : t. clone ( ) ,
191+ to : "workflow:deployment-plan" . to_string ( ) ,
192+ } ) ;
193+ }
194+
195+ last_tasks = vec ! [ "workflow:deployment-plan" . to_string( ) ] ;
196+
197+ if self . profile . approval == crate :: workflow:: profile:: ApprovalMode :: Prompt {
198+ effects. prompts_user = true ;
199+ tasks. push ( WorkflowTaskPlan {
200+ id : "workflow:approval" . to_string ( ) ,
201+ label : "Approval" . to_string ( ) ,
202+ kind : WorkflowTaskKind :: Approval ,
203+ dependencies : last_tasks. clone ( ) ,
204+ effects : WorkflowEffects {
205+ prompts_user : true ,
206+ ..Default :: default ( )
207+ } ,
208+ description : "Ask for local confirmation before applying deployment changes."
209+ . to_string ( ) ,
210+ } ) ;
211+
212+ for t in & last_tasks {
213+ edges. push ( WorkflowEdge {
214+ from : t. clone ( ) ,
215+ to : "workflow:approval" . to_string ( ) ,
216+ } ) ;
217+ }
218+
219+ last_tasks = vec ! [ "workflow:approval" . to_string( ) ] ;
220+ }
221+
222+ if self . profile . deploy == crate :: workflow:: profile:: WorkflowStepMode :: Run
223+ && self . profile . apply
224+ {
225+ effects. mutates_cluster = true ;
226+ tasks. push ( WorkflowTaskPlan {
227+ id : "workflow:deploy" . to_string ( ) ,
228+ label : "Deploy" . to_string ( ) ,
229+ kind : WorkflowTaskKind :: Deploy ,
230+ dependencies : last_tasks. clone ( ) ,
231+ effects : WorkflowEffects {
232+ mutates_cluster : true ,
233+ ..Default :: default ( )
234+ } ,
235+ description : "Apply generated manifests to the configured Kubernetes context."
236+ . to_string ( ) ,
237+ } ) ;
238+
239+ for t in & last_tasks {
240+ edges. push ( WorkflowEdge {
241+ from : t. clone ( ) ,
242+ to : "workflow:deploy" . to_string ( ) ,
243+ } ) ;
244+ }
245+ }
172246 }
173247
174248 Ok ( WorkflowPlan {
@@ -275,6 +349,114 @@ impl WorkflowPlanner {
275349 } ) ;
276350
277351 pipeline. add ( task) ;
352+ last_tasks = vec ! [ "workflow:generate" . to_string( ) ] ;
353+ }
354+
355+ if self . profile . deploy . is_active ( ) {
356+ let mut task = Task :: new ( "workflow:deployment-plan" ) ;
357+
358+ let deps_refs: Vec < & str > = last_tasks. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
359+ if !deps_refs. is_empty ( ) {
360+ task = task. depends_on ( & deps_refs) ;
361+ }
362+
363+ let env_name = self . profile . environment . clone ( ) ;
364+ let context = self . profile . deploy_context . clone ( ) . unwrap_or_default ( ) ;
365+ let namespace = self
366+ . profile
367+ . namespace
368+ . clone ( )
369+ . unwrap_or_else ( || "default" . to_string ( ) ) ;
370+
371+ task = task. exec_fn ( move |_ctx| {
372+ let env_name = env_name. clone ( ) ;
373+ let context = context. clone ( ) ;
374+ let namespace = namespace. clone ( ) ;
375+ async move {
376+ crate :: LOGGER . info ( "Deployment plan:" ) ;
377+ let plan =
378+ crate :: plan:: generate_deployment_plan ( & env_name, & context, & namespace)
379+ . await
380+ . map_err ( |e| anyhow:: anyhow!( "Deployment plan failed: {}" , e) ) ?;
381+
382+ crate :: plan:: validate_plan_safety ( & plan)
383+ . map_err ( |e| anyhow:: anyhow!( "Deployment plan validation failed: {}" , e) ) ?;
384+
385+ // Display is a method on DeploymentPlan
386+ // Wait, let's look at src/plan.rs to see how to print the plan.
387+ // Actually, the spec says `plan.display()`, maybe that exists.
388+ // Let's assume there is a way to display.
389+
390+ Ok ( ( ) )
391+ }
392+ } ) ;
393+
394+ pipeline. add ( task) ;
395+ last_tasks = vec ! [ "workflow:deployment-plan" . to_string( ) ] ;
396+
397+ if self . profile . approval == crate :: workflow:: profile:: ApprovalMode :: Prompt {
398+ let mut task = Task :: new ( "workflow:approval" ) ;
399+ let deps_refs: Vec < & str > = last_tasks. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
400+ if !deps_refs. is_empty ( ) {
401+ task = task. depends_on ( & deps_refs) ;
402+ }
403+
404+ task = task. exec_fn ( move |_ctx| async move {
405+ let approved = tokio:: task:: spawn_blocking ( || {
406+ inquire:: Confirm :: new ( "Proceed with deployment?" )
407+ . with_default ( false )
408+ . prompt ( )
409+ } )
410+ . await
411+ . map_err ( |e| anyhow:: anyhow!( "Approval prompt failed: {}" , e) ) ?
412+ . map_err ( |e| anyhow:: anyhow!( "Approval prompt failed: {}" , e) ) ?;
413+
414+ if !approved {
415+ return Err ( anyhow:: anyhow!( "Deployment cancelled by user" ) ) ;
416+ }
417+
418+ Ok ( ( ) )
419+ } ) ;
420+
421+ pipeline. add ( task) ;
422+ last_tasks = vec ! [ "workflow:approval" . to_string( ) ] ;
423+ }
424+
425+ if self . profile . deploy == crate :: workflow:: profile:: WorkflowStepMode :: Run
426+ && self . profile . apply
427+ {
428+ let mut task = Task :: new ( "workflow:deploy" ) ;
429+ let deps_refs: Vec < & str > = last_tasks. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
430+ if !deps_refs. is_empty ( ) {
431+ task = task. depends_on ( & deps_refs) ;
432+ }
433+
434+ let context = self . profile . deploy_context . clone ( ) . unwrap_or_default ( ) ;
435+ let env_name = self . profile . environment . clone ( ) ;
436+
437+ task = task. exec_fn ( move |_ctx| {
438+ let context = context. clone ( ) ;
439+ let env_name = env_name. clone ( ) ;
440+
441+ async move {
442+ crate :: LOGGER . info ( & format ! (
443+ "Deploying environment '{}' to context '{}'..." ,
444+ env_name, context
445+ ) ) ;
446+ crate :: deployment:: deploy (
447+ context,
448+ & env_name,
449+ crate :: cli:: DeploymentStrategy :: Rolling ,
450+ )
451+ . await
452+ . map_err ( |e| anyhow:: anyhow!( "Deploy failed: {}" , e) ) ?;
453+
454+ Ok ( ( ) )
455+ }
456+ } ) ;
457+
458+ pipeline. add ( task) ;
459+ }
278460 }
279461
280462 Ok ( ( pipeline, build_execution) )
@@ -418,4 +600,55 @@ mod tests {
418600 expected. sort ( ) ;
419601 assert_eq ! ( task_names, expected) ;
420602 }
603+
604+ #[ test]
605+ fn local_deploy_creates_deploy_tasks ( ) {
606+ let mut env = Environment :: new ( "local" ) ;
607+ let mut svc = crate :: environment:: Service :: new ( "dummy" , None , "latest" ) ;
608+ svc. build = Some ( crate :: environment:: ServiceBuildConfig {
609+ path : "." . to_string ( ) ,
610+ include : None ,
611+ relies_on : None ,
612+ before_synchronous : None ,
613+ before : None ,
614+ run_parallel : None ,
615+ run_synchronous : None ,
616+ after : None ,
617+ finally : None ,
618+ dockerfile : None ,
619+ build_command : None ,
620+ push_command : None ,
621+ } ) ;
622+ env. services . push ( svc) ;
623+
624+ let mut profile = dummy_profile ( WorkflowStepMode :: Run , WorkflowStepMode :: Plan ) ;
625+ profile. approval = ApprovalMode :: Prompt ;
626+ profile. apply = true ;
627+ profile. deploy_context = Some ( "minikube" . to_string ( ) ) ;
628+
629+ let planner =
630+ WorkflowPlanner :: new ( profile, Arc :: new ( env) , dummy_options ( true ) , dummy_runner ( ) ) ;
631+ let plan = planner. plan ( ) . unwrap ( ) ;
632+
633+ // Check tasks in plan
634+ let task_kinds: Vec < _ > = plan. tasks . iter ( ) . map ( |t| t. kind ) . collect ( ) ;
635+ assert ! ( task_kinds. contains( & WorkflowTaskKind :: DeploymentPlan ) ) ;
636+ assert ! ( task_kinds. contains( & WorkflowTaskKind :: Approval ) ) ;
637+ assert ! ( task_kinds. contains( & WorkflowTaskKind :: Deploy ) ) ;
638+
639+ let ( pipeline, _) = planner. build_pipeline_from_plan ( & plan) . unwrap ( ) ;
640+ let mut task_names: Vec < String > = pipeline. tasks ( ) . map ( |t| t. name . clone ( ) ) . collect ( ) ;
641+ task_names. sort ( ) ;
642+
643+ let mut expected = vec ! [
644+ "workflow:validate-config" . to_string( ) ,
645+ "workflow:build-plan" . to_string( ) ,
646+ "workflow:generate" . to_string( ) ,
647+ "workflow:deployment-plan" . to_string( ) ,
648+ "workflow:approval" . to_string( ) ,
649+ "workflow:deploy" . to_string( ) ,
650+ ] ;
651+ expected. sort ( ) ;
652+ assert_eq ! ( task_names, expected) ;
653+ }
421654}
0 commit comments