use anyhow::Result;
use indexmap::IndexMap;
use std::path::PathBuf;
use crate::{
ci_patcher,
commands::generate::Generate,
orb_config::{OrbConfig, OrbSection, SubcommandConfig},
};
pub const DEFAULT_DOCKER_ORB_VERSION: &str = "3.0.1";
#[derive(Debug, clap::Args)]
pub struct Init {
#[arg(long)]
pub binary: String,
#[arg(long = "public-orb-namespace")]
pub public_orb_namespaces: Vec<String>,
#[arg(long = "private-orb-namespace")]
pub private_orb_namespaces: Vec<String>,
#[arg(long)]
pub build_workflow: String,
#[arg(long)]
pub release_workflow: String,
#[arg(long)]
pub requires_job: Option<String>,
#[arg(long)]
pub crate_tag_prefix: String,
#[arg(long)]
pub release_after_job: String,
#[arg(long, default_value = "orb")]
pub orb_dir: String,
#[arg(long, default_value = ".circleci")]
pub ci_dir: PathBuf,
#[arg(long, default_value = "12.3.3")]
pub orb_tools_version: String,
#[arg(long, default_value = DEFAULT_DOCKER_ORB_VERSION)]
pub docker_orb_version: String,
#[arg(long)]
pub docker_namespace: String,
#[arg(long, default_value = "docker-credentials")]
pub docker_context: String,
#[arg(long, default_value = "orb-publishing")]
pub orb_context: String,
#[arg(long, default_value = env!("CARGO_PKG_VERSION"))]
pub gen_circleci_orb_version: String,
#[arg(long)]
pub mcp: bool,
#[arg(long, default_value = "0.0.1")]
pub mcp_earliest_version: String,
#[arg(long, default_value = "pcu-app")]
pub mcp_context: String,
#[arg(long)]
pub dry_run: bool,
}
pub(crate) fn build_bootstrap_config(
binary: &str,
namespaces: &[String],
orb_dir: &str,
) -> OrbConfig {
let mut subcommands = IndexMap::new();
subcommands.insert(
"help".to_string(),
SubcommandConfig {
generate_job: Some(false),
param: None,
},
);
OrbConfig {
orb: Some(OrbSection {
binary: Some(binary.to_string()),
namespaces: Some(namespaces.to_vec()),
orb_dir: Some(orb_dir.to_string()),
base_image: None,
install_method: None,
home_url: None,
source_url: None,
}),
orbs: None,
subcommand: Some(subcommands),
job_group: None,
extra_job: None,
}
}
impl Init {
pub fn run(&self) -> Result<()> {
let namespaces: Vec<String> = self
.public_orb_namespaces
.iter()
.chain(self.private_orb_namespaces.iter())
.cloned()
.collect();
tracing::info!("Generating orb source into ./{}", self.orb_dir);
let gen = Generate {
binary: self.binary.clone(),
namespaces: namespaces.clone(),
output: PathBuf::from("."),
orb_dir: self.orb_dir.clone(),
install_method: crate::commands::generate::InstallMethod::Binstall,
base_image: crate::commands::generate::DEFAULT_BASE_IMAGE.to_string(),
home_url: None,
source_url: None,
git_push_subcommands: vec![],
circleci_cli_version: None,
apt_packages: vec![],
dry_run: self.dry_run,
config: None,
};
gen.run()?;
let opts = ci_patcher::PatchOpts {
binary: self.binary.clone(),
namespaces,
docker_namespace: self.docker_namespace.clone(),
orb_dir: self.orb_dir.clone(),
build_workflow: self.build_workflow.clone(),
release_workflow: self.release_workflow.clone(),
requires_job: self.requires_job.clone(),
crate_tag_prefix: self.crate_tag_prefix.clone(),
release_after_job: self.release_after_job.clone(),
orb_tools_version: self.orb_tools_version.clone(),
docker_orb_version: self.docker_orb_version.clone(),
docker_context: self.docker_context.clone(),
orb_context: self.orb_context.clone(),
private_namespaces: self.private_orb_namespaces.clone(),
gen_circleci_orb_version: self.gen_circleci_orb_version.clone(),
mcp: self.mcp,
mcp_earliest_version: self.mcp_earliest_version.clone(),
mcp_context: self.mcp_context.clone(),
};
let summary = ci_patcher::apply_patches(&self.ci_dir, &opts, self.dry_run)?;
for line in &summary {
println!("{line}");
}
let config_path = std::path::Path::new("gen-circleci-orb.toml");
let bootstrap =
build_bootstrap_config(&self.binary, opts.namespaces.as_slice(), &self.orb_dir);
if self.dry_run {
let content = toml::to_string_pretty(&bootstrap)?;
println!("(dry-run) Would write {}", config_path.display());
println!("{content}");
println!("(dry-run: no files written)");
} else {
crate::orb_config::save_config(config_path, &bootstrap)?;
println!("Wrote {}", config_path.display());
println!("Done.");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_docker_orb_version_matches_registry() {
assert_eq!(
DEFAULT_DOCKER_ORB_VERSION, "3.0.1",
"DEFAULT_DOCKER_ORB_VERSION must be the registry-available version"
);
}
#[test]
fn bootstrap_config_has_orb_section_with_binary() {
let config = build_bootstrap_config("mytool", &["my-org".to_string()], "orb");
assert!(
config.orb.is_some(),
"bootstrap config must have [orb] section"
);
assert_eq!(
config.orb.as_ref().unwrap().binary.as_deref(),
Some("mytool")
);
}
#[test]
fn bootstrap_config_has_namespaces() {
let config =
build_bootstrap_config("mytool", &["ns1".to_string(), "ns2".to_string()], "orb");
assert_eq!(
config.orb.as_ref().unwrap().namespaces.as_deref(),
Some(&["ns1".to_string(), "ns2".to_string()][..])
);
}
#[test]
fn bootstrap_config_suppresses_help_subcommand() {
let config = build_bootstrap_config("mytool", &["my-org".to_string()], "orb");
let subcommands = config
.subcommand
.as_ref()
.expect("subcommand section missing");
let help = subcommands.get("help").expect("help entry missing");
assert_eq!(
help.generate_job,
Some(false),
"help subcommand must be suppressed in bootstrap config"
);
}
#[test]
fn bootstrap_config_has_orb_dir() {
let config = build_bootstrap_config("mytool", &["my-org".to_string()], "custom-orb");
assert_eq!(
config.orb.as_ref().unwrap().orb_dir.as_deref(),
Some("custom-orb")
);
}
}