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"];