use std::collections::BTreeMap;
use crate::release::publish::plan::LoadedPlan;
use crate::release::publish::registry::{RegistryInspector, RegistryStatus};
use crate::release::publish::report::{CrateResult, PublishReport};
use crate::release::publish::retry::{PublishOutcome, classify_status, decide_retry};
use crate::release::publish::tag::CrateTag;
use crate::release::version::Ybf;
#[allow(dead_code)]
pub(crate) trait Publisher {
fn publish_crate(&mut self, crate_name: &str, version: &Ybf) -> u16;
}
#[allow(dead_code)]
pub(crate) trait TagCreator {
fn create_tag(&mut self, tag: &str) -> Result<(), String>;
}
#[allow(dead_code)]
pub(crate) fn execute_plan(
plan: &LoadedPlan,
registry: &mut dyn RegistryInspector,
publisher: &mut dyn Publisher,
tag_creator: &mut dyn TagCreator,
) -> PublishReport {
let mut results: BTreeMap<String, CrateResult> = BTreeMap::new();
let ordered = plan.ordered_entries();
let mut failed = false;
let mut failure_reason = String::new();
for entry in &ordered {
if failed {
results.insert(
entry.crate_name.clone(),
CrateResult::Skipped {
reason: format!("prior failure: {failure_reason}"),
},
);
continue;
}
match registry.check_version(&entry.crate_name, &entry.version) {
RegistryStatus::Published => {
results.insert(entry.crate_name.clone(), CrateResult::AlreadyPublished);
continue;
}
RegistryStatus::Absent => { }
RegistryStatus::VersionMismatch { expected, found } => {
failed = true;
failure_reason = format!(
"version mismatch for {}: expected {expected}, found {found}",
entry.crate_name
);
results.insert(
entry.crate_name.clone(),
CrateResult::Failed {
reason: failure_reason.clone(),
},
);
continue;
}
}
let mut attempt: u32 = 1;
let outcome = loop {
let status = publisher.publish_crate(&entry.crate_name, &entry.version);
let this_outcome = classify_status(status);
match decide_retry(&this_outcome, attempt) {
crate::release::publish::retry::RetryDecision::Retry { attempt: next, .. } => {
attempt = next;
continue;
}
crate::release::publish::retry::RetryDecision::Fail => {
break this_outcome;
}
}
};
match outcome {
PublishOutcome::Success => {
let tag = CrateTag::render(&entry.crate_name, &entry.version);
match tag_creator.create_tag(&tag) {
Ok(()) => {
results.insert(entry.crate_name.clone(), CrateResult::Published { tag });
}
Err(error) => {
failed = true;
failure_reason =
format!("tag creation failed for {}: {error}", entry.crate_name);
results.insert(
entry.crate_name.clone(),
CrateResult::Failed {
reason: failure_reason.clone(),
},
);
}
}
}
PublishOutcome::AlreadyPublished => {
results.insert(entry.crate_name.clone(), CrateResult::AlreadyPublished);
}
PublishOutcome::RateLimited
| PublishOutcome::AuthFailed
| PublishOutcome::BuildFailed
| PublishOutcome::Failed => {
failed = true;
failure_reason = format!(
"publish {crate} v{version} failed: {outcome:?}",
crate = entry.crate_name,
version = entry.version,
);
results.insert(
entry.crate_name.clone(),
CrateResult::Failed {
reason: failure_reason.clone(),
},
);
}
}
}
let all_published = !failed
&& results.values().all(|r| {
matches!(
r,
CrateResult::Published { .. } | CrateResult::AlreadyPublished
)
});
PublishReport {
transaction_id: plan.transaction_id.clone(),
commit: plan.commit.clone(),
results,
all_published,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::release::publish::plan::{LoadedEntry, LoadedPlan};
use crate::release::publish::registry::FakeRegistry;
use crate::release::version::Ybf;
use std::collections::BTreeMap;
struct FakePublisher {
responses: Vec<(String, Ybf, u16)>,
calls: Vec<(String, Ybf)>,
}
impl FakePublisher {
fn new(responses: Vec<(&str, &str, u16)>) -> Self {
Self {
responses: responses
.into_iter()
.map(|(c, v, s)| (c.to_string(), Ybf::parse(v).unwrap(), s))
.collect(),
calls: Vec::new(),
}
}
}
impl Publisher for FakePublisher {
fn publish_crate(&mut self, crate_name: &str, version: &Ybf) -> u16 {
self.calls.push((crate_name.to_string(), *version));
for (name, ver, status) in &self.responses {
if name == crate_name && *ver == *version {
return *status;
}
}
201
}
}
struct FakeTagCreator {
created: Vec<String>,
fail: bool,
}
impl FakeTagCreator {
fn new() -> Self {
Self {
created: Vec::new(),
fail: false,
}
}
fn failing() -> Self {
Self {
created: Vec::new(),
fail: true,
}
}
}
impl TagCreator for FakeTagCreator {
fn create_tag(&mut self, tag: &str) -> Result<(), String> {
if self.fail {
return Err("simulated tag failure".to_string());
}
self.created.push(tag.to_string());
Ok(())
}
}
fn make_plan(entries: Vec<LoadedEntry>) -> LoadedPlan {
let map: BTreeMap<String, LoadedEntry> = entries
.into_iter()
.map(|e| (e.crate_name.clone(), e))
.collect();
LoadedPlan {
transaction_id: "2026-08-15.01".to_string(),
commit: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0".to_string(),
entries: map,
}
}
fn entry(name: &str, version: &str, order: usize) -> LoadedEntry {
LoadedEntry {
crate_name: name.to_string(),
version: Ybf::parse(version).unwrap(),
order,
}
}
#[test]
fn publish_all_success() {
let plan = make_plan(vec![
entry("arcature-auth", "2026.1.7", 1),
entry("arcature-cli", "2026.1.9", 2),
]);
let mut registry = FakeRegistry::default();
let mut publisher = FakePublisher::new(vec![]);
let mut tags = FakeTagCreator::new();
let report = execute_plan(&plan, &mut registry, &mut publisher, &mut tags);
assert!(report.all_published);
assert_eq!(report.published_count(), 2);
assert_eq!(report.failed_count(), 0);
assert_eq!(
tags.created,
vec!["arcature-auth-v2026.1.7", "arcature-cli-v2026.1.9"]
);
}
#[test]
fn idempotent_skip_already_published() {
let plan = make_plan(vec![entry("arcature-auth", "2026.1.7", 1)]);
let mut registry = FakeRegistry::default();
registry
.published
.insert(("arcature-auth".to_string(), "2026.1.7".to_string()), true);
let mut publisher = FakePublisher::new(vec![]);
let mut tags = FakeTagCreator::new();
let report = execute_plan(&plan, &mut registry, &mut publisher, &mut tags);
assert!(report.all_published);
assert_eq!(report.published_count(), 0);
assert_eq!(report.already_published_count(), 1);
assert!(tags.created.is_empty());
}
#[test]
fn partial_failure_skips_remaining() {
let plan = make_plan(vec![
entry("arcature-auth", "2026.1.7", 1),
entry("arcature-cli", "2026.1.9", 2),
entry("arcature-jobs", "2026.1.5", 3),
]);
let mut registry = FakeRegistry::default();
let mut publisher = FakePublisher::new(vec![("arcature-cli", "2026.1.9", 403)]);
let mut tags = FakeTagCreator::new();
let report = execute_plan(&plan, &mut registry, &mut publisher, &mut tags);
assert!(!report.all_published);
assert_eq!(report.published_count(), 1);
assert_eq!(report.failed_count(), 1);
assert_eq!(report.skipped_count(), 1);
assert!(matches!(
report.results.get("arcature-auth"),
Some(CrateResult::Published { .. })
));
assert!(matches!(
report.results.get("arcature-cli"),
Some(CrateResult::Failed { .. })
));
assert!(matches!(
report.results.get("arcature-jobs"),
Some(CrateResult::Skipped { .. })
));
}
#[test]
fn build_error_fails_fast() {
let plan = make_plan(vec![
entry("arcature-auth", "2026.1.7", 1),
entry("arcature-cli", "2026.1.9", 2),
]);
let mut registry = FakeRegistry::default();
let mut publisher = FakePublisher::new(vec![("arcature-auth", "2026.1.7", 400)]);
let mut tags = FakeTagCreator::new();
let report = execute_plan(&plan, &mut registry, &mut publisher, &mut tags);
assert!(!report.all_published);
assert_eq!(report.failed_count(), 1);
assert_eq!(report.skipped_count(), 1);
}
#[test]
fn tag_creation_failure_stops_remaining() {
let plan = make_plan(vec![
entry("arcature-auth", "2026.1.7", 1),
entry("arcature-cli", "2026.1.9", 2),
]);
let mut registry = FakeRegistry::default();
let mut publisher = FakePublisher::new(vec![]);
let mut tags = FakeTagCreator::failing();
let report = execute_plan(&plan, &mut registry, &mut publisher, &mut tags);
assert!(!report.all_published);
assert_eq!(report.failed_count(), 1);
assert_eq!(report.skipped_count(), 1);
assert!(matches!(
report.results.get("arcature-auth"),
Some(CrateResult::Failed { .. })
));
}
#[test]
fn empty_plan_succeeds() {
let plan = make_plan(vec![]);
let mut registry = FakeRegistry::default();
let mut publisher = FakePublisher::new(vec![]);
let mut tags = FakeTagCreator::new();
let report = execute_plan(&plan, &mut registry, &mut publisher, &mut tags);
assert!(report.all_published);
assert_eq!(report.results.len(), 0);
}
#[test]
fn registry_409_treated_as_already_published() {
let plan = make_plan(vec![entry("arcature-auth", "2026.1.7", 1)]);
let mut registry = FakeRegistry::default();
let mut publisher = FakePublisher::new(vec![("arcature-auth", "2026.1.7", 409)]);
let mut tags = FakeTagCreator::new();
let report = execute_plan(&plan, &mut registry, &mut publisher, &mut tags);
assert!(report.all_published);
assert_eq!(report.already_published_count(), 1);
assert_eq!(report.published_count(), 0);
assert!(tags.created.is_empty());
}
}