mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(ci): staging builds resolve to prod API URL — bake VITE vars into build.yml (#1371)
This commit is contained in:
@@ -68,6 +68,11 @@ jobs:
|
||||
cargo tauri build -c "$TAURI_CONFIG_OVERRIDE" --bundles deb
|
||||
env:
|
||||
NODE_ENV: production
|
||||
# CI builds should point at staging, not production.
|
||||
# Without these, APP_ENV is undefined in config.ts and
|
||||
# DEFAULT_BACKEND_URL falls through to api.tinyhumans.ai.
|
||||
VITE_OPENHUMAN_APP_ENV: staging
|
||||
VITE_BACKEND_URL: https://staging-api.tinyhumans.ai
|
||||
CARGO_PROFILE_RELEASE_OPT_LEVEL: "1"
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16"
|
||||
CARGO_PROFILE_RELEASE_LTO: "false"
|
||||
|
||||
+135
-16
@@ -11,10 +11,12 @@ pub const VITE_APP_ENV_VAR: &str = "VITE_OPENHUMAN_APP_ENV";
|
||||
|
||||
/// Resolves the hosted API base URL (no path suffix).
|
||||
///
|
||||
/// Order: non-empty `api_url` from config → [`api_base_from_env`] (`BACKEND_URL`, then `VITE_BACKEND_URL`)
|
||||
/// → [`default_api_base_url_for_env`](`app_env_from_env().as_deref()`), which selects the
|
||||
/// environment-aware default host based on the current app environment instead of always using
|
||||
/// [`DEFAULT_API_BASE_URL`].
|
||||
/// Order:
|
||||
/// 1. Non-empty `api_url` from config (user explicitly set it)
|
||||
/// 2. `BACKEND_URL` / `VITE_BACKEND_URL` runtime env vars (each checked independently)
|
||||
/// 3. `BACKEND_URL` / `VITE_BACKEND_URL` baked in at compile time via `option_env!`
|
||||
/// 4. Environment-aware default: `app_env_from_env()` == `staging` →
|
||||
/// [`DEFAULT_STAGING_API_BASE_URL`], otherwise [`DEFAULT_API_BASE_URL`]
|
||||
pub fn effective_api_url(api_url: &Option<String>) -> String {
|
||||
if let Some(u) = api_url.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
return normalize_api_base_url(u);
|
||||
@@ -30,22 +32,67 @@ pub fn normalize_api_base_url(url: &str) -> String {
|
||||
url.trim().trim_end_matches('/').to_string()
|
||||
}
|
||||
|
||||
/// Resolve API base from process environment (`BACKEND_URL` first, then `VITE_BACKEND_URL`).
|
||||
/// Resolve API base URL from the environment.
|
||||
///
|
||||
/// Each key is checked independently so that an empty `BACKEND_URL` does not
|
||||
/// shadow a valid `VITE_BACKEND_URL`. Runtime vars are checked first, then
|
||||
/// compile-time values baked in via `option_env!`. The compile-time path is
|
||||
/// what makes a shipped DMG/installer resolve to the correct environment —
|
||||
/// at runtime the process has no shell env vars set.
|
||||
pub fn api_base_from_env() -> Option<String> {
|
||||
std::env::var("BACKEND_URL")
|
||||
.or_else(|_| std::env::var("VITE_BACKEND_URL"))
|
||||
.ok()
|
||||
.map(|s| normalize_api_base_url(&s))
|
||||
.filter(|s| !s.is_empty())
|
||||
// 1. Runtime — each key checked independently; empty values are skipped
|
||||
// so VITE_BACKEND_URL is still reachable when BACKEND_URL="" is set.
|
||||
for key in ["BACKEND_URL", "VITE_BACKEND_URL"] {
|
||||
if let Ok(v) = std::env::var(key) {
|
||||
let url = normalize_api_base_url(&v);
|
||||
if !url.is_empty() {
|
||||
return Some(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Compile-time fallback — baked in by build-desktop.yml.
|
||||
// Each key checked independently for the same reason as above.
|
||||
for v in [option_env!("BACKEND_URL"), option_env!("VITE_BACKEND_URL")]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let url = normalize_api_base_url(v);
|
||||
if !url.is_empty() {
|
||||
return Some(url);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Resolve the app environment from process environment.
|
||||
/// Resolve the app environment, checking runtime env first then compile-time.
|
||||
///
|
||||
/// Each key is checked independently so that an empty primary key does not
|
||||
/// shadow a valid secondary key. The compile-time fallback (`option_env!`)
|
||||
/// mirrors what the Tauri shell already does for its Sentry environment tag.
|
||||
pub fn app_env_from_env() -> Option<String> {
|
||||
std::env::var(APP_ENV_VAR)
|
||||
.or_else(|_| std::env::var(VITE_APP_ENV_VAR))
|
||||
.ok()
|
||||
.map(|s| s.trim().to_ascii_lowercase())
|
||||
.filter(|s| !s.is_empty())
|
||||
// 1. Runtime — each key checked independently
|
||||
for key in [APP_ENV_VAR, VITE_APP_ENV_VAR] {
|
||||
if let Ok(v) = std::env::var(key) {
|
||||
let s = v.trim().to_ascii_lowercase();
|
||||
if !s.is_empty() {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Compile-time fallback — each key checked independently
|
||||
for v in [
|
||||
option_env!("OPENHUMAN_APP_ENV"),
|
||||
option_env!("VITE_OPENHUMAN_APP_ENV"),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let s = v.trim().to_ascii_lowercase();
|
||||
if !s.is_empty() {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_staging_app_env(app_env: Option<&str>) -> bool {
|
||||
@@ -62,8 +109,14 @@ pub fn default_api_base_url_for_env(app_env: Option<&str>) -> &'static str {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use super::*;
|
||||
|
||||
// Serialise all env-mutating tests to prevent flaky failures under
|
||||
// parallel test execution (std::env is process-global).
|
||||
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
|
||||
#[test]
|
||||
fn staging_app_env_uses_staging_default_api() {
|
||||
assert_eq!(
|
||||
@@ -82,4 +135,70 @@ mod tests {
|
||||
assert_eq!(default_api_base_url_for_env(None), DEFAULT_API_BASE_URL);
|
||||
assert!(!is_staging_app_env(Some("development")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_env_from_env_reads_runtime_var() {
|
||||
let _guard = ENV_LOCK.get_or_init(Mutex::default).lock().unwrap();
|
||||
let key = APP_ENV_VAR;
|
||||
let prev = std::env::var(key).ok();
|
||||
std::env::set_var(key, "staging");
|
||||
let result = app_env_from_env();
|
||||
match prev {
|
||||
Some(v) => std::env::set_var(key, v),
|
||||
None => std::env::remove_var(key),
|
||||
}
|
||||
assert_eq!(result.as_deref(), Some("staging"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_env_from_env_falls_through_empty_primary_to_secondary() {
|
||||
let _guard = ENV_LOCK.get_or_init(Mutex::default).lock().unwrap();
|
||||
let prev_primary = std::env::var(APP_ENV_VAR).ok();
|
||||
let prev_secondary = std::env::var(VITE_APP_ENV_VAR).ok();
|
||||
std::env::set_var(APP_ENV_VAR, ""); // empty — must not block secondary
|
||||
std::env::set_var(VITE_APP_ENV_VAR, "staging");
|
||||
let result = app_env_from_env();
|
||||
match prev_primary {
|
||||
Some(v) => std::env::set_var(APP_ENV_VAR, v),
|
||||
None => std::env::remove_var(APP_ENV_VAR),
|
||||
}
|
||||
match prev_secondary {
|
||||
Some(v) => std::env::set_var(VITE_APP_ENV_VAR, v),
|
||||
None => std::env::remove_var(VITE_APP_ENV_VAR),
|
||||
}
|
||||
assert_eq!(result.as_deref(), Some("staging"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_base_from_env_reads_runtime_var() {
|
||||
let _guard = ENV_LOCK.get_or_init(Mutex::default).lock().unwrap();
|
||||
let key = "BACKEND_URL";
|
||||
let prev = std::env::var(key).ok();
|
||||
std::env::set_var(key, "https://staging-api.tinyhumans.ai/");
|
||||
let result = api_base_from_env();
|
||||
match prev {
|
||||
Some(v) => std::env::set_var(key, v),
|
||||
None => std::env::remove_var(key),
|
||||
}
|
||||
assert_eq!(result.as_deref(), Some("https://staging-api.tinyhumans.ai"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_base_from_env_falls_through_empty_primary_to_secondary() {
|
||||
let _guard = ENV_LOCK.get_or_init(Mutex::default).lock().unwrap();
|
||||
let prev_primary = std::env::var("BACKEND_URL").ok();
|
||||
let prev_secondary = std::env::var("VITE_BACKEND_URL").ok();
|
||||
std::env::set_var("BACKEND_URL", ""); // empty — must not block secondary
|
||||
std::env::set_var("VITE_BACKEND_URL", "https://staging-api.tinyhumans.ai/");
|
||||
let result = api_base_from_env();
|
||||
match prev_primary {
|
||||
Some(v) => std::env::set_var("BACKEND_URL", v),
|
||||
None => std::env::remove_var("BACKEND_URL"),
|
||||
}
|
||||
match prev_secondary {
|
||||
Some(v) => std::env::set_var("VITE_BACKEND_URL", v),
|
||||
None => std::env::remove_var("VITE_BACKEND_URL"),
|
||||
}
|
||||
assert_eq!(result.as_deref(), Some("https://staging-api.tinyhumans.ai"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user