use std::io::IsTerminal;
use anyhow::{bail, Result};
use console::style;
use crate::config::Config;
use crate::remotes;
use crate::store::SecretStore;
use crate::sync_tracker::SyncIndex;
use crate::targets::{CommandRunner, RealCommandRunner};
use crate::ui;
pub struct DeleteOptions<'a> {
pub key: &'a str,
pub env: &'a str,
pub no_sync: bool,
pub strict: bool,
}
struct DeleteReport {
key: String,
env: String,
version: u64,
push_results: Vec<super::sync::RemotePushResult>,
}
impl DeleteReport {
fn remote_failure_count(&self) -> usize {
self.push_results
.iter()
.filter(|r| r.outcome.is_err())
.count()
}
fn render(&self) -> Result<()> {
cliclack::log::success(format!(
"Deleted {}:{} (v{})",
self.key, self.env, self.version
))?;
Ok(())
}
}
pub fn run(config: &Config, opts: &DeleteOptions<'_>) -> Result<()> {
let version = SecretStore::open(&config.root)?.payload()?.version;
cliclack::intro(
style(format!(
"{} · {}",
style(&config.project).bold(),
style(format!("v{version}")).dim()
))
.to_string(),
)?;
run_with_runner(config, opts, &RealCommandRunner)?;
let payload = SecretStore::open(&config.root)?.payload()?;
let env_versions: Vec<(String, u64)> = config
.environments
.iter()
.map(|e| (e.clone(), payload.env_version(e)))
.collect();
cliclack::outro(
style(ui::format_store_outro(
payload.version,
&env_versions,
Some(opts.env),
))
.dim()
.to_string(),
)?;
Ok(())
}
pub fn run_with_runner(
config: &Config,
opts: &DeleteOptions<'_>,
runner: &dyn CommandRunner,
) -> Result<()> {
let key = opts.key;
let env = opts.env;
config.validate_env(env)?;
if config.find_secret(key).is_none() {
cliclack::log::warning(format!("Secret '{key}' is not defined in esk.yaml"))?;
}
if let Some((_, def)) = config.find_secret(key) {
if def.required.is_required_in(env) && std::io::stdin().is_terminal() {
let targets: Vec<String> = def
.targets
.keys()
.map(std::string::ToString::to_string)
.collect();
let target_list = if targets.is_empty() {
String::new()
} else {
format!(" (targets: {})", targets.join(", "))
};
let confirm = cliclack::confirm(format!(
"{key}:{env} is required{target_list}. Delete anyway?",
))
.initial_value(false)
.interact()?;
if !confirm {
cliclack::log::info("Cancelled.")?;
return Ok(());
}
}
}
let store = SecretStore::open(&config.root)?;
let payload = store.delete(key, env)?;
let mut push_results = Vec::new();
if !opts.no_sync && !config.remotes.is_empty() {
let sync_index_path = config.root.join(".esk/sync-index.json");
let mut sync_index = SyncIndex::load(&sync_index_path);
let all_remotes = remotes::build_remotes(config, runner);
push_results =
super::sync::push_to_remotes(&all_remotes, &payload, config, env, &mut sync_index)?;
sync_index.save()?;
}
let report = DeleteReport {
key: key.to_string(),
env: env.to_string(),
version: payload.version,
push_results,
};
report.render()?;
if opts.no_sync {
return Ok(());
}
let remote_failures = report.remote_failure_count();
if remote_failures > 0 && opts.strict {
bail!(
"{remote_failures} remote(s) failed to push (--strict). Target deploy skipped.\n\
Fix the remote issue, then run:\n \
esk sync --env {env}\n \
esk deploy --env {env}"
);
}
crate::cli::deploy::run_with_runner(
config,
&crate::cli::deploy::DeployOptions {
env: Some(env),
force: false,
dry_run: false,
verbose: false,
skip_validation: false,
strict: false,
allow_empty: true,
prune: false,
},
runner,
)?;
if remote_failures > 0 {
bail!("{remote_failures} remote(s) failed to push. Run `esk sync --env {env}` to retry.");
}
Ok(())
}