xbp 10.40.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use crate::strategies::{get_all_services, ServiceConfig, XbpConfig};

/// Resolve `--env` or project default or `production`.
pub fn resolve_deploy_env(config: &XbpConfig, explicit: Option<&str>) -> String {
    if let Some(env) = explicit.map(str::trim).filter(|s| !s.is_empty()) {
        return env.to_string();
    }
    config
        .deploy
        .as_ref()
        .and_then(|d| d.default_env.as_deref())
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .unwrap_or("production")
        .to_string()
}

/// Resolve target → ordered services for the given env.
///
/// Order:
/// 1. `all` → services with deploy.envs[env]
/// 2. `deploy.groups.<name>` → ordered group members that exist
/// 3. service name match
pub fn resolve_deploy_targets(
    config: &XbpConfig,
    target: &str,
    env: &str,
) -> Result<Vec<ServiceConfig>, String> {
    let target = target.trim();
    if target.is_empty() {
        return Err("deploy target is required (service name, group, or `all`)".to_string());
    }

    let all = get_all_services(config);

    if target.eq_ignore_ascii_case("all") {
        let selected: Vec<ServiceConfig> = all
            .into_iter()
            .filter(|s| service_has_env(s, env))
            .collect();
        if selected.is_empty() {
            return Err(format!(
                "no services declare `deploy.envs.{env}`; add per-service deploy config first"
            ));
        }
        return Ok(selected);
    }

    if let Some(group) = config
        .deploy
        .as_ref()
        .and_then(|d| d.groups.get(target))
    {
        let order = if group.order.is_empty() {
            group.services.clone()
        } else {
            group.order.clone()
        };
        let mut out = Vec::new();
        for name in order {
            let Some(service) = all.iter().find(|s| s.name == name).cloned() else {
                return Err(format!(
                    "deploy group `{target}` references unknown service `{name}`"
                ));
            };
            if !service_has_env(&service, env) {
                return Err(format!(
                    "service `{name}` in group `{target}` has no `deploy.envs.{env}`"
                ));
            }
            out.push(service);
        }
        if out.is_empty() {
            return Err(format!("deploy group `{target}` has no services"));
        }
        return Ok(out);
    }

    if let Some(service) = all.iter().find(|s| s.name == target).cloned() {
        if !service_has_env(&service, env) {
            return Err(format!(
                "service `{target}` has no `deploy.envs.{env}` (and is not a deploy.groups name)"
            ));
        }
        return Ok(vec![service]);
    }

    let available_services: Vec<&str> = all
        .iter()
        .filter(|s| service_has_env(s, env))
        .map(|s| s.name.as_str())
        .collect();
    let available_groups: Vec<String> = config
        .deploy
        .as_ref()
        .map(|d| d.groups.keys().cloned().collect())
        .unwrap_or_default();

    Err(format!(
        "unknown deploy target `{target}`. \
Try a service name, `all`, or a group. \
Services with env `{env}`: [{}]. Groups: [{}].",
        available_services.join(", "),
        available_groups.join(", ")
    ))
}

fn service_has_env(service: &ServiceConfig, env: &str) -> bool {
    service
        .deploy
        .as_ref()
        .map(|d| d.envs.contains_key(env))
        .unwrap_or(false)
}