use crate::error::{Result, PublishError};
use crate::publish::{CargoPublisher, PublishConfig, PublishResult, YankResult};
use crate::workspace::{WorkspaceInfo, DependencyGraph, PublishTier};
use semver::Version;
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug)]
pub struct Publisher {
workspace: WorkspaceInfo,
dependency_graph: DependencyGraph,
cargo_publisher: CargoPublisher,
config: PublisherConfig,
publish_state: PublishState,
}
#[derive(Debug, Clone)]
pub struct PublisherConfig {
pub inter_package_delay: Duration,
pub dry_run_first: bool,
pub continue_on_failure: bool,
pub max_concurrent_per_tier: usize,
pub registry: Option<String>,
pub allow_dirty: bool,
pub additional_cargo_args: Vec<String>,
}
impl Default for PublisherConfig {
fn default() -> Self {
Self {
inter_package_delay: Duration::from_secs(15), dry_run_first: true,
continue_on_failure: false,
max_concurrent_per_tier: 3,
registry: None,
allow_dirty: false,
additional_cargo_args: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
struct PublishState {
completed_publishes: HashMap<String, PublishResult>,
failed_packages: HashMap<String, String>,
current_tier: usize,
total_tiers: usize,
start_time: Option<std::time::Instant>,
}
impl Default for PublishState {
fn default() -> Self {
Self {
completed_publishes: HashMap::new(),
failed_packages: HashMap::new(),
current_tier: 0,
total_tiers: 0,
start_time: None,
}
}
}
#[derive(Debug, Clone)]
pub struct PublishingResult {
pub successful_publishes: HashMap<String, PublishResult>,
pub failed_packages: HashMap<String, String>,
pub total_duration: Duration,
pub tiers_processed: usize,
pub all_successful: bool,
}
#[derive(Debug, Clone)]
pub struct RollbackResult {
pub yanked_packages: HashMap<String, YankResult>,
pub yank_failures: HashMap<String, String>,
pub duration: Duration,
pub fully_successful: bool,
}
impl Publisher {
pub fn new(workspace: &WorkspaceInfo) -> Result<Self> {
let dependency_graph = DependencyGraph::build(workspace)?;
let cargo_publisher = CargoPublisher::new();
let config = PublisherConfig::default();
let publish_state = PublishState::default();
Ok(Self {
workspace: workspace.clone(),
dependency_graph,
cargo_publisher,
config,
publish_state,
})
}
pub fn with_config(workspace: &WorkspaceInfo, config: PublisherConfig) -> Result<Self> {
let dependency_graph = DependencyGraph::build(workspace)?;
let cargo_publisher = CargoPublisher::new();
let publish_state = PublishState::default();
Ok(Self {
workspace: workspace.clone(),
dependency_graph,
cargo_publisher,
config,
publish_state,
})
}
pub async fn publish_all_packages(&mut self) -> Result<PublishingResult> {
self.publish_state.start_time = Some(std::time::Instant::now());
let publish_order = self.dependency_graph.publish_order()?;
self.publish_state.total_tiers = publish_order.tier_count();
for (tier_index, tier) in publish_order.tiers.iter().enumerate() {
self.publish_state.current_tier = tier_index;
match self.publish_tier(tier).await {
Ok(()) => {
if tier_index < publish_order.tiers.len() - 1 {
sleep(self.config.inter_package_delay).await;
}
}
Err(e) if self.config.continue_on_failure => {
eprintln!("Tier {} failed but continuing: {}", tier_index, e);
}
Err(e) => {
return Err(e);
}
}
}
let total_duration = self.publish_state.start_time
.map(|start| start.elapsed())
.unwrap_or_default();
let all_successful = self.publish_state.failed_packages.is_empty();
Ok(PublishingResult {
successful_publishes: self.publish_state.completed_publishes.clone(),
failed_packages: self.publish_state.failed_packages.clone(),
total_duration,
tiers_processed: self.publish_state.current_tier + 1,
all_successful,
})
}
async fn publish_tier(&mut self, tier: &PublishTier) -> Result<()> {
let publish_config = self.create_publish_config();
if tier.packages.len() == 1 {
let package_name = &tier.packages[0];
self.publish_single_package(package_name, &publish_config).await?;
} else {
self.publish_packages_concurrently(&tier.packages, &publish_config).await?;
}
Ok(())
}
async fn publish_single_package(
&mut self,
package_name: &str,
publish_config: &PublishConfig,
) -> Result<()> {
let package_info = self.workspace.get_package(package_name)?;
println!("📦 Publishing {} v{}...", package_name, package_info.version);
match self.cargo_publisher.publish_package(package_info, publish_config).await {
Ok(result) => {
println!("✅ {}", result.summary());
self.publish_state.completed_publishes.insert(package_name.to_string(), result);
Ok(())
}
Err(e) => {
let error_msg = format!("Failed to publish {}: {}", package_name, e);
self.publish_state.failed_packages.insert(package_name.to_string(), error_msg.clone());
Err(PublishError::PublishFailed {
package: package_name.to_string(),
reason: error_msg,
}.into())
}
}
}
async fn publish_packages_concurrently(
&mut self,
package_names: &[String],
publish_config: &PublishConfig,
) -> Result<()> {
use tokio::sync::Semaphore;
use std::sync::Arc;
let semaphore = Arc::new(Semaphore::new(self.config.max_concurrent_per_tier));
let mut handles = Vec::new();
for package_name in package_names {
let package_info = self.workspace.get_package(package_name)?.clone();
let publisher = self.cargo_publisher.clone();
let config = publish_config.clone();
let semaphore = Arc::clone(&semaphore);
let package_name = package_name.clone();
let handle = tokio::spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
println!("📦 Publishing {} v{}...", package_name, package_info.version);
let result = publisher.publish_package(&package_info, &config).await;
(package_name, result)
});
handles.push(handle);
}
for handle in handles {
let (package_name, result) = handle.await
.map_err(|e| PublishError::PublishFailed {
package: "unknown".to_string(),
reason: format!("Task join error: {}", e),
})?;
match result {
Ok(publish_result) => {
println!("✅ {}", publish_result.summary());
self.publish_state.completed_publishes.insert(package_name, publish_result);
}
Err(e) => {
let error_msg = format!("Failed to publish {}: {}", package_name, e);
self.publish_state.failed_packages.insert(package_name.clone(), error_msg.clone());
if !self.config.continue_on_failure {
return Err(PublishError::PublishFailed {
package: package_name,
reason: error_msg,
}.into());
}
}
}
}
Ok(())
}
pub async fn rollback_published_packages(&self) -> Result<RollbackResult> {
let start_time = std::time::Instant::now();
let mut yanked_packages = HashMap::new();
let mut yank_failures = HashMap::new();
let publish_config = self.create_publish_config();
let publish_order = self.dependency_graph.publish_order()?;
let mut packages_to_yank: Vec<_> = publish_order.ordered_packages().collect();
packages_to_yank.reverse();
for package_name in packages_to_yank {
if let Some(publish_result) = self.publish_state.completed_publishes.get(package_name) {
println!("🔄 Yanking {} v{}...", package_name, publish_result.version);
match self.cargo_publisher.yank_package(
package_name,
&publish_result.version,
&publish_config,
).await {
Ok(yank_result) => {
println!("✅ {}", yank_result.format_result());
yanked_packages.insert(package_name.to_string(), yank_result);
}
Err(e) => {
let error_msg = format!("Failed to yank {}: {}", package_name, e);
println!("❌ {}", error_msg);
yank_failures.insert(package_name.to_string(), error_msg);
}
}
}
}
let duration = start_time.elapsed();
let fully_successful = yank_failures.is_empty();
Ok(RollbackResult {
yanked_packages,
yank_failures,
duration,
fully_successful,
})
}
fn create_publish_config(&self) -> PublishConfig {
PublishConfig {
registry: self.config.registry.clone(),
dry_run_first: self.config.dry_run_first, allow_dirty: self.config.allow_dirty,
additional_args: self.config.additional_cargo_args.clone(),
token: None, }
}
pub async fn check_already_published(&self) -> Result<HashMap<String, bool>> {
let mut results = HashMap::new();
for (package_name, package_info) in &self.workspace.packages {
let version = Version::parse(&package_info.version)
.map_err(|e| PublishError::PublishFailed {
package: package_name.clone(),
reason: format!("Invalid version: {}", e),
})?;
let is_published = self.cargo_publisher.is_package_published(package_name, &version).await?;
results.insert(package_name.clone(), is_published);
}
Ok(results)
}
pub fn get_progress(&self) -> PublishProgress {
let total_packages = self.workspace.packages.len();
let completed_packages = self.publish_state.completed_publishes.len();
let failed_packages = self.publish_state.failed_packages.len();
let remaining_packages = total_packages - completed_packages - failed_packages;
PublishProgress {
total_packages,
completed_packages,
failed_packages,
remaining_packages,
current_tier: self.publish_state.current_tier,
total_tiers: self.publish_state.total_tiers,
elapsed_time: self.publish_state.start_time.map(|start| start.elapsed()),
}
}
pub fn set_config(&mut self, config: PublisherConfig) {
self.config = config;
}
pub fn config(&self) -> &PublisherConfig {
&self.config
}
pub fn clear_state(&mut self) {
self.publish_state = PublishState::default();
}
}
#[derive(Debug, Clone)]
pub struct PublishProgress {
pub total_packages: usize,
pub completed_packages: usize,
pub failed_packages: usize,
pub remaining_packages: usize,
pub current_tier: usize,
pub total_tiers: usize,
pub elapsed_time: Option<Duration>,
}
impl PublishingResult {
pub fn success_rate(&self) -> f64 {
let total = self.successful_publishes.len() + self.failed_packages.len();
if total == 0 {
0.0
} else {
(self.successful_publishes.len() as f64 / total as f64) * 100.0
}
}
pub fn format_summary(&self) -> String {
let status = if self.all_successful { "✅" } else { "⚠️" };
format!(
"{} Publishing completed: {}/{} packages successful ({:.1}%) in {:.2}s",
status,
self.successful_publishes.len(),
self.successful_publishes.len() + self.failed_packages.len(),
self.success_rate(),
self.total_duration.as_secs_f64()
)
}
pub fn format_report(&self) -> String {
let mut report = self.format_summary();
report.push('\n');
if !self.successful_publishes.is_empty() {
report.push_str("\n📦 Successfully Published:\n");
for (_package, result) in &self.successful_publishes {
report.push_str(&format!(" ✅ {}\n", result.summary()));
}
}
if !self.failed_packages.is_empty() {
report.push_str("\n❌ Failed Packages:\n");
for (package, error) in &self.failed_packages {
report.push_str(&format!(" ❌ {}: {}\n", package, error));
}
}
report
}
}
impl RollbackResult {
pub fn format_summary(&self) -> String {
let status = if self.fully_successful { "✅" } else { "⚠️" };
format!(
"{} Rollback completed: {}/{} packages yanked in {:.2}s",
status,
self.yanked_packages.len(),
self.yanked_packages.len() + self.yank_failures.len(),
self.duration.as_secs_f64()
)
}
}
impl PublishProgress {
pub fn completion_percentage(&self) -> f64 {
if self.total_packages == 0 {
100.0
} else {
((self.completed_packages + self.failed_packages) as f64 / self.total_packages as f64) * 100.0
}
}
pub fn format_progress(&self) -> String {
let elapsed = self.elapsed_time
.map(|d| format!(" ({}s)", d.as_secs()))
.unwrap_or_default();
format!(
"📊 Progress: {:.1}% ({}/{} packages) - Tier {}/{}{} - {} remaining",
self.completion_percentage(),
self.completed_packages,
self.total_packages,
self.current_tier + 1,
self.total_tiers,
elapsed,
self.remaining_packages
)
}
}