use crate::stack::checker::StackChecker;
use crate::stack::types::*;
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::path::Path;
pub use super::super::releaser_types::{
format_plan_text, BumpType, ReleaseConfig, ReleaseResult, ReleasedCrate,
};
pub struct ReleaseOrchestrator {
pub(in crate::stack) config: ReleaseConfig,
checker: StackChecker,
preflight_results: HashMap<String, PreflightResult>,
}
impl ReleaseOrchestrator {
pub fn new(checker: StackChecker, config: ReleaseConfig) -> Self {
Self { config, checker, preflight_results: HashMap::new() }
}
#[cfg(feature = "native")]
pub fn from_workspace(workspace_path: &Path, config: ReleaseConfig) -> Result<Self> {
let checker = StackChecker::from_workspace(workspace_path)?;
Ok(Self::new(checker, config))
}
pub fn plan_release(&mut self, crate_name: &str) -> Result<ReleasePlan> {
let release_order = self.checker.release_order_for(crate_name)?;
let mut releases = Vec::new();
for name in &release_order {
let planned = self.plan_single_release(name)?;
releases.push(planned);
}
Ok(ReleasePlan {
releases,
dry_run: self.config.dry_run,
preflight_results: self.preflight_results.clone(),
})
}
pub fn plan_all_releases(&mut self) -> Result<ReleasePlan> {
let release_order = self.checker.topological_order()?;
let mut releases = Vec::new();
for name in &release_order {
let planned = self.plan_single_release(name)?;
releases.push(planned);
}
Ok(ReleasePlan {
releases,
dry_run: self.config.dry_run,
preflight_results: self.preflight_results.clone(),
})
}
fn plan_single_release(&self, crate_name: &str) -> Result<PlannedRelease> {
let current_version = self
.checker
.get_crate(crate_name)
.map(|c| c.local_version.clone())
.unwrap_or_else(|| semver::Version::new(0, 0, 0));
let new_version = match self.config.bump_type {
Some(bump) => bump.apply(¤t_version),
None => semver::Version::new(
current_version.major,
current_version.minor,
current_version.patch + 1,
),
};
let ready = self.preflight_results.get(crate_name).map(|r| r.passed).unwrap_or(true);
Ok(PlannedRelease {
crate_name: crate_name.to_string(),
current_version,
new_version,
dependents: vec![],
ready,
})
}
pub fn run_preflight(
&mut self,
crate_name: &str,
crate_path: &Path,
) -> Result<PreflightResult> {
let mut result = PreflightResult::new(crate_name);
if self.config.no_verify {
result.add_check(PreflightCheck::pass("verification", "Skipped (--no-verify)"));
self.preflight_results.insert(crate_name.to_string(), result.clone());
return Ok(result);
}
let git_check = self.check_git_clean(crate_path);
result.add_check(git_check);
let lint_check = self.check_lint(crate_path);
result.add_check(lint_check);
let coverage_check = self.check_coverage(crate_path);
result.add_check(coverage_check);
let comply_check = self.check_pmat_comply(crate_path);
result.add_check(comply_check);
let path_check = self.check_no_path_deps(crate_name);
result.add_check(path_check);
let version_check = self.check_version_bumped(crate_name);
result.add_check(version_check);
let quality_gate_check = self.check_pmat_quality_gate(crate_path);
result.add_check(quality_gate_check);
let tdg_check = self.check_pmat_tdg(crate_path);
result.add_check(tdg_check);
let dead_code_check = self.check_pmat_dead_code(crate_path);
result.add_check(dead_code_check);
let complexity_check = self.check_pmat_complexity(crate_path);
result.add_check(complexity_check);
let satd_check = self.check_pmat_satd(crate_path);
result.add_check(satd_check);
let popper_check = self.check_pmat_popper(crate_path);
result.add_check(popper_check);
let book_check = self.check_book_build(crate_path);
result.add_check(book_check);
let examples_check = self.check_examples_run(crate_path);
result.add_check(examples_check);
self.preflight_results.insert(crate_name.to_string(), result.clone());
Ok(result)
}
#[cfg(feature = "native")]
pub fn execute(&self, plan: &ReleasePlan) -> Result<ReleaseResult> {
if plan.dry_run {
return Ok(ReleaseResult {
success: true,
released_crates: vec![],
message: "Dry run - no changes made".to_string(),
});
}
let mut released = Vec::new();
for release in &plan.releases {
if let Some(preflight) = plan.preflight_results.get(&release.crate_name) {
if !preflight.passed {
return Err(anyhow!(
"Pre-flight checks failed for {}: cannot release",
release.crate_name
));
}
}
let manifest_path = self
.checker
.get_crate(&release.crate_name)
.map(|c| c.manifest_path.clone())
.filter(|p| p.exists());
if let Some(ref path) = manifest_path {
self.update_cargo_toml(path, &release.new_version)?;
self.create_git_tag(&release.crate_name, &release.new_version)?;
}
if self.config.publish {
if let Some(ref path) = manifest_path {
let crate_dir = path.parent().unwrap_or(Path::new("."));
self.cargo_publish(crate_dir)?;
}
}
released.push(ReleasedCrate {
name: release.crate_name.clone(),
version: release.new_version.clone(),
published: self.config.publish && manifest_path.is_some(),
});
}
Ok(ReleaseResult {
success: true,
released_crates: released,
message: format!("Successfully released {} crates", plan.releases.len()),
})
}
#[cfg(feature = "native")]
pub(crate) fn update_cargo_toml(
&self,
manifest_path: &Path,
new_version: &semver::Version,
) -> Result<()> {
let content = std::fs::read_to_string(manifest_path)
.map_err(|e| anyhow!("Failed to read {}: {}", manifest_path.display(), e))?;
let version_str = new_version.to_string();
let mut output = String::with_capacity(content.len());
let mut in_package = false;
let mut version_replaced = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[package]" {
in_package = true;
} else if trimmed.starts_with('[') {
in_package = false;
}
if in_package && !version_replaced && trimmed.starts_with("version") {
if let Some(eq_pos) = line.find('=') {
let prefix = &line[..=eq_pos];
output.push_str(&format!("{} \"{}\"", prefix, version_str));
output.push('\n');
version_replaced = true;
continue;
}
}
output.push_str(line);
output.push('\n');
}
if !version_replaced {
return Err(anyhow!(
"Could not find version field in [package] section of {}",
manifest_path.display()
));
}
std::fs::write(manifest_path, output)
.map_err(|e| anyhow!("Failed to write {}: {}", manifest_path.display(), e))?;
Ok(())
}
#[cfg(feature = "native")]
fn create_git_tag(&self, crate_name: &str, version: &semver::Version) -> Result<()> {
let tag = format!("{}-v{}", crate_name, version);
let message = format!("Release {} v{}", crate_name, version);
let output = std::process::Command::new("git")
.args(["tag", "-a", &tag, "-m", &message])
.output()
.map_err(|e| anyhow!("Failed to create git tag: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Git tag failed: {}", stderr));
}
Ok(())
}
#[cfg(feature = "native")]
fn cargo_publish(&self, crate_dir: &Path) -> Result<()> {
let mut cmd = std::process::Command::new("cargo");
cmd.arg("publish").current_dir(crate_dir);
if self.config.dry_run {
cmd.arg("--dry-run");
}
let output = cmd.output().map_err(|e| anyhow!("Failed to run cargo publish: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("cargo publish failed: {}", stderr));
}
Ok(())
}
}