pub mod admin;
pub mod answers_crypto;
pub mod bundle;
pub mod bundle_source;
pub mod capabilities;
pub mod card_setup;
pub mod cli_args;
pub mod cli_commands;
pub mod cli_helpers;
pub mod cli_i18n;
pub mod config_envelope;
pub mod deployment_targets;
pub mod discovery;
pub mod doctor;
pub mod engine;
pub mod env_mode;
pub mod env_wizard;
pub mod flow;
pub mod gtbundle;
pub mod no_ui_oauth;
pub mod oauth_callback;
pub mod oauth_device;
pub mod plan;
pub mod platform_setup;
pub mod provider_state;
pub mod reload;
pub mod secret_name;
pub mod secrets;
pub mod setup_actions;
pub mod setup_input;
pub mod setup_to_formspec;
pub mod setup_tunnel;
pub mod tenant_config;
pub mod webhook;
#[cfg(feature = "ui")]
pub mod ui;
pub mod qa {
pub mod bridge;
pub mod persist;
pub mod prompts;
pub mod shared_questions;
pub mod wizard;
}
pub use bundle_source::BundleSource;
pub use engine::SetupEngine;
pub use plan::{SetupMode, SetupPlan, SetupStep, SetupStepKind};
pub use qa::wizard::{
ProviderFormSpec, SHARED_QUESTION_IDS, SharedQuestionsResult, build_provider_form_specs,
collect_shared_questions, prompt_shared_questions, run_qa_setup_with_shared,
};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub const DEFAULT_ENV_ID: &str = "local";
pub const LEGACY_ENV_ID: &str = "dev";
pub const DISABLE_ALIAS_ENV_VAR: &str = "GREENTIC_DISABLE_DEV_ALIAS";
pub fn resolve_env(override_env: Option<&str>) -> String {
let raw = override_env
.map(|v| v.to_string())
.or_else(|| std::env::var("GREENTIC_ENV").ok())
.unwrap_or_else(|| DEFAULT_ENV_ID.to_string());
compat_alias::apply_dev_alias(&raw)
}
mod compat_alias {
use std::sync::atomic::{AtomicBool, Ordering};
use super::{DEFAULT_ENV_ID, DISABLE_ALIAS_ENV_VAR, LEGACY_ENV_ID};
static WARNED: AtomicBool = AtomicBool::new(false);
pub fn apply_dev_alias(env: &str) -> String {
if env != LEGACY_ENV_ID {
return env.to_string();
}
if alias_disabled() {
panic!(
"environment `{LEGACY_ENV_ID}` is no longer accepted (set via {DISABLE_ALIAS_ENV_VAR}=1). \
Migrate to `{DEFAULT_ENV_ID}` via `gtc op env migrate-dev {DEFAULT_ENV_ID} --check` then `--apply`, \
or pass `--env {DEFAULT_ENV_ID}` / unset $GREENTIC_ENV.",
);
}
if !WARNED.swap(true, Ordering::SeqCst) {
tracing::warn!(
target: "greentic_setup::compat_alias",
legacy = LEGACY_ENV_ID,
target_env = DEFAULT_ENV_ID,
"env `{LEGACY_ENV_ID}` is deprecated; resolving as `{DEFAULT_ENV_ID}` for this process. \
Plan the migration with `gtc op env migrate-dev {DEFAULT_ENV_ID} --check`; \
set {DISABLE_ALIAS_ENV_VAR}=1 to hard-fail on `{LEGACY_ENV_ID}` in CI.",
);
}
DEFAULT_ENV_ID.to_string()
}
fn alias_disabled() -> bool {
std::env::var(DISABLE_ALIAS_ENV_VAR)
.ok()
.map(|v| {
let v = v.trim().to_ascii_lowercase();
matches!(v.as_str(), "1" | "true" | "yes" | "on")
})
.unwrap_or(false)
}
#[cfg(test)]
pub(super) fn reset_warning_latch_for_tests() {
WARNED.store(false, Ordering::SeqCst);
}
}
pub fn canonical_secret_uri(
env: &str,
tenant: &str,
team: Option<&str>,
provider: &str,
key: &str,
) -> String {
let team_segment = greentic_secrets_lib::normalize_team(team)
.unwrap_or_else(|| greentic_secrets_lib::TEAM_PLACEHOLDER.to_string());
let provider_segment = if provider.is_empty() {
"messaging".to_string()
} else {
provider.to_string()
};
let normalized_key = secret_name::canonical_secret_name(key);
format!("secrets://{env}/{tenant}/{team_segment}/{provider_segment}/{normalized_key}")
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_clean_env<R>(body: impl FnOnce() -> R) -> R {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev_env = std::env::var_os("GREENTIC_ENV");
let prev_disable = std::env::var_os(DISABLE_ALIAS_ENV_VAR);
unsafe {
std::env::remove_var("GREENTIC_ENV");
std::env::remove_var(DISABLE_ALIAS_ENV_VAR);
}
compat_alias::reset_warning_latch_for_tests();
let out = body();
unsafe {
match prev_env {
Some(v) => std::env::set_var("GREENTIC_ENV", v),
None => std::env::remove_var("GREENTIC_ENV"),
}
match prev_disable {
Some(v) => std::env::set_var(DISABLE_ALIAS_ENV_VAR, v),
None => std::env::remove_var(DISABLE_ALIAS_ENV_VAR),
}
}
out
}
#[test]
fn version_is_correct() {
assert!(version().starts_with("1.1"));
}
#[test]
fn secret_uri_basic() {
let uri = canonical_secret_uri("dev", "demo", None, "messaging-telegram", "bot_token");
assert_eq!(uri, "secrets://dev/demo/_/messaging-telegram/bot_token");
}
#[test]
fn secret_uri_with_team() {
let uri = canonical_secret_uri("dev", "acme", Some("ops"), "state-redis", "redis_url");
assert_eq!(uri, "secrets://dev/acme/ops/state-redis/redis_url");
}
#[test]
fn secret_uri_default_team_becomes_wildcard() {
let uri = canonical_secret_uri(
"dev",
"demo",
Some("default"),
"messaging-slack",
"bot_token",
);
assert_eq!(uri, "secrets://dev/demo/_/messaging-slack/bot_token");
}
#[test]
fn resolve_env_returns_local_by_default() {
with_clean_env(|| {
assert_eq!(resolve_env(None), "local");
});
}
#[test]
fn resolve_env_passes_through_non_legacy_override() {
with_clean_env(|| {
assert_eq!(resolve_env(Some("staging")), "staging");
assert_eq!(resolve_env(Some("prod")), "prod");
assert_eq!(resolve_env(Some("local")), "local");
});
}
#[test]
fn resolve_env_remaps_dev_override_to_local() {
with_clean_env(|| {
assert_eq!(resolve_env(Some("dev")), "local");
});
}
#[test]
fn resolve_env_remaps_dev_env_var_to_local() {
with_clean_env(|| {
unsafe {
std::env::set_var("GREENTIC_ENV", "dev");
}
assert_eq!(resolve_env(None), "local");
});
}
#[test]
fn alias_warning_fires_only_once_per_process() {
with_clean_env(|| {
assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
compat_alias::reset_warning_latch_for_tests();
assert_eq!(compat_alias::apply_dev_alias("dev"), "local");
});
}
#[test]
fn disable_alias_env_var_panics_on_dev() {
with_clean_env(|| {
unsafe {
std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
}
let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
assert!(
result.is_err(),
"resolve_env should panic when alias is disabled and input is `dev`"
);
});
}
#[test]
fn disable_alias_accepts_truthy_strings() {
for value in ["1", "true", "TRUE", "yes", "YES", "on", " true "] {
with_clean_env(|| {
unsafe {
std::env::set_var(DISABLE_ALIAS_ENV_VAR, value);
}
let result = std::panic::catch_unwind(|| resolve_env(Some("dev")));
assert!(
result.is_err(),
"DISABLE value `{value}` should hard-fail on dev resolution"
);
});
}
}
#[test]
fn disable_alias_does_not_panic_on_non_legacy_values() {
with_clean_env(|| {
unsafe {
std::env::set_var(DISABLE_ALIAS_ENV_VAR, "1");
}
assert_eq!(resolve_env(Some("local")), "local");
assert_eq!(resolve_env(Some("staging")), "staging");
assert_eq!(resolve_env(None), "local");
});
}
}