use clap::Args;
use homeboy::log_status;
use serde::Serialize;
use homeboy::deploy::{self, ComponentDeployResult, DeployConfig, DeploySummary};
use homeboy::resolve::{infer_project_for_components, resolve_project_components};
use super::{CmdResult, ProjectsSummary};
#[derive(Args)]
pub struct DeployArgs {
pub target_id: Option<String>,
pub component_ids: Vec<String>,
#[arg(long, short = 'p')]
pub project: Option<String>,
#[arg(long, short = 'c')]
pub component: Option<Vec<String>>,
#[arg(long)]
pub json: Option<String>,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub outdated: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long, visible_alias = "status")]
pub check: bool,
#[arg(long)]
pub force: bool,
#[arg(long, value_delimiter = ',')]
pub projects: Option<Vec<String>>,
#[arg(long, short = 'f')]
pub fleet: Option<String>,
#[arg(long, short = 's')]
pub shared: bool,
#[arg(long)]
pub keep_deps: bool,
}
#[derive(Serialize)]
pub struct DeployOutput {
pub command: String,
pub project_id: String,
pub all: bool,
pub outdated: bool,
pub dry_run: bool,
pub check: bool,
pub force: bool,
pub results: Vec<ComponentDeployResult>,
pub summary: DeploySummary,
}
#[derive(Serialize)]
pub struct MultiProjectDeployOutput {
pub command: String,
pub component_ids: Vec<String>,
pub projects: Vec<ProjectDeployResult>,
pub summary: ProjectsSummary,
pub dry_run: bool,
pub check: bool,
pub force: bool,
}
#[derive(Serialize)]
pub struct ProjectDeployResult {
pub project_id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub results: Vec<ComponentDeployResult>,
pub summary: DeploySummary,
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum DeployCommandOutput {
Single(DeployOutput),
Multi(MultiProjectDeployOutput),
}
pub fn run(
mut args: DeployArgs,
_global: &crate::commands::GlobalArgs,
) -> CmdResult<DeployCommandOutput> {
if let Some(ref fleet_id) = args.fleet {
let fl = homeboy::fleet::load(fleet_id)?;
return run_multi_project(&args, &fl.project_ids);
}
if args.shared {
let component_ids: Vec<String> = if let Some(ref comps) = args.component {
comps.clone()
} else if let Some(ref target) = args.target_id {
vec![target.clone()]
} else {
return Err(homeboy::Error::validation_invalid_argument(
"component",
"At least one component ID is required when using --shared",
None,
None,
));
};
if component_ids.is_empty() {
return Err(homeboy::Error::validation_invalid_argument(
"component",
"At least one component ID is required when using --shared",
None,
None,
));
}
let mut project_ids: Vec<String> = Vec::new();
for component_id in &component_ids {
let using = homeboy::component::projects_using(component_id).unwrap_or_default();
for pid in using {
if !project_ids.contains(&pid) {
project_ids.push(pid);
}
}
}
if project_ids.is_empty() {
return Err(homeboy::Error::validation_invalid_argument(
"component",
format!("No projects found using component(s): {:?}", component_ids),
None,
Some(vec![
"Run 'homeboy component shared' to see component usage".to_string(),
]),
));
}
args.component_ids = component_ids;
args.target_id = None;
return run_multi_project(&args, &project_ids);
}
if let Some(ref project_ids) = args.projects {
return run_multi_project(&args, project_ids);
}
let target_id = args.target_id.as_ref().ok_or_else(|| {
homeboy::Error::validation_invalid_argument(
"input",
"Provide component ID, project ID with --all, or use flags",
None,
Some(vec![
"Deploy a single component: homeboy deploy <component-id>".to_string(),
"Deploy to a project: homeboy deploy <project-id> --all".to_string(),
]),
)
})?;
let (project_id, component_ids) = match (&args.project, &args.component) {
(Some(ref proj), Some(ref comps)) => (proj.clone(), comps.clone()),
(Some(ref proj), None) => {
let mut comps = vec![target_id.clone()];
comps.extend(args.component_ids.clone());
(proj.clone(), comps)
}
(None, Some(ref comps)) => {
let projects = homeboy::project::list_ids().unwrap_or_default();
if projects.contains(target_id) {
(target_id.clone(), comps.clone())
} else {
match infer_project_for_components(comps) {
Some(proj) => (proj, comps.clone()),
None => {
return Err(homeboy::Error::validation_invalid_argument(
"project_id",
"Could not infer project. Use --project flag or provide project as first argument.",
None,
None,
))
}
}
}
}
(None, None) => resolve_project_components(target_id, &args.component_ids)?,
};
args.target_id = Some(project_id.clone());
args.component_ids = component_ids;
if let Some(ref spec) = args.json {
args.component_ids = deploy::parse_bulk_component_ids(spec)?;
}
let config = DeployConfig {
component_ids: args.component_ids.clone(),
all: args.all,
outdated: args.outdated,
dry_run: args.dry_run,
check: args.check,
force: args.force,
skip_build: false,
keep_deps: args.keep_deps,
};
let result = deploy::run(&project_id, &config).map_err(|e| {
if e.message.contains("No components configured for project") {
e.with_hint(format!(
"Run 'homeboy project components add {} <component-id>' to add components",
project_id
))
.with_hint("Run 'homeboy init' to see project context and available components")
} else {
e
}
})?;
let exit_code = if result.summary.failed > 0 { 1 } else { 0 };
Ok((
DeployCommandOutput::Single(DeployOutput {
command: "deploy.run".to_string(),
project_id: project_id.clone(),
all: args.all,
outdated: args.outdated,
dry_run: args.dry_run,
check: args.check,
force: args.force,
results: result.results,
summary: result.summary,
}),
exit_code,
))
}
fn run_multi_project(args: &DeployArgs, project_ids: &[String]) -> CmdResult<DeployCommandOutput> {
let mut component_ids: Vec<String> = Vec::new();
if let Some(ref target) = args.target_id {
component_ids.push(target.clone());
}
component_ids.extend(args.component_ids.clone());
let component_ids: Vec<String> = component_ids
.into_iter()
.filter(|s| !s.is_empty())
.collect();
if component_ids.is_empty() {
return Err(homeboy::Error::validation_invalid_argument(
"component_ids",
"At least one component ID is required when using --projects",
None,
None,
));
}
let known_projects = homeboy::project::list_ids().unwrap_or_default();
for project_id in project_ids {
if !known_projects.contains(project_id) {
return Err(homeboy::Error::validation_invalid_argument(
"projects",
&format!("Unknown project: '{}'", project_id),
None,
None,
));
}
}
log_status!(
"deploy",
"Deploying {:?} to {} project(s)...",
component_ids,
project_ids.len()
);
let mut project_results = Vec::new();
let mut succeeded: u32 = 0;
let mut failed: u32 = 0;
let mut first_project = true;
for project_id in project_ids {
log_status!("deploy", "Deploying to project '{}'...", project_id);
let config = DeployConfig {
component_ids: component_ids.clone(),
all: args.all,
outdated: args.outdated,
dry_run: args.dry_run,
check: args.check,
force: args.force,
skip_build: !first_project, keep_deps: args.keep_deps,
};
match deploy::run(project_id, &config) {
Ok(result) => {
let deploy_failed = result.summary.failed > 0;
if deploy_failed {
let error_msg = result
.results
.iter()
.find_map(|r| r.error.clone())
.unwrap_or_else(|| "Deployment failed".to_string());
project_results.push(ProjectDeployResult {
project_id: project_id.clone(),
status: "failed".to_string(),
error: Some(error_msg),
results: result.results,
summary: result.summary,
});
failed += 1;
} else {
project_results.push(ProjectDeployResult {
project_id: project_id.clone(),
status: "deployed".to_string(),
error: None,
results: result.results,
summary: result.summary,
});
succeeded += 1;
}
}
Err(e) => {
project_results.push(ProjectDeployResult {
project_id: project_id.clone(),
status: "failed".to_string(),
error: Some(e.to_string()),
results: vec![],
summary: DeploySummary {
total: 0,
succeeded: 0,
skipped: 0,
failed: 1,
},
});
failed += 1;
}
}
first_project = false;
}
let total = project_results.len() as u32;
let exit_code = if failed > 0 { 1 } else { 0 };
Ok((
DeployCommandOutput::Multi(MultiProjectDeployOutput {
command: "deploy.run_multi".to_string(),
component_ids,
projects: project_results,
summary: ProjectsSummary {
total_projects: total,
succeeded,
failed,
},
dry_run: args.dry_run,
check: args.check,
force: args.force,
}),
exit_code,
))
}