Skip to main content

gen_circleci_orb/commands/
init.rs

1use anyhow::Result;
2use std::path::PathBuf;
3
4use crate::{ci_patcher, commands::generate::Generate};
5
6pub const DEFAULT_DOCKER_ORB_VERSION: &str = "3.0.1";
7
8/// Wire orb generation into an existing repo's CI configuration.
9#[derive(Debug, clap::Args)]
10pub struct Init {
11    /// Name of the binary to introspect (must be on PATH).
12    #[arg(long)]
13    pub binary: String,
14
15    /// CircleCI namespace(s) to publish the orb under as a public orb (repeatable).
16    /// Must be set correctly on first init — visibility cannot be changed after the orb is created.
17    #[arg(long = "public-orb-namespace")]
18    pub public_orb_namespaces: Vec<String>,
19
20    /// CircleCI namespace(s) to publish the orb under as a private orb (repeatable).
21    /// Each listed namespace gets `--private` in its `circleci orb create` command.
22    /// Must be set correctly on first init — visibility cannot be changed after the orb is created.
23    #[arg(long = "private-orb-namespace")]
24    pub private_orb_namespaces: Vec<String>,
25
26    /// Name of the build/validation workflow to patch.
27    #[arg(long)]
28    pub build_workflow: String,
29
30    /// Name of the release workflow to patch.
31    #[arg(long)]
32    pub release_workflow: String,
33
34    /// Job in the build workflow that regenerate-orb should require.
35    #[arg(long)]
36    pub requires_job: Option<String>,
37
38    /// Job in the release workflow after which the generated release jobs
39    /// (build-binary-release, pack-orb-release, build-container, ensure-orb-registered)
40    /// should be gated. This is the sole mechanism for specifying where the generated
41    /// jobs plug into the existing pipeline topology.
42    #[arg(long)]
43    pub release_after_job: String,
44
45    /// Output directory for the generated orb source (relative to repo root).
46    #[arg(long, default_value = "orb")]
47    pub orb_dir: String,
48
49    /// Path to the .circleci/ directory.
50    #[arg(long, default_value = ".circleci")]
51    pub ci_dir: PathBuf,
52
53    /// circleci/orb-tools version to pin in generated CI.
54    #[arg(long, default_value = "12.3.3")]
55    pub orb_tools_version: String,
56
57    /// circleci/docker orb version to pin in generated CI.
58    #[arg(long, default_value = DEFAULT_DOCKER_ORB_VERSION)]
59    pub docker_orb_version: String,
60
61    /// Docker Hub (or registry) namespace for the built container image.
62    #[arg(long)]
63    pub docker_namespace: String,
64
65    /// CircleCI context name holding Docker Hub credentials.
66    #[arg(long, default_value = "docker-credentials")]
67    pub docker_context: String,
68
69    /// CircleCI context name holding orb publishing credentials.
70    #[arg(long, default_value = "orb-publishing")]
71    pub orb_context: String,
72
73    /// Wire in toolkit/build_mcp_server after orb publish (requires jerus-org/circleci-toolkit).
74    #[arg(long)]
75    pub mcp: bool,
76
77    /// Show planned changes without modifying any files.
78    #[arg(long)]
79    pub dry_run: bool,
80}
81
82impl Init {
83    pub fn run(&self) -> Result<()> {
84        let namespaces: Vec<String> = self
85            .public_orb_namespaces
86            .iter()
87            .chain(self.private_orb_namespaces.iter())
88            .cloned()
89            .collect();
90
91        // Step 1: generate orb source files
92        tracing::info!("Generating orb source into ./{}", self.orb_dir);
93        let gen = Generate {
94            binary: self.binary.clone(),
95            namespaces: namespaces.clone(),
96            output: PathBuf::from("."),
97            orb_dir: self.orb_dir.clone(),
98            install_method: crate::commands::generate::InstallMethod::Binstall,
99            base_image: crate::commands::generate::DEFAULT_BASE_IMAGE.to_string(),
100            home_url: None,
101            source_url: None,
102            dry_run: self.dry_run,
103        };
104        gen.run()?;
105
106        // Step 2: patch CI configs
107        let opts = ci_patcher::PatchOpts {
108            binary: self.binary.clone(),
109            namespaces,
110            docker_namespace: self.docker_namespace.clone(),
111            orb_dir: self.orb_dir.clone(),
112            build_workflow: self.build_workflow.clone(),
113            release_workflow: self.release_workflow.clone(),
114            requires_job: self.requires_job.clone(),
115            release_after_job: self.release_after_job.clone(),
116            orb_tools_version: self.orb_tools_version.clone(),
117            docker_orb_version: self.docker_orb_version.clone(),
118            docker_context: self.docker_context.clone(),
119            orb_context: self.orb_context.clone(),
120            private_namespaces: self.private_orb_namespaces.clone(),
121            mcp: self.mcp,
122        };
123
124        let summary = ci_patcher::apply_patches(&self.ci_dir, &opts, self.dry_run)?;
125        for line in &summary {
126            println!("{line}");
127        }
128
129        if self.dry_run {
130            println!("(dry-run: no files written)");
131        } else {
132            println!("Done.");
133        }
134
135        Ok(())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::DEFAULT_DOCKER_ORB_VERSION;
142
143    #[test]
144    fn default_docker_orb_version_matches_registry() {
145        // The CircleCI registry has circleci/docker@3.0.1 as latest.
146        // 3.2.0 does not exist and causes "Cannot find circleci/docker@3.2.0" errors.
147        assert_eq!(
148            DEFAULT_DOCKER_ORB_VERSION, "3.0.1",
149            "DEFAULT_DOCKER_ORB_VERSION must be the registry-available version"
150        );
151    }
152}