feat(config): allow project targeting via env vars (#823)

* feat(config): allow project targeting via env vars without railway link

Add support for RAILWAY_PROJECT_ID, RAILWAY_ENVIRONMENT_ID, and
RAILWAY_SERVICE_ID env vars to configure the target project/environment
without requiring `railway link`. Useful for CI/CD and scripting.

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

* run cargo fmt

* fix(config): update environment variable checks for project targeting

Modify the `has_env_var_project_config` function to return true if either `RAILWAY_PROJECT_ID` or `RAILWAY_ENVIRONMENT_ID` is set, allowing for more flexible project targeting. Additionally, enforce that both environment variables must be set together in the relevant section of the code, providing clearer error messaging when one is missing.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mahmoud Abdelwahab 2026-03-25 22:00:47 +02:00 committed by GitHub
parent 34a635ab34
commit 8ab159ce6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 3 deletions

View file

@ -5,7 +5,7 @@ use std::{
path::PathBuf,
};
use anyhow::{Context, Result, anyhow};
use anyhow::{Context, Result, anyhow, bail};
use colored::Colorize;
use inquire::ui::{Attributes, RenderConfig, StyleSheet, Styled};
use serde::{Deserialize, Serialize};
@ -123,6 +123,24 @@ impl Configs {
std::env::var(consts::RAILWAY_API_TOKEN_ENV).ok()
}
pub fn get_railway_project_id() -> Option<String> {
std::env::var(consts::RAILWAY_PROJECT_ID_ENV).ok()
}
pub fn get_railway_environment_id() -> Option<String> {
std::env::var(consts::RAILWAY_ENVIRONMENT_ID_ENV).ok()
}
pub fn get_railway_service_id() -> Option<String> {
std::env::var(consts::RAILWAY_SERVICE_ID_ENV).ok()
}
/// Returns true if either RAILWAY_PROJECT_ID or RAILWAY_ENVIRONMENT_ID env vars are set,
/// indicating the user intends to use env-var-based project targeting.
pub fn has_env_var_project_config() -> bool {
Self::get_railway_project_id().is_some() || Self::get_railway_environment_id().is_some()
}
/// Returns true if using token-based auth (RAILWAY_TOKEN or RAILWAY_API_TOKEN)
/// rather than session-based auth from `railway login`.
/// Token-based auth bypasses 2FA on the backend, so client-side 2FA checks are unnecessary.
@ -227,7 +245,7 @@ impl Configs {
}
pub fn get_closest_linked_project_directory(&self) -> Result<String> {
if Self::get_railway_token().is_some() {
if Self::has_env_var_project_config() || Self::get_railway_token().is_some() {
return self.get_current_directory();
}
@ -291,6 +309,41 @@ impl Configs {
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 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));
return Ok(LinkedProject {
project_path: self.get_current_directory()?,
name: None,
project: project_id,
environment: environment_id,
environment_name: None,
service: service_id,
});
}
project
.cloned()
.ok_or_else(|| RailwayError::NoLinkedProject.into())

View file

@ -4,6 +4,9 @@ pub const fn get_user_agent() -> &'static str {
pub const RAILWAY_TOKEN_ENV: &str = "RAILWAY_TOKEN";
pub const RAILWAY_API_TOKEN_ENV: &str = "RAILWAY_API_TOKEN";
pub const RAILWAY_PROJECT_ID_ENV: &str = "RAILWAY_PROJECT_ID";
pub const RAILWAY_ENVIRONMENT_ID_ENV: &str = "RAILWAY_ENVIRONMENT_ID";
pub const RAILWAY_SERVICE_ID_ENV: &str = "RAILWAY_SERVICE_ID";
pub const TICK_STRING: &str = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ ";
pub const NON_INTERACTIVE_FAILURE: &str = "This command is only available in interactive mode";

View file

@ -77,7 +77,7 @@ pub async fn ensure_project_and_environment_exist(
linked_project
.environment_name
.clone()
.unwrap_or("Production".to_string()),
.unwrap_or_else(|| linked_project.environment.clone()),
);
match environment {