xbp 10.30.1

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use super::project::{
    parse_multiline_env_file, pick_first_non_empty, read_configured_custom_domain_routes,
};
use super::wrangler::{
    apply_remote_settings_to_config, build_worker_plain_text_bindings,
    generate_dashboard_config_value, resolve_deploy_env, write_json_file,
    WORKER_PLAIN_TEXT_BINDING_KEYS,
};
use crate::provider_support::CloudflareClient;
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::process::Command;

pub async fn run_predeploy(
    worker_root: &Path,
    token_override: Option<&str>,
    account_id_override: Option<&str>,
    ci: bool,
) -> Result<(), String> {
    if ci || env::var("WORKERS_CI").ok().as_deref() == Some("1") {
        println!("Skipping local predeploy sync because Workers CI mode is active.");
        return Ok(());
    }
    run_sync_env_local(worker_root, token_override, account_id_override).await
}

pub async fn run_sync_env_local(
    worker_root: &Path,
    token_override: Option<&str>,
    account_id_override: Option<&str>,
) -> Result<(), String> {
    let env_local_path = worker_root.join(".env.local");
    let has_env_local = env_local_path.exists();
    let mut local_env = if has_env_local {
        parse_multiline_env_file(&env_local_path)?
    } else {
        HashMap::new()
    };

    if let Some(account_id) = account_id_override
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        local_env.insert("CLOUDFLARE_ACCOUNT_ID".to_string(), account_id.to_string());
    }
    if let Some(token) = token_override
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        local_env.insert("CLOUDFLARE_API_TOKEN".to_string(), token.to_string());
    }

    let deploy_env = resolve_deploy_env(worker_root, &local_env)?;
    let worker_vars = build_worker_plain_text_bindings(&local_env, &deploy_env.worker_name)?;
    let account_id = deploy_env.account_id.clone().ok_or_else(|| {
        "Missing required deploy value: CLOUDFLARE_ACCOUNT_ID. Set it in .env.local or the environment.".to_string()
    })?;

    write_dev_vars_file(worker_root, &worker_vars)?;

    let mut deploy_config = generate_dashboard_config_value(&deploy_env);
    deploy_config["account_id"] = Value::String(account_id.clone());
    deploy_config["routes"] = Value::Array(read_configured_custom_domain_routes(worker_root));
    deploy_config["vars"] = Value::Object(
        worker_vars
            .iter()
            .map(|(key, value)| (key.clone(), Value::String(value.clone())))
            .collect::<Map<String, Value>>(),
    );

    let remote_settings = fetch_remote_worker_settings(
        token_override,
        &local_env,
        &account_id,
        &deploy_env.worker_name,
    )
    .await?;

    let fallback_routes = deploy_config
        .get("routes")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    apply_remote_settings_to_config(
        &mut deploy_config,
        remote_settings.as_ref(),
        &fallback_routes,
        &worker_vars,
    );

    write_json_file(&worker_root.join("wrangler.deploy.json"), &deploy_config)?;
    write_json_file(&worker_root.join("wrangler.jsonc"), &deploy_config)?;
    write_json_file(&worker_root.join("wrangler.dev.jsonc"), &deploy_config)?;

    println!("Prepared:");
    println!(
        "  - deploy inputs source={}",
        if has_env_local {
            ".env.local + environment"
        } else {
            "environment"
        }
    );
    println!("  - .dev.vars");
    println!("  - wrangler.deploy.json");
    println!("  - wrangler.jsonc");
    println!("  - wrangler.dev.jsonc");
    if let Some(url) = worker_vars.get("BETTER_AUTH_URL") {
        println!("  - BETTER_AUTH_URL={}", url);
    }

    Ok(())
}

pub fn run_deploy_ci_script(worker_root: &Path, version_upload: bool) -> Result<(), String> {
    let mut args = vec!["scripts/deploy-ci.mjs".to_string()];
    if version_upload {
        args.push("--version-upload".to_string());
    }
    run_node_script(worker_root, &args, &HashMap::new())
}

pub fn run_select_deploy_script(
    worker_root: &Path,
    ci: bool,
    branch: Option<&str>,
) -> Result<(), String> {
    let mut env_overrides = HashMap::new();
    if ci {
        env_overrides.insert("WORKERS_CI".to_string(), "1".to_string());
    }
    if let Some(branch) = branch.map(str::trim).filter(|value| !value.is_empty()) {
        env_overrides.insert("WORKERS_CI_BRANCH".to_string(), branch.to_string());
    }
    run_node_script(
        worker_root,
        &[String::from("scripts/select-deploy-command.mjs")],
        &env_overrides,
    )
}

async fn fetch_remote_worker_settings(
    token_override: Option<&str>,
    local_env: &HashMap<String, String>,
    account_id: &str,
    worker_name: &str,
) -> Result<Option<crate::provider_support::CloudflareWorkerSettings>, String> {
    let api_token = pick_first_non_empty([
        token_override.map(str::trim).map(ToOwned::to_owned),
        local_env.get("CLOUDFLARE_API_TOKEN").cloned(),
        env::var("CLOUDFLARE_API_TOKEN").ok(),
    ]);

    let Some(api_token) = api_token else {
        return Ok(None);
    };

    let client = CloudflareClient::new(api_token, account_id.to_string())?;
    client.get_worker_settings(worker_name).await
}

fn write_dev_vars_file(
    worker_root: &Path,
    worker_vars: &HashMap<String, String>,
) -> Result<(), String> {
    let ordered_vars = WORKER_PLAIN_TEXT_BINDING_KEYS
        .iter()
        .filter_map(|key| {
            worker_vars
                .get(*key)
                .map(|value| format!("{key}={}", quote_dev_var(value)))
        })
        .collect::<Vec<_>>();

    let lines = WORKER_DEV_VAR_HEADER
        .iter()
        .copied()
        .map(str::to_string)
        .chain(ordered_vars)
        .collect::<Vec<_>>();

    std::fs::write(
        worker_root.join(".dev.vars"),
        format!("{}\n", lines.join("\n")),
    )
    .map_err(|error| {
        format!(
            "Failed to write {}: {}",
            worker_root.join(".dev.vars").display(),
            error
        )
    })
}

fn run_node_script(
    worker_root: &Path,
    script_args: &[String],
    env_overrides: &HashMap<String, String>,
) -> Result<(), String> {
    let mut command = Command::new("node");
    command.args(script_args).current_dir(worker_root);
    for (key, value) in env_overrides {
        command.env(key, value);
    }

    let status = command
        .status()
        .map_err(|error| format!("Failed to run node {}: {}", script_args.join(" "), error))?;
    if !status.success() {
        return Err(format!(
            "Node script `{}` failed with status {}.",
            script_args.join(" "),
            status
        ));
    }
    Ok(())
}

fn quote_dev_var(value: &str) -> String {
    format!(
        "\"{}\"",
        value
            .replace('\\', "\\\\")
            .replace('"', "\\\"")
            .replace('\n', "\\n")
    )
}

const WORKER_DEV_VAR_HEADER: &[&str] = &["# Generated from .env.local - do not commit"];