From cb5780825b6da2410618c24f88bd5b3f5406ff10 Mon Sep 17 00:00:00 2001 From: Jake Runzer Date: Thu, 16 Jul 2026 01:58:09 -0400 Subject: [PATCH 1/2] fix(add): don't fail after service creation when project context is env-injected railway add resolved RAILWAY_PROJECT_ID/RAILWAY_ENVIRONMENT_ID for the serviceCreate mutation, then errored in link_service because there is no on-disk link entry to update in that mode. The service was created but the CLI exited nonzero without ever printing the new service id, so callers (CI, cloud agents) treated creation as failed and picked wrong targets. - add: print the created {id, name} before local-link bookkeeping and skip linking entirely under env-var/token targeting - deploy fetch_and_create: same skip for the post-create auto-link - config: name the predicate (uses_env_project_targeting) and reuse it in get_closest_linked_project_directory Claude-Session: https://claude.ai/code/session_01LPDihPsFce5vyxdbNGvV8S --- src/commands/add.rs | 18 ++++++++++++++---- src/commands/deploy.rs | 12 +++++++++--- src/config.rs | 9 ++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/commands/add.rs b/src/commands/add.rs index c954e6b42..3d84e9f8e 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -337,8 +337,10 @@ async fn create_service( } let s = post_graphql::(client, &configs.get_backboard(), vars).await?; - configs.link_service(s.service_create.id.clone())?; - configs.write()?; + + // Report the created service before local-link bookkeeping so a config + // write failure can never mask a successful creation. + let will_link = !Configs::uses_env_project_targeting(); if json { println!( "{}", @@ -346,10 +348,18 @@ 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 { "" } )); } + + // Env-var / token targeted runs have no on-disk link entry to update; + // attempting to would fail with ProjectNotFound. + 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..6a25b6370 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -213,8 +213,14 @@ 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() { + // Auto-link if should_link is true and no service is currently linked. + // Env-var / token targeted runs have no on-disk link entry to update; + // attempting to 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 +247,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/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(); } From c3e974227e86894c1435deb746d16344997a0216 Mon Sep 17 00:00:00 2001 From: Jake Runzer Date: Thu, 16 Jul 2026 02:03:18 -0400 Subject: [PATCH 2/2] refactor: reuse uses_env_project_targeting, tighten comments Claude-Session: https://claude.ai/code/session_01LPDihPsFce5vyxdbNGvV8S --- src/commands/add.rs | 7 ++++--- src/commands/deploy.rs | 3 +-- src/commands/ssh/config.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/add.rs b/src/commands/add.rs index 3d84e9f8e..e53373237 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -338,9 +338,12 @@ async fn create_service( let s = post_graphql::(client, &configs.get_backboard(), vars).await?; + // 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. - let will_link = !Configs::uses_env_project_targeting(); if json { println!( "{}", @@ -354,8 +357,6 @@ async fn create_service( )); } - // Env-var / token targeted runs have no on-disk link entry to update; - // attempting to would fail with ProjectNotFound. if will_link { configs.link_service(s.service_create.id.clone())?; configs.write()?; diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 6a25b6370..287b7169a 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -213,9 +213,8 @@ 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. // Env-var / token targeted runs have no on-disk link entry to update; - // attempting to would fail with ProjectNotFound after the template was + // linking there would fail with ProjectNotFound after the template was // already deployed. let should_auto_link = options.should_link && linked_project.service.is_none() 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(()); }