use crate::command::{CommandRunner, SystemCommandRunner};
use crate::config::Config;
use crate::doctor::{CapabilityStatus, run_doctor};
use crate::domain::{ArtifactIdentity, ReleaseIntent, ReleasePlan};
use crate::git::GitRepository;
use crate::lifecycle::{PublishRequest, publish_release_with_progress, resolve_plan};
use crate::progress::StderrProgressReporter;
use anyhow::Result;
use chrono::Local;
use clap::{Args, Parser, Subcommand};
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
#[derive(Debug, Parser)]
#[command(version, about)]
struct Cli {
#[arg(long, default_value = "release.toml", global = true)]
config: PathBuf,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Doctor,
Plan(ReleaseArguments),
Publish(PublishArguments),
}
#[derive(Clone, Debug, Args)]
struct ReleaseArguments {
#[arg(long)]
release: bool,
#[arg(long = "target")]
targets: Vec<String>,
#[arg(long, conflicts_with = "targets")]
all: bool,
}
#[derive(Clone, Debug, Args)]
struct PublishArguments {
#[command(flatten)]
release: ReleaseArguments,
#[arg(long)]
yes: bool,
}
pub fn entrypoint() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("error: {error:#}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
let config = Config::load(&cli.config)?;
let root = cli
.config
.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| std::path::Path::new("."));
match cli.command {
Command::Doctor => {
let report = run_doctor(&config, root, Arc::new(SystemCommandRunner))?;
for capability in report.capabilities {
let status = match capability.status {
CapabilityStatus::Verified => "VERIFIED",
CapabilityStatus::Unverifiable => "UNVERIFIABLE",
};
println!("{status} {}", capability.description);
}
Ok(())
}
Command::Plan(arguments) => {
let runner = Arc::new(SystemCommandRunner);
let repository = GitRepository::new(root, runner.clone());
let snapshot = repository.snapshot(&config.repository.branch)?;
let intent = if arguments.release {
ReleaseIntent::New
} else {
ReleaseIntent::Existing
};
let plan = resolve_plan(
&config,
&snapshot,
intent,
Local::now().date_naive(),
&arguments.targets,
arguments.all,
runner,
)?;
print_plan(&plan);
Ok(())
}
Command::Publish(arguments) => {
let intent = if arguments.release.release {
ReleaseIntent::New
} else {
ReleaseIntent::Existing
};
let runner: Arc<dyn CommandRunner> = Arc::new(SystemCommandRunner);
let assume_yes = arguments.yes;
let mut confirm = |plan: &ReleasePlan| -> Result<bool> {
if assume_yes {
return Ok(true);
}
print!(
"Publish {} at commit {}? [y/N] ",
plan.release.tag, plan.release.commit
);
io::stdout().flush()?;
let mut answer = String::new();
io::stdin().read_line(&mut answer)?;
Ok(matches!(answer.trim(), "y" | "Y" | "yes" | "YES"))
};
let outcomes = publish_release_with_progress(
&config,
root,
&PublishRequest {
intent,
today: Local::now().date_naive(),
targets: arguments.release.targets,
all_targets: arguments.release.all,
},
runner,
&mut confirm,
&StderrProgressReporter,
)?;
for outcome in outcomes {
if outcome.skipped {
println!(
"release-tool {}: verified existing {} at {}; skipped (commit {})",
outcome.tool_version, outcome.target, outcome.tag, outcome.commit
);
} else {
println!(
"release-tool {}: published {} at {} ({})",
outcome.tool_version, outcome.target, outcome.tag, outcome.commit
);
}
}
Ok(())
}
}
}
fn print_plan(plan: &ReleasePlan) {
println!("release-tool: {}", plan.tool_version);
println!("repository: {}", plan.release.repository);
println!("commit: {}", plan.release.commit);
println!("tag: {}", plan.release.tag);
println!("sealed: {}", plan.release.tag_already_sealed);
for target in &plan.targets {
println!("target: {}", target.name);
println!(" publisher: {}", target.publisher);
for artifact in &target.artifacts {
match artifact {
ArtifactIdentity::GithubReleaseAsset { name } => {
println!(" artifact: github-release:{name}");
}
ArtifactIdentity::MavenPackage {
group_id,
artifact_id,
version,
extension,
} => println!(" artifact: maven:{group_id}:{artifact_id}:{version}:{extension}"),
}
}
}
}