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
54pub 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}