use crate::cli::{Cli, Commands, DeployCommand};
use crate::db::run_db_command;
use crate::dev::run_dev;
use crate::gateway::run_gateway_command;
use crate::service::run_service_command;
use crate::service_local::setup_current_service;
use crate::util::{Result, make_vars, repo_root, run_cmd, run_make};
use std::fs;
use std::path::Path;
use std::process::Command;
pub(crate) fn run_cli(cli: Cli) -> Result<()> {
match cli.command {
Commands::Service { command } => run_service_command(command),
Commands::Gateway { command } => run_gateway_command(command),
Commands::Db { command } => run_db_command(command),
Commands::Setup(args) => setup_current_service(&args.vars),
Commands::Dev { command } => run_dev(command),
Commands::Sync => run_sync(),
Commands::Release(args) => run_release(&args.vars),
Commands::Publish(args) => run_publish(&args.vars),
Commands::ReleaseSync(args) => run_release_sync(&args.vars),
Commands::Deploy { command } => run_deploy_command(command),
}
}
fn run_deploy_command(command: DeployCommand) -> Result<()> {
match command {
DeployCommand::Kubernetes(args) => {
run_sync()?;
run_make(
&repo_root().join("devops"),
"deploy-kubernetes",
&make_vars(&args.vars),
)
}
DeployCommand::Docker(args) => run_make(
&repo_root().join("devops"),
"deploy-docker",
&make_vars(&args.vars),
),
DeployCommand::Migrations(args) => run_make(
&repo_root().join("devops"),
"apply-migrations",
&make_vars(&args.vars),
),
DeployCommand::ObservabilityUp(args) => run_make(
&repo_root().join("devops"),
"observability-full-up",
&make_vars(&args.vars),
),
DeployCommand::ObservabilityDown(args) => run_make(
&repo_root().join("devops"),
"observability-full-down",
&make_vars(&args.vars),
),
DeployCommand::BackupNow(args) => run_make(
&repo_root().join("devops"),
"backup-now",
&make_vars(&args.vars),
),
DeployCommand::InstallBackupCron(args) => run_make(
&repo_root().join("devops"),
"install-backup-cron",
&make_vars(&args.vars),
),
DeployCommand::ReleaseSync(args) => run_release_sync(&args.vars),
DeployCommand::ReloadCaddy(args) => run_make(
&repo_root().join("devops"),
"reload-caddy",
&make_vars(&args.vars),
),
DeployCommand::RestartCdn(args) => run_make(
&repo_root().join("devops"),
"restart-cdn",
&make_vars(&args.vars),
),
DeployCommand::Recreate(args) => run_make(
&repo_root().join("devops"),
"recreate",
&make_vars(&args.vars),
),
}
}
fn run_sync() -> Result<()> {
run_cmd(
&repo_root(),
"devops/droplet/scripts/sync-service-assets.sh",
&[],
)
}
fn run_release(args: &[String]) -> Result<()> {
run_make(&repo_root().join("devops"), "release", &make_vars(args))
}
fn run_publish(args: &[String]) -> Result<()> {
let manifest = repo_root().join("tools/exe/Cargo.toml");
let max_attempts = publish_max_attempts();
for attempt in 1..=max_attempts {
let version = bump_manifest_patch_version(&manifest)?;
println!("[publish] bumped executesoft crate to {version}");
let output = cargo_publish(&manifest, args)?;
if output.status.success() {
print_command_output(&output);
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
print_command_output(&output);
if stderr.contains("already exists on crates.io index") && attempt < max_attempts {
println!("[publish] version {version} already exists; bumping patch and retrying");
continue;
}
return Err(format!("cargo publish failed with {}", output.status).into());
}
Err("cargo publish retry limit reached".into())
}
fn cargo_publish(manifest: &Path, args: &[String]) -> Result<std::process::Output> {
let mut publish_args = vec![
"publish".to_string(),
"--manifest-path".to_string(),
manifest.display().to_string(),
];
publish_args.extend(args.iter().cloned());
Ok(Command::new("cargo")
.args(&publish_args)
.current_dir(repo_root())
.output()?)
}
fn print_command_output(output: &std::process::Output) {
print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
fn publish_max_attempts() -> usize {
std::env::var("EXE_PUBLISH_MAX_VERSION_BUMPS")
.ok()
.and_then(|value| value.parse().ok())
.filter(|value| *value > 0)
.unwrap_or(10)
}
pub(crate) fn bump_manifest_patch_version(manifest: &Path) -> Result<String> {
let text = fs::read_to_string(manifest)?;
let mut next_version = None;
let mut updated = Vec::new();
let mut in_package = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_package = trimmed == "[package]";
}
if in_package
&& trimmed.starts_with("version")
&& let Some((key, value)) = line.split_once('=')
{
let current = value.trim().trim_matches('"');
let next = next_patch_version(current)?;
next_version = Some(next.clone());
updated.push(format!("{}= \"{}\"", key, next));
continue;
}
updated.push(line.to_string());
}
let Some(version) = next_version else {
return Err(format!("version not found in {}", manifest.display()).into());
};
fs::write(manifest, format!("{}\n", updated.join("\n")))?;
Ok(version)
}
fn next_patch_version(version: &str) -> Result<String> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 3 {
return Err(format!("expected semver major.minor.patch, got {version}").into());
}
let major: u64 = parts[0].parse()?;
let minor: u64 = parts[1].parse()?;
let patch: u64 = parts[2].parse()?;
Ok(format!("{major}.{minor}.{}", patch + 1))
}
fn run_release_sync(args: &[String]) -> Result<()> {
run_make(
&repo_root().join("devops"),
"release-sync",
&make_vars(args),
)
}