@@ -232,7 +232,50 @@ User: "deploy this service"
232232 let project_id = session. project_id . clone ( ) . unwrap_or_default ( ) ;
233233 let environment_id = session. environment_id . clone ( ) ;
234234
235- // 4. Get available providers
235+ // 4. Check for existing deployment configs (duplicate detection)
236+ let existing_configs = match client. list_deployment_configs ( & project_id) . await {
237+ Ok ( configs) => configs,
238+ Err ( e) => {
239+ // Non-fatal - continue without duplicate detection
240+ tracing:: warn!( "Failed to fetch existing configs: {}" , e) ;
241+ Vec :: new ( )
242+ }
243+ } ;
244+
245+ // Get service name early to check for duplicates
246+ let service_name = get_service_name ( & analysis_path) ;
247+
248+ // Find existing config with same service name
249+ let existing_config = existing_configs
250+ . iter ( )
251+ . find ( |c| c. service_name . eq_ignore_ascii_case ( & service_name) ) ;
252+
253+ // 5. Get environment info for display
254+ let environments = match client. list_environments ( & project_id) . await {
255+ Ok ( envs) => envs,
256+ Err ( _) => Vec :: new ( ) ,
257+ } ;
258+
259+ // Resolve environment name for display
260+ let ( resolved_env_id, resolved_env_name, is_production) = if let Some ( ref env_id) = environment_id {
261+ let env = environments. iter ( ) . find ( |e| e. id == * env_id) ;
262+ let name = env. map ( |e| e. name . clone ( ) ) . unwrap_or_else ( || "Unknown" . to_string ( ) ) ;
263+ let is_prod = name. to_lowercase ( ) . contains ( "prod" ) ;
264+ ( env_id. clone ( ) , name, is_prod)
265+ } else if let Some ( existing) = & existing_config {
266+ // Use the environment from existing config
267+ let env = environments. iter ( ) . find ( |e| e. id == existing. environment_id ) ;
268+ let name = env. map ( |e| e. name . clone ( ) ) . unwrap_or_else ( || "Unknown" . to_string ( ) ) ;
269+ let is_prod = name. to_lowercase ( ) . contains ( "prod" ) ;
270+ ( existing. environment_id . clone ( ) , name, is_prod)
271+ } else if let Some ( first_env) = environments. first ( ) {
272+ let is_prod = first_env. name . to_lowercase ( ) . contains ( "prod" ) ;
273+ ( first_env. id . clone ( ) , first_env. name . clone ( ) , is_prod)
274+ } else {
275+ ( "" . to_string ( ) , "No environment" . to_string ( ) , false )
276+ } ;
277+
278+ // 6. Get available providers
236279 let capabilities = match get_provider_deployment_statuses ( & client, & project_id) . await {
237280 Ok ( c) => c,
238281 Err ( e) => {
@@ -297,13 +340,63 @@ User: "deploy this service"
297340 . map ( |i| i. has_kubernetes )
298341 . unwrap_or ( false ) ;
299342
300- // 8. Get service name
301- let service_name = get_service_name ( & analysis_path) ;
302-
303- // 9. If preview_only, return recommendation
343+ // 10. If preview_only, return recommendation
304344 if args. preview_only {
345+ // Build the deployment mode info
346+ let ( deployment_mode, mode_explanation, next_steps) = if let Some ( existing) = & existing_config {
347+ (
348+ "REDEPLOY" ,
349+ format ! (
350+ "Service '{}' already has a deployment config (ID: {}). Deploying will trigger a REDEPLOY of the existing service." ,
351+ existing. service_name, existing. id
352+ ) ,
353+ vec ! [
354+ "To redeploy with current config: call deploy_service with preview_only=false" . to_string( ) ,
355+ "This will trigger a new deployment of the existing service" . to_string( ) ,
356+ "The existing configuration will be used" . to_string( ) ,
357+ ]
358+ )
359+ } else {
360+ (
361+ "NEW_DEPLOYMENT" ,
362+ format ! (
363+ "No existing deployment config found for '{}'. This will create a NEW deployment configuration." ,
364+ service_name
365+ ) ,
366+ vec ! [
367+ "To deploy with these settings: call deploy_service with preview_only=false" . to_string( ) ,
368+ "To customize: specify provider, machine_type, region, or port parameters" . to_string( ) ,
369+ "To see more options: check the alternatives section above" . to_string( ) ,
370+ ]
371+ )
372+ } ;
373+
374+ // Production warning
375+ let production_warning = if is_production {
376+ Some ( "⚠️ WARNING: This will deploy to PRODUCTION environment. Please confirm you intend to deploy to production." )
377+ } else {
378+ None
379+ } ;
380+
305381 let response = json ! ( {
306382 "status" : "recommendation" ,
383+ "deployment_mode" : deployment_mode,
384+ "mode_explanation" : mode_explanation,
385+ "environment" : {
386+ "id" : resolved_env_id,
387+ "name" : resolved_env_name,
388+ "is_production" : is_production,
389+ } ,
390+ "production_warning" : production_warning,
391+ "existing_config" : existing_config. map( |c| json!( {
392+ "id" : c. id,
393+ "service_name" : c. service_name,
394+ "environment_id" : c. environment_id,
395+ "branch" : c. branch,
396+ "port" : c. port,
397+ "auto_deploy_enabled" : c. auto_deploy_enabled,
398+ "created_at" : c. created_at. to_rfc3339( ) ,
399+ } ) ) ,
307400 "analysis" : {
308401 "path" : analysis_path. display( ) . to_string( ) ,
309402 "language" : primary_language,
@@ -345,26 +438,73 @@ User: "deploy this service"
345438 } ) ) . collect:: <Vec <_>>( ) ,
346439 } ,
347440 "service_name" : service_name,
348- "next_steps" : [
349- "To deploy with these settings: call deploy_service with preview_only=false" ,
350- "To customize: specify provider, machine_type, region, or port parameters" ,
351- "To see more options: check the alternatives section above" ,
352- ] ,
353- "confirmation_prompt" : format!(
354- "Deploy '{}' to {} ({}) with {} in {}?" ,
355- service_name,
356- recommendation. provider. display_name( ) ,
357- recommendation. target. display_name( ) ,
358- recommendation. machine_type,
359- recommendation. region
360- ) ,
441+ "next_steps" : next_steps,
442+ "confirmation_prompt" : if existing_config. is_some( ) {
443+ format!(
444+ "REDEPLOY '{}' to {} environment?{}" ,
445+ service_name,
446+ resolved_env_name,
447+ if is_production { " ⚠️ (PRODUCTION)" } else { "" }
448+ )
449+ } else {
450+ format!(
451+ "Deploy NEW service '{}' to {} ({}) with {} in {} on {} environment?{}" ,
452+ service_name,
453+ recommendation. provider. display_name( ) ,
454+ recommendation. target. display_name( ) ,
455+ recommendation. machine_type,
456+ recommendation. region,
457+ resolved_env_name,
458+ if is_production { " ⚠️ (PRODUCTION)" } else { "" }
459+ )
460+ } ,
361461 } ) ;
362462
363463 return serde_json:: to_string_pretty ( & response)
364464 . map_err ( |e| DeployServiceError ( format ! ( "Failed to serialize: {}" , e) ) ) ;
365465 }
366466
367- // 10. Execute deployment
467+ // 11. Execute deployment - EITHER redeploy existing OR create new
468+
469+ // If existing config found, trigger redeploy instead of creating new config
470+ if let Some ( existing) = & existing_config {
471+ let trigger_request = TriggerDeploymentRequest {
472+ project_id : project_id. clone ( ) ,
473+ config_id : existing. id . clone ( ) ,
474+ commit_sha : None ,
475+ } ;
476+
477+ return match client. trigger_deployment ( & trigger_request) . await {
478+ Ok ( response) => {
479+ let result = json ! ( {
480+ "status" : "redeployed" ,
481+ "deployment_mode" : "REDEPLOY" ,
482+ "config_id" : existing. id,
483+ "task_id" : response. backstage_task_id,
484+ "service_name" : service_name,
485+ "environment" : {
486+ "id" : resolved_env_id,
487+ "name" : resolved_env_name,
488+ "is_production" : is_production,
489+ } ,
490+ "message" : format!(
491+ "Redeploy triggered for existing service '{}' on {} environment. Task ID: {}" ,
492+ service_name, resolved_env_name, response. backstage_task_id
493+ ) ,
494+ "next_steps" : [
495+ format!( "Monitor progress: use get_deployment_status with task_id '{}'" , response. backstage_task_id) ,
496+ "View logs after deployment: use get_service_logs" ,
497+ ] ,
498+ } ) ;
499+
500+ serde_json:: to_string_pretty ( & result)
501+ . map_err ( |e| DeployServiceError ( format ! ( "Failed to serialize: {}" , e) ) )
502+ }
503+ Err ( e) => Ok ( format_api_error ( "deploy_service" , e) ) ,
504+ } ;
505+ }
506+
507+ // NEW DEPLOYMENT PATH - no existing config found
368508 let final_provider = args. provider
369509 . as_ref ( )
370510 . and_then ( |p| CloudProvider :: from_str ( p) . ok ( ) )
@@ -409,36 +549,15 @@ User: "deploy this service"
409549 }
410550 } ;
411551
412- // Get environment from session or use default
413- let env_id = match & environment_id {
414- Some ( id) => id. clone ( ) ,
415- None => {
416- // Try to get environments from API
417- match client. list_environments ( & project_id) . await {
418- Ok ( envs) => {
419- match envs. first ( ) {
420- Some ( env) => env. id . clone ( ) ,
421- None => {
422- return Ok ( format_error_for_llm (
423- "deploy_service" ,
424- ErrorCategory :: ResourceUnavailable ,
425- "No environment found for project" ,
426- Some ( vec ! [ "Create an environment in the platform first" ] ) ,
427- ) ) ;
428- }
429- }
430- }
431- Err ( e) => {
432- return Ok ( format_error_for_llm (
433- "deploy_service" ,
434- ErrorCategory :: NetworkError ,
435- & format ! ( "Failed to get environments: {}" , e) ,
436- None ,
437- ) ) ;
438- }
439- }
440- }
441- } ;
552+ // Use resolved environment ID from earlier
553+ if resolved_env_id. is_empty ( ) {
554+ return Ok ( format_error_for_llm (
555+ "deploy_service" ,
556+ ErrorCategory :: ResourceUnavailable ,
557+ "No environment found for project" ,
558+ Some ( vec ! [ "Create an environment in the platform first" ] ) ,
559+ ) ) ;
560+ }
442561
443562 // Build deployment config request
444563 // Derive dockerfile path and build context from DockerfileInfo
@@ -485,7 +604,7 @@ User: "deploy this service"
485604 branch : repo. default_branch . clone ( ) . unwrap_or_else ( || "main" . to_string ( ) ) ,
486605 target_type : recommendation. target . as_str ( ) . to_string ( ) ,
487606 cloud_provider : final_provider. as_str ( ) . to_string ( ) ,
488- environment_id : env_id . clone ( ) ,
607+ environment_id : resolved_env_id . clone ( ) ,
489608 cluster_id : None , // Cloud Runner doesn't need cluster
490609 registry_id : None , // Auto-provision
491610 auto_deploy_enabled : true ,
@@ -512,16 +631,22 @@ User: "deploy this service"
512631 Ok ( response) => {
513632 let result = json ! ( {
514633 "status" : "deployed" ,
634+ "deployment_mode" : "NEW_DEPLOYMENT" ,
515635 "config_id" : config. id,
516636 "task_id" : response. backstage_task_id,
517637 "service_name" : service_name,
638+ "environment" : {
639+ "id" : resolved_env_id,
640+ "name" : resolved_env_name,
641+ "is_production" : is_production,
642+ } ,
518643 "provider" : final_provider. as_str( ) ,
519644 "machine_type" : final_machine,
520645 "region" : final_region,
521646 "port" : final_port,
522647 "message" : format!(
523- "Deployment started for '{}'. Task ID: {}" ,
524- service_name, response. backstage_task_id
648+ "NEW deployment started for '{}' on {} environment . Task ID: {}" ,
649+ service_name, resolved_env_name , response. backstage_task_id
525650 ) ,
526651 "next_steps" : [
527652 format!( "Monitor progress: use get_deployment_status with task_id '{}'" , response. backstage_task_id) ,
0 commit comments