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 #[arg(long)]
55 skip_tests: bool,
56}
57
58pub fn entrypoint() -> ExitCode {
64 match run() {
65 Ok(()) => ExitCode::SUCCESS,
66 Err(error) => {
67 eprintln!("error: {error:#}");
68 ExitCode::FAILURE
69 }
70 }
71}
72
73fn run() -> Result<()> {
74 let cli = Cli::parse();
75 let mut config = Config::load(&cli.config)?;
76 let root = cli
77 .config
78 .parent()
79 .filter(|path| !path.as_os_str().is_empty())
80 .unwrap_or_else(|| std::path::Path::new("."));
81 match cli.command {
82 Command::Doctor => {
83 let report = run_doctor(&config, root, Arc::new(SystemCommandRunner))?;
84 for capability in report.capabilities {
85 let status = match capability.status {
86 CapabilityStatus::Verified => "VERIFIED",
87 CapabilityStatus::Unverifiable => "UNVERIFIABLE",
88 };
89 println!("{status} {}", capability.description);
90 }
91 Ok(())
92 }
93 Command::Plan(arguments) => {
94 let runner = Arc::new(SystemCommandRunner);
95 let repository = GitRepository::new(root, runner.clone());
96 let snapshot = repository.snapshot(&config.repository.branch)?;
97 let intent = if arguments.release {
98 ReleaseIntent::New
99 } else {
100 ReleaseIntent::Existing
101 };
102 let plan = resolve_plan(
103 &config,
104 &snapshot,
105 intent,
106 Local::now().date_naive(),
107 &arguments.targets,
108 arguments.all,
109 runner,
110 )?;
111 print_plan(&plan);
112 Ok(())
113 }
114 Command::Publish(arguments) => {
115 if arguments.skip_tests {
116 config.hooks.preflight.clear();
117 eprintln!("[release-tool] Skipping project preflight (--skip-tests)");
118 }
119 let intent = if arguments.release.release {
120 ReleaseIntent::New
121 } else {
122 ReleaseIntent::Existing
123 };
124 let runner: Arc<dyn CommandRunner> = Arc::new(SystemCommandRunner);
125 let assume_yes = arguments.yes;
126 let mut confirm = |plan: &ReleasePlan| -> Result<bool> {
127 if assume_yes {
128 return Ok(true);
129 }
130 print!(
131 "Publish {} at commit {}? [y/N] ",
132 plan.release.tag, plan.release.commit
133 );
134 io::stdout().flush()?;
135 let mut answer = String::new();
136 io::stdin().read_line(&mut answer)?;
137 Ok(matches!(answer.trim(), "y" | "Y" | "yes" | "YES"))
138 };
139 let outcomes = publish_release_with_progress(
140 &config,
141 root,
142 &PublishRequest {
143 intent,
144 today: Local::now().date_naive(),
145 targets: arguments.release.targets,
146 all_targets: arguments.release.all,
147 },
148 runner,
149 &mut confirm,
150 &StderrProgressReporter,
151 )?;
152 for outcome in outcomes {
153 if outcome.skipped {
154 println!(
155 "release-tool {}: verified existing {} at {}; skipped (commit {})",
156 outcome.tool_version, outcome.target, outcome.tag, outcome.commit
157 );
158 } else {
159 println!(
160 "release-tool {}: published {} at {} ({})",
161 outcome.tool_version, outcome.target, outcome.tag, outcome.commit
162 );
163 }
164 }
165 Ok(())
166 }
167 }
168}
169
170fn print_plan(plan: &ReleasePlan) {
171 println!("release-tool: {}", plan.tool_version);
172 println!("repository: {}", plan.release.repository);
173 println!("commit: {}", plan.release.commit);
174 println!("tag: {}", plan.release.tag);
175 println!("sealed: {}", plan.release.tag_already_sealed);
176 for target in &plan.targets {
177 println!("target: {}", target.name);
178 println!(" publisher: {}", target.publisher);
179 for artifact in &target.artifacts {
180 match artifact {
181 ArtifactIdentity::GithubReleaseAsset { name } => {
182 println!(" artifact: github-release:{name}");
183 }
184 ArtifactIdentity::MavenPackage {
185 group_id,
186 artifact_id,
187 version,
188 extension,
189 classifier,
190 } => {
191 let classifier = classifier
192 .as_ref()
193 .map(|value| format!(":{value}"))
194 .unwrap_or_default();
195 println!(
196 " artifact: maven:{group_id}:{artifact_id}:{version}:{extension}{classifier}"
197 );
198 }
199 ArtifactIdentity::OciImage {
200 repository,
201 tag,
202 platform,
203 } => println!(" artifact: oci:{repository}:{tag} ({platform})"),
204 }
205 }
206 }
207}