Skip to main content

release_tool/
cli.rs

1use crate::command::{CommandRunner, SystemCommandRunner};
2use crate::config::Config;
3use crate::doctor::{CapabilityStatus, run_doctor};
4use crate::domain::{ArtifactIdentity, ReleaseIntent, ReleasePlan};
5use crate::git::GitRepository;
6use crate::lifecycle::{PublishRequest, publish_release_with_progress, resolve_plan};
7use crate::progress::StderrProgressReporter;
8use anyhow::Result;
9use chrono::Local;
10use clap::{Args, Parser, Subcommand};
11use std::io::{self, Write};
12use std::path::PathBuf;
13use std::process::ExitCode;
14use std::sync::Arc;
15
16#[derive(Debug, Parser)]
17#[command(version, about)]
18struct Cli {
19    #[arg(long, default_value = "release.toml", global = true)]
20    config: PathBuf,
21
22    #[command(subcommand)]
23    command: Command,
24}
25
26#[derive(Debug, Subcommand)]
27enum Command {
28    Doctor,
29    Plan(ReleaseArguments),
30    Publish(PublishArguments),
31}
32
33#[derive(Clone, Debug, Args)]
34struct ReleaseArguments {
35    #[arg(long)]
36    release: bool,
37
38    #[arg(long = "target")]
39    targets: Vec<String>,
40
41    #[arg(long, conflicts_with = "targets")]
42    all: bool,
43}
44
45#[derive(Clone, Debug, Args)]
46struct PublishArguments {
47    #[command(flatten)]
48    release: ReleaseArguments,
49
50    #[arg(long)]
51    yes: bool,
52}
53
54/// Runs the release-tool CLI using the current process arguments and I/O.
55///
56/// Cargo-based adopters call this from a one-line runner binary, so Cargo owns
57/// source acquisition, locking, compilation, and caching without a separate
58/// release-tool installation step.
59pub fn entrypoint() -> ExitCode {
60    match run() {
61        Ok(()) => ExitCode::SUCCESS,
62        Err(error) => {
63            eprintln!("error: {error:#}");
64            ExitCode::FAILURE
65        }
66    }
67}
68
69fn run() -> Result<()> {
70    let cli = Cli::parse();
71    let config = Config::load(&cli.config)?;
72    let root = cli
73        .config
74        .parent()
75        .filter(|path| !path.as_os_str().is_empty())
76        .unwrap_or_else(|| std::path::Path::new("."));
77    match cli.command {
78        Command::Doctor => {
79            let report = run_doctor(&config, root, Arc::new(SystemCommandRunner))?;
80            for capability in report.capabilities {
81                let status = match capability.status {
82                    CapabilityStatus::Verified => "VERIFIED",
83                    CapabilityStatus::Unverifiable => "UNVERIFIABLE",
84                };
85                println!("{status} {}", capability.description);
86            }
87            Ok(())
88        }
89        Command::Plan(arguments) => {
90            let runner = Arc::new(SystemCommandRunner);
91            let repository = GitRepository::new(root, runner.clone());
92            let snapshot = repository.snapshot(&config.repository.branch)?;
93            let intent = if arguments.release {
94                ReleaseIntent::New
95            } else {
96                ReleaseIntent::Existing
97            };
98            let plan = resolve_plan(
99                &config,
100                &snapshot,
101                intent,
102                Local::now().date_naive(),
103                &arguments.targets,
104                arguments.all,
105                runner,
106            )?;
107            print_plan(&plan);
108            Ok(())
109        }
110        Command::Publish(arguments) => {
111            let intent = if arguments.release.release {
112                ReleaseIntent::New
113            } else {
114                ReleaseIntent::Existing
115            };
116            let runner: Arc<dyn CommandRunner> = Arc::new(SystemCommandRunner);
117            let assume_yes = arguments.yes;
118            let mut confirm = |plan: &ReleasePlan| -> Result<bool> {
119                if assume_yes {
120                    return Ok(true);
121                }
122                print!(
123                    "Publish {} at commit {}? [y/N] ",
124                    plan.release.tag, plan.release.commit
125                );
126                io::stdout().flush()?;
127                let mut answer = String::new();
128                io::stdin().read_line(&mut answer)?;
129                Ok(matches!(answer.trim(), "y" | "Y" | "yes" | "YES"))
130            };
131            let outcomes = publish_release_with_progress(
132                &config,
133                root,
134                &PublishRequest {
135                    intent,
136                    today: Local::now().date_naive(),
137                    targets: arguments.release.targets,
138                    all_targets: arguments.release.all,
139                },
140                runner,
141                &mut confirm,
142                &StderrProgressReporter,
143            )?;
144            for outcome in outcomes {
145                if outcome.skipped {
146                    println!(
147                        "release-tool {}: verified existing {} at {}; skipped (commit {})",
148                        outcome.tool_version, outcome.target, outcome.tag, outcome.commit
149                    );
150                } else {
151                    println!(
152                        "release-tool {}: published {} at {} ({})",
153                        outcome.tool_version, outcome.target, outcome.tag, outcome.commit
154                    );
155                }
156            }
157            Ok(())
158        }
159    }
160}
161
162fn print_plan(plan: &ReleasePlan) {
163    println!("release-tool: {}", plan.tool_version);
164    println!("repository: {}", plan.release.repository);
165    println!("commit: {}", plan.release.commit);
166    println!("tag: {}", plan.release.tag);
167    println!("sealed: {}", plan.release.tag_already_sealed);
168    for target in &plan.targets {
169        println!("target: {}", target.name);
170        println!("  publisher: {}", target.publisher);
171        for artifact in &target.artifacts {
172            match artifact {
173                ArtifactIdentity::GithubReleaseAsset { name } => {
174                    println!("  artifact: github-release:{name}");
175                }
176                ArtifactIdentity::MavenPackage {
177                    group_id,
178                    artifact_id,
179                    version,
180                    extension,
181                } => println!("  artifact: maven:{group_id}:{artifact_id}:{version}:{extension}"),
182                ArtifactIdentity::OciImage {
183                    repository,
184                    tag,
185                    platform,
186                } => println!("  artifact: oci:{repository}:{tag} ({platform})"),
187            }
188        }
189    }
190}