fix(config): make RAILWAY_ENVIRONMENT_ID optional for project-level commands (#826)

* fix(config): make RAILWAY_ENVIRONMENT_ID optional for project-level commands

Commands like `env new`, `env delete`, and `env link` only need a
project ID, not an environment. The previous validation required both
RAILWAY_PROJECT_ID and RAILWAY_ENVIRONMENT_ID to be set together,
which made these commands unusable with just RAILWAY_PROJECT_ID.

Make `LinkedProject.environment` an `Option<String>` and remove the
blanket XOR validation. Commands that need an environment now error
at the point of use with a clear message, while project-level
commands work with just RAILWAY_PROJECT_ID set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: context-aware error messages for missing environment

When RAILWAY_PROJECT_ID is set, tell the user to set
RAILWAY_ENVIRONMENT_ID (since `railway environment` writes to local
config which is ignored when env vars take priority). When using local
config, suggest `railway environment` as before.

Also consolidates duplicated error messages across callers into the
central `environment_id()` helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify error message for missing environment

The environment is only None when using env-var targeting (local config
always has an environment set via link_project), so drop the
unreachable branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reject env-only override and defer environment preflight

P1: Reject RAILWAY_ENVIRONMENT_ID set without RAILWAY_PROJECT_ID
instead of silently falling back to local config.

P2: Make ensure_project_and_environment_exist() skip the environment
check when no environment is linked. Callers that accept --environment
resolve and validate it themselves, so the preflight no longer blocks
them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add tests for env var project targeting validation

Extract resolve_env_var_project() from get_linked_project() so the
env var validation logic can be tested without Configs/auth/API deps.

Two tests:
- PROJECT_ID alone → succeeds with environment None
- ENVIRONMENT_ID alone → rejected

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: improve error messages and graceful status for missing environment

Provide actionable guidance (set env var, use --environment, or run
railway environment) and show "None" in status instead of crashing
when no environment is linked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix rustfmt formatting in status.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: merge locally linked environment in project-only env-var mode

When RAILWAY_PROJECT_ID is set without RAILWAY_ENVIRONMENT_ID, fall
back to the environment from the local config so that `railway
environment` remains a valid remediation path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only merge local environment when project IDs match

Prevents silently using project A's environment with project B when
RAILWAY_PROJECT_ID overrides the locally linked project.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve stale env name, nested dir lookup, and down panic in env-var mode

- Don't carry local environment_name when RAILWAY_ENVIRONMENT_ID override
  is set, preventing preflight from validating against the wrong environment
- Use ancestor-walking lookup for local project so nested directories still
  find the linked config in env-var mode
- Replace expect() with fallback in `down` command to avoid panic when
  project/environment names are absent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mahmoud Abdelwahab 2026-03-30 11:05:18 +02:00 committed by GitHub
parent cc8cc6ec0d
commit 3ca7e63637
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 240 additions and 113 deletions

View file

@ -320,7 +320,7 @@ async fn create_service(
let vars = mutations::service_create::Variables {
name: service,
project_id: linked_project.project.clone(),
environment_id: linked_project.environment.clone(),
environment_id: linked_project.environment_id()?.to_string(),
source: Some(source),
variables,
branch,

View file

@ -137,10 +137,10 @@ pub async fn command(args: Args) -> Result<()> {
ensure_project_and_environment_exist(&client, &configs, &linked_project).await?;
let project = get_project(&client, &configs, linked_project.project.clone()).await?;
let environment_input = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment_input = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let environment = get_matched_environment(&project, environment_input)?;
let environment_config = fetch_environment_config(&client, &configs, &environment.id, false)
.await?

View file

@ -38,10 +38,10 @@ pub async fn command(args: Args) -> Result<()> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let linked_project = configs.get_linked_project().await?;
let environment = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let project = get_project(&client, &configs, linked_project.project.clone()).await?;

View file

@ -172,7 +172,7 @@ pub async fn fetch_and_create(
let mutation_vars = mutations::template_deploy::Variables {
project_id: linked_project.project.clone(),
environment_id: linked_project.environment.clone(),
environment_id: linked_project.environment_id()?.to_string(),
template_id: details.template.id.clone(),
serialized_config: serde_json::to_value(&config).context("Failed to serialize config")?,
};

View file

@ -108,7 +108,10 @@ async fn list_deployments(
};
let project = get_project(&client, &configs, linked_project.project.clone()).await?;
let environment = environment.unwrap_or(linked_project.environment.clone());
let environment = match environment {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let environment_id = get_matched_environment(&project, environment)?.id;
let service_id = if let Some(service_name_or_id) = service {

View file

@ -260,7 +260,7 @@ async fn configure_command(args: ConfigureArgs) -> Result<()> {
.collect();
let project_id = linked_project.project.clone();
let environment_id = linked_project.environment.clone();
let environment_id = linked_project.environment_id()?.to_string();
let env_response = fetch_environment_config(&client, &configs, &environment_id, false).await?;
let config = env_response.config;
@ -841,10 +841,10 @@ async fn up_command(args: UpArgs) -> Result<()> {
.collect();
let project_id = linked_project.project.clone();
let environment_id = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment_id = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let env_response = fetch_environment_config(&client, &configs, &environment_id, true).await?;
let env_name = env_response.name;

View file

@ -61,7 +61,7 @@ async fn create_service_domain(service_name: Option<String>, json: bool) -> Resu
let vars = queries::domains::Variables {
project_id: linked_project.project.clone(),
environment_id: linked_project.environment.clone(),
environment_id: linked_project.environment_id()?.to_string(),
service_id: service.id.clone(),
};
@ -80,7 +80,7 @@ async fn create_service_domain(service_name: Option<String>, json: bool) -> Resu
let vars = mutations::service_domain_create::Variables {
service_id: service.id.clone(),
environment_id: linked_project.environment.clone(),
environment_id: linked_project.environment_id()?.to_string(),
};
let domain =
post_graphql::<mutations::ServiceDomainCreate, _>(&client, configs.get_backboard(), vars)
@ -264,7 +264,7 @@ async fn create_custom_domain(
let vars = mutations::custom_domain_create::Variables {
input: mutations::custom_domain_create::CustomDomainCreateInput {
domain: domain.clone(),
environment_id: linked_project.environment.clone(),
environment_id: linked_project.environment_id()?.to_string(),
project_id: linked_project.project.clone(),
service_id: service.id.clone(),
target_port: port.map(|p| p as i64),

View file

@ -36,10 +36,10 @@ pub async fn command(args: Args) -> Result<()> {
let project = get_project(&client, &configs, linked_project.project.clone()).await?;
let environment = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let services = project.services.edges.iter().collect::<Vec<_>>();
@ -72,11 +72,13 @@ pub async fn command(args: Args) -> Result<()> {
let linked_project_name = linked_project
.name
.expect("Linked project is missing the name");
.as_deref()
.unwrap_or(linked_project.project.as_str());
let linked_environment_name = linked_project
.environment_name
.expect("Linked environment is missing the name");
.as_deref()
.unwrap_or(linked_project.environment.as_deref().unwrap_or("unknown"));
let linked_project_environment = format!(
"{} environment of project {}",

View file

@ -178,7 +178,7 @@ fn resolve_environment(
bail!(RailwayError::EnvironmentNotFound(env_input.clone()))
}
} else {
let env_id = linked_project.environment.clone();
let env_id = linked_project.environment_id()?.to_string();
let env_name = project
.environments
.edges

View file

@ -178,7 +178,7 @@ fn resolve_environment(
}
} else {
// Use linked environment
let env_id = linked_project.environment.clone();
let env_id = linked_project.environment_id()?.to_string();
let env_name = project
.environments
.edges

View file

@ -111,10 +111,10 @@ pub async fn command(args: Args) -> Result<()> {
let client = GQLClient::new_authorized(&configs)?;
let linked_project = configs.get_linked_project().await?;
let project = get_project(&client, &configs, linked_project.project.clone()).await?;
let environment_input = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment_input = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let environment = project
.environments
.edges

View file

@ -294,10 +294,10 @@ pub async fn command(args: Args) -> Result<()> {
let project = get_project(&client, &configs, linked_project.project.clone()).await?;
let environment = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let services = project.services.edges.iter().collect::<Vec<_>>();

View file

@ -81,7 +81,7 @@ impl RailwayMcp {
.map_err(|e| McpError::internal_error(format!("Failed to get project: {e}"), None))?;
let env_id_or_name = environment_id
.or_else(|| linked.as_ref().map(|l| l.environment.clone()))
.or_else(|| linked.as_ref().and_then(|l| l.environment.clone()))
.ok_or_else(|| {
let available = format_environments(&project);
McpError::invalid_params(
@ -689,7 +689,7 @@ impl RailwayMcp {
.map_err(|e| McpError::internal_error(format!("Failed to get project: {e}"), None))?;
// Filter to services present in the linked environment (matches CLI behavior)
let environment_id = linked.as_ref().map(|l| l.environment.as_str());
let environment_id = linked.as_ref().and_then(|l| l.environment.as_deref());
let env_service_ids = environment_id
.and_then(|eid| project.environments.edges.iter().find(|e| e.node.id == eid))
.map(|e| {

View file

@ -264,7 +264,7 @@ impl RailwayMcp {
let linked = self.configs.get_linked_project().await.ok();
let environment_id = params
.environment_id
.or_else(|| linked.as_ref().map(|l| l.environment.clone()))
.or_else(|| linked.as_ref().and_then(|l| l.environment.clone()))
.ok_or_else(|| {
McpError::invalid_params(
"environment_id is required when updating mount_path.",

View file

@ -21,7 +21,8 @@ pub async fn command(args: Args) -> Result<()> {
let url = format!(
"https://{hostname}/project/{}?environmentId={}",
linked_project.project, linked_project.environment
linked_project.project,
linked_project.environment_id()?
);
if args.print || !std::io::stdout().is_terminal() {

View file

@ -49,9 +49,10 @@ pub async fn command(args: Args) -> Result<()> {
.ok_or_else(|| anyhow!(RailwayError::ServiceNotFound(service_id)))?;
let service_in_env =
find_service_instance(&project, &linked_project.environment, &service.node.id).ok_or_else(
|| anyhow!("The service specified doesn't exist in the current environment"),
)?;
find_service_instance(&project, linked_project.environment_id()?, &service.node.id)
.ok_or_else(|| {
anyhow!("The service specified doesn't exist in the current environment")
})?;
let Some(ref latest) = service_in_env.latest_deployment else {
bail!("No deployment found for service")

View file

@ -59,9 +59,10 @@ pub async fn command(args: Args) -> Result<()> {
.ok_or_else(|| anyhow!(RailwayError::ServiceNotFound(service_id)))?;
let service_in_env =
find_service_instance(&project, &linked_project.environment, &service.node.id).ok_or_else(
|| anyhow!("The service specified doesn't exist in the current environment"),
)?;
find_service_instance(&project, linked_project.environment_id()?, &service.node.id)
.ok_or_else(|| {
anyhow!("The service specified doesn't exist in the current environment")
})?;
let Some(ref latest) = service_in_env.latest_deployment else {
bail!("No deployment found for service")

View file

@ -118,10 +118,14 @@ pub async fn command(args: Args) -> Result<()> {
let environment = args
.environment
.clone()
.or_else(|| linked_project.as_ref().map(|lp| lp.environment.clone()))
.or_else(|| {
linked_project
.as_ref()
.and_then(|lp| lp.environment.clone())
})
.ok_or_else(|| {
anyhow::anyhow!(
"No environment specified. Use --environment or run `railway link` first"
"No environment specified. Set RAILWAY_ENVIRONMENT_ID, use --environment, or run `railway environment` to link one."
)
})?;

View file

@ -53,10 +53,10 @@ pub async fn command(args: Args) -> Result<()> {
)
.await?
.project;
let environment = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let (existing, service_id) =
get_existing_config(&args, &linked_project, &project, &environment)?;
let new_config = convert_hashmap_to_map(

View file

@ -114,7 +114,7 @@ async fn link_command(args: LinkArgs) -> Result<()> {
ensure_project_and_environment_exist(&client, &configs, &linked_project).await?;
let service_ids_in_env = get_service_ids_in_env(&project, &linked_project.environment);
let service_ids_in_env = get_service_ids_in_env(&project, linked_project.environment_id()?);
let services: Vec<_> = project
.services
.edges
@ -156,7 +156,7 @@ async fn status_command(args: StatusArgs) -> Result<()> {
let env = get_matched_environment(&project, env_name)?;
env.id
} else {
linked_project.environment.clone()
linked_project.environment_id()?.to_string()
};
let environment_name = project

View file

@ -63,18 +63,18 @@ pub async fn command(args: Args) -> Result<()> {
&client,
&configs,
linked_project.project.clone(),
linked_project.environment.clone(),
linked_project.environment_id()?.to_string(),
service_id.node.id.clone(),
)
.await?;
all_variables.append(&mut variables);
} else if let Some(service) = linked_project.service {
} else if let Some(ref service) = linked_project.service {
let mut variables = get_service_variables(
&client,
&configs,
linked_project.project.clone(),
linked_project.environment.clone(),
linked_project.environment_id()?.to_string(),
service.clone(),
)
.await?;

View file

@ -119,7 +119,11 @@ pub async fn get_ssh_connect_params(
let environment = if let Some(env) = args.environment {
env
} else {
linked_project.as_ref().unwrap().environment.clone()
linked_project
.as_ref()
.unwrap()
.environment_id()?
.to_string()
};
let environment_id = get_matched_environment(&project, environment)?.id;

View file

@ -20,19 +20,18 @@ pub async fn command(args: Args) -> Result<()> {
if !args.json {
println!("Project: {}", project.name.purple().bold());
println!(
"Environment: {}",
if let Some(env_name) = linked_project.environment.as_deref().and_then(|eid| {
project
.environments
.edges
.iter()
.map(|env| &env.node)
.find(|env| env.id == linked_project.environment)
.context("Environment not found!")?
.name
.blue()
.bold()
);
.find(|env| env.node.id == eid)
.map(|env| env.node.name.as_str())
}) {
println!("Environment: {}", env_name.blue().bold());
} else {
println!("Environment: {}", "None".red().bold());
}
if let Some(linked_service) = linked_project.service {
if let Some(service) = project

View file

@ -99,10 +99,14 @@ pub async fn command(args: Args) -> Result<()> {
let environment = args
.environment
.clone()
.or_else(|| linked_project.as_ref().map(|lp| lp.environment.clone()))
.or_else(|| {
linked_project
.as_ref()
.and_then(|lp| lp.environment.clone())
})
.ok_or_else(|| {
anyhow::anyhow!(
"No environment specified. Use --environment or run `railway link` first"
"No environment specified. Set RAILWAY_ENVIRONMENT_ID, use --environment, or run `railway environment` to link one."
)
})?;
let environment_id = get_matched_environment(&project, environment)?.id;

View file

@ -134,10 +134,10 @@ pub async fn command(args: Args) -> Result<()> {
let project = get_project(&client, &configs, linked_project.project.clone()).await?;
let service = args.service.or_else(|| linked_project.service.clone());
let environment = args
.environment
.clone()
.unwrap_or(linked_project.environment.clone());
let environment = match args.environment.clone() {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
match args.command {
Commands::Add(a) => add(service, environment, a.mount_path, project, a.json).await?,

View file

@ -24,11 +24,22 @@ pub struct LinkedProject {
pub project_path: String,
pub name: Option<String>,
pub project: String,
pub environment: String,
pub environment: Option<String>,
pub environment_name: Option<String>,
pub service: Option<String>,
}
impl LinkedProject {
/// Returns the environment ID, or an error if no environment is linked.
pub fn environment_id(&self) -> Result<&str> {
self.environment.as_deref().ok_or_else(|| {
anyhow!(
"No environment specified. Set RAILWAY_ENVIRONMENT_ID, use --environment, or run `railway environment` to link one."
)
})
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde_with::skip_serializing_none]
#[serde(rename_all = "camelCase")]
@ -302,44 +313,47 @@ impl Configs {
project_path: self.get_current_directory()?,
name: Some(data.project_token.project.name),
project: data.project_token.project.id,
environment: data.project_token.environment.id,
environment: Some(data.project_token.environment.id),
environment_name: Some(data.project_token.environment.name),
service: project.cloned().and_then(|p| p.service),
};
return Ok(project);
}
let has_project_id = Self::get_railway_project_id().is_some();
let has_environment_id = Self::get_railway_environment_id().is_some();
if has_project_id != has_environment_id {
bail!(
"Both RAILWAY_PROJECT_ID and RAILWAY_ENVIRONMENT_ID must be set together. {} is missing.",
if has_project_id {
"RAILWAY_ENVIRONMENT_ID"
} else {
"RAILWAY_PROJECT_ID"
}
);
}
if let (Some(project_id), Some(environment_id)) = (
Self::get_railway_project_id(),
Self::get_railway_environment_id(),
) {
if let Some(resolved) = Self::resolve_env_var_project()? {
if self.get_railway_auth_token().is_none() {
bail!(RailwayError::Unauthorized);
}
let service_id =
Self::get_railway_service_id().or_else(|| project.cloned().and_then(|p| p.service));
// Only merge local config when it targets the same project,
// to avoid silently mixing project A's environment with project B.
// Walk ancestor directories so nested dirs still find the local link.
let local = self
.get_local_linked_project()
.ok()
.filter(|p| p.project == resolved.project_id);
let service_id = Self::get_railway_service_id()
.or_else(|| local.as_ref().and_then(|p| p.service.clone()));
let env_from_override = resolved.environment_id.is_some();
let environment = resolved
.environment_id
.or_else(|| local.as_ref().and_then(|p| p.environment.clone()));
// Only carry the local environment name when we fell back to the
// local environment ID. If the override supplied its own ID, the
// local name would refer to a different environment.
let environment_name = if !env_from_override && environment.is_some() {
local.as_ref().and_then(|p| p.environment_name.clone())
} else {
None
};
return Ok(LinkedProject {
project_path: self.get_current_directory()?,
name: None,
project: project_id,
environment: environment_id,
environment_name: None,
project: resolved.project_id,
environment,
environment_name,
service: service_id,
});
}
@ -368,7 +382,7 @@ impl Configs {
project_path: path.clone(),
name,
project: project_id,
environment: environment_id,
environment: Some(environment_id),
environment_name,
service: None,
};
@ -516,4 +530,93 @@ impl Configs {
Ok(())
}
/// Resolves env-var-based project targeting. Returns:
/// - `Ok(Some(...))` if RAILWAY_PROJECT_ID is set (with optional environment)
/// - `Ok(None)` if neither env var is set (fall through to local config)
/// - `Err(...)` if RAILWAY_ENVIRONMENT_ID is set without RAILWAY_PROJECT_ID
fn resolve_env_var_project() -> Result<Option<ResolvedEnvVarProject>> {
let project_id = Self::get_railway_project_id();
let environment_id = Self::get_railway_environment_id();
match (project_id, environment_id) {
(Some(project_id), env_id) => Ok(Some(ResolvedEnvVarProject {
project_id,
environment_id: env_id,
})),
(None, Some(_)) => {
bail!("RAILWAY_ENVIRONMENT_ID cannot be set without RAILWAY_PROJECT_ID.")
}
(None, None) => Ok(None),
}
}
}
#[derive(Debug)]
struct ResolvedEnvVarProject {
project_id: String,
environment_id: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
// Env var tests must run sequentially to avoid races.
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env_vars<F, R>(vars: &[(&str, Option<&str>)], f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = ENV_LOCK.lock().unwrap();
// SAFETY: tests run sequentially under ENV_LOCK, so no concurrent mutation.
unsafe {
for (key, val) in vars {
match val {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
let result = f();
unsafe {
for (key, _) in vars {
std::env::remove_var(key);
}
}
result
}
#[test]
fn env_var_project_id_only_returns_none_environment() {
let result = with_env_vars(
&[
("RAILWAY_PROJECT_ID", Some("proj-123")),
("RAILWAY_ENVIRONMENT_ID", None),
],
Configs::resolve_env_var_project,
);
let resolved = result.unwrap().expect("should return Some");
assert_eq!(resolved.project_id, "proj-123");
assert!(resolved.environment_id.is_none());
}
#[test]
fn env_var_environment_id_without_project_id_is_rejected() {
let result = with_env_vars(
&[
("RAILWAY_PROJECT_ID", None),
("RAILWAY_ENVIRONMENT_ID", Some("env-456")),
],
Configs::resolve_env_var_project,
);
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("RAILWAY_ENVIRONMENT_ID cannot be set without RAILWAY_PROJECT_ID"),
"unexpected error: {err}"
);
}
}

View file

@ -72,22 +72,24 @@ pub async fn ensure_project_and_environment_exist(
bail!(RailwayError::ProjectDeleted);
}
let environment = get_matched_environment(
&project,
linked_project
.environment_name
.clone()
.unwrap_or_else(|| linked_project.environment.clone()),
);
// Only validate the environment if one is linked; callers that need an
// environment (or accept --environment) resolve and validate it themselves.
if let Some(env_id_or_name) = linked_project
.environment_name
.clone()
.or_else(|| linked_project.environment.clone())
{
let environment = get_matched_environment(&project, env_id_or_name);
match environment {
Ok(environment) => {
if environment.deleted_at.is_some() {
bail!(RailwayError::EnvironmentDeleted);
match environment {
Ok(environment) => {
if environment.deleted_at.is_some() {
bail!(RailwayError::EnvironmentDeleted);
}
}
}
Err(_) => bail!(RailwayError::EnvironmentDeleted),
};
Err(_) => bail!(RailwayError::EnvironmentDeleted),
};
}
Ok(())
}
@ -155,7 +157,10 @@ pub async fn resolve_service_context(
ensure_project_and_environment_exist(&client, &configs, &linked_project).await?;
let project = get_project(&client, &configs, linked_project.project.clone()).await?;
let env = environment_arg.unwrap_or(linked_project.environment.clone());
let env = match environment_arg {
Some(env) => env,
None => linked_project.environment_id()?.to_string(),
};
let environment_id = get_matched_environment(&project, env)?.id;
let services = &project.services.edges;