diff --git a/src/commands/add.rs b/src/commands/add.rs index c954e6b42..e53373237 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -337,8 +337,13 @@ async fn create_service( } let s = post_graphql::(client, &configs.get_backboard(), vars).await?; - configs.link_service(s.service_create.id.clone())?; - configs.write()?; + + // Env-var / token targeted runs have no on-disk link entry to update; + // linking there would fail with ProjectNotFound. + let will_link = !Configs::uses_env_project_targeting(); + + // Report the created service before local-link bookkeeping so a config + // write failure can never mask a successful creation. if json { println!( "{}", @@ -346,10 +351,16 @@ async fn create_service( ); } else if let Some(spinner) = spinner { spinner.finish_with_message(format!( - "Successfully created the service \"{}\" and linked to it", - s.service_create.name.blue() + "Successfully created the service \"{}\"{}", + s.service_create.name.blue(), + if will_link { " and linked to it" } else { "" } )); } + + if will_link { + configs.link_service(s.service_create.id.clone())?; + configs.write()?; + } Ok(()) } diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index a52eedc1c..287b7169a 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -213,8 +213,13 @@ pub async fn fetch_and_create( .iter() .find(|s| !old_service_ids.contains(&s.node.id)); - // Auto-link if should_link is true and no service is currently linked - if options.should_link && linked_project.service.is_none() { + // Env-var / token targeted runs have no on-disk link entry to update; + // linking there would fail with ProjectNotFound after the template was + // already deployed. + let should_auto_link = options.should_link + && linked_project.service.is_none() + && !Configs::uses_env_project_targeting(); + if should_auto_link { if let Some(service) = new_service { configs.link_service(service.node.id.clone())?; configs.write()?; @@ -241,7 +246,7 @@ pub async fn fetch_and_create( println!("{}", output); } else if let Some(spinner) = spinner { let mut msg = format!("🎉 Added {} to project", template_name.green().bold()); - if options.should_link && linked_project.service.is_none() && new_service.is_some() { + if should_auto_link && new_service.is_some() { msg.push_str(" and linked"); } spinner.finish_with_message(msg); diff --git a/src/commands/ssh/config.rs b/src/commands/ssh/config.rs index 4acefeb7f..d6d837349 100644 --- a/src/commands/ssh/config.rs +++ b/src/commands/ssh/config.rs @@ -162,7 +162,7 @@ async fn ensure_default_target_has_linked_service(target: &TargetArgs) -> Result if !std::io::stdout().is_terminal() { return Ok(()); } - if Configs::has_env_var_project_config() || Configs::get_railway_token().is_some() { + if Configs::uses_env_project_targeting() { return Ok(()); } diff --git a/src/config.rs b/src/config.rs index ee9cdb0b6..9df60de8c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -220,6 +220,13 @@ impl Configs { Self::get_railway_token().is_some() || Self::get_railway_api_token().is_some() } + /// True when project targeting comes from env vars or RAILWAY_TOKEN rather + /// than an on-disk link (e.g. CI, cloud agents). In this mode there is no + /// local link entry to read or update. + pub fn uses_env_project_targeting() -> bool { + Self::has_env_var_project_config() || Self::get_railway_token().is_some() + } + /// True when the `CI` env var is set to any truthy value (`true`, /// `1`, `yes`, ...). Runners differ on the value they export, so /// treat anything except empty / `false` / `0` as CI — consistent @@ -348,7 +355,7 @@ impl Configs { } pub fn get_closest_linked_project_directory(&self) -> Result { - if Self::has_env_var_project_config() || Self::get_railway_token().is_some() { + if Self::uses_env_project_targeting() { return self.get_current_directory(); }