use std::path::Path;
use std::process::Command;
use std::time::Duration;
pub const PUBLISH_ORDER: &[&[&str]] = &[
&[
"rok-acl-macros",
"rok-audit-macros",
"rok-auth-macros",
"rok-config-macros",
"rok-events-macros",
"rok-orm-macros",
"rok-queue-macros",
"rok-search-macros",
"rok-validate-macros",
],
&[
"rok-cache",
"rok-cors",
"rok-encrypt",
"rok-feature",
"rok-hash",
"rok-health",
"rok-i18n",
"rok-ids",
"rok-lock",
"rok-mail",
"rok-orm-core",
"rok-orm-migrate",
"rok-problem",
"rok-rate-limit",
"rok-router",
"rok-shield",
"rok-studio",
"rok-telemetry",
"rok-testing",
"rok-websocket",
],
&[
"rok-acl",
"rok-audit",
"rok-auth",
"rok-config",
"rok-events",
"rok-orm",
"rok-orm-factory",
"rok-queue",
"rok-search",
"rok-storage",
"rok-validate",
],
&[
"rok-auth-basic",
"rok-auth-session",
"rok-auth-social",
"rok-bouncer",
"rok-media",
"rok-notification",
"rok-schedule",
],
&["rok-error", "rok-tui"],
&["rok-cli"],
];
fn run_gate(name: &str, cmd: &mut Command) -> bool {
print!(" {} ... ", name);
if std::io::Write::flush(&mut std::io::stdout()).is_err() {
}
let output = cmd.output();
match output {
Ok(out) if out.status.success() => {
println!("{}", console::style("PASS").green());
true
}
Ok(out) => {
println!("{}", console::style("FAIL").red());
let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.is_empty() {
println!("{}", stderr);
}
false
}
Err(e) => {
println!("{} ({})", console::style("FAIL").red(), e);
false
}
}
}
fn run_gates() -> bool {
println!("{}", console::style("Running acceptance gates...").bold());
let fmt_ok = run_gate(
"fmt",
Command::new("cargo").args(["fmt", "--all", "--", "--check"]),
);
let clippy_ok = run_gate(
"clippy",
Command::new("cargo").args(["clippy", "--workspace", "--", "-D", "warnings"]),
);
let test_ok = run_gate("test", Command::new("cargo").args(["test", "--workspace"]));
let doc_ok = run_gate(
"doc",
Command::new("cargo").args(["doc", "--workspace", "--no-deps"]),
);
let all_pass = fmt_ok && clippy_ok && test_ok && doc_ok;
if all_pass {
println!(" {} All gates passed", console::style("✔").green());
} else {
println!(
" {} Some gates failed — aborting",
console::style("✖").red()
);
}
all_pass
}
fn clean_tree() -> bool {
let output = Command::new("git").args(["status", "--porcelain"]).output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
if stdout.trim().is_empty() {
true
} else {
println!(
" {} Working tree has uncommitted changes:",
console::style("⚠").yellow()
);
for line in stdout.lines() {
println!(" {}", line);
}
false
}
}
Err(_) => {
println!(
" {} Could not check git status",
console::style("⚠").yellow()
);
true }
}
}
fn publish_crate(name: &str, dry_run: bool) -> bool {
if dry_run {
println!(
" {} {} (dry run — would publish)",
console::style("~").yellow(),
name
);
return true;
}
print!(" Publishing {} ... ", name);
let _ = std::io::Write::flush(&mut std::io::stdout());
let mut cmd = Command::new("cargo");
cmd.args(["publish", "-p", name]);
let output = cmd.output();
match output {
Ok(out) if out.status.success() => {
println!("{}", console::style("OK").green());
println!(" Waiting 60s for crates.io rate limit...");
std::thread::sleep(Duration::from_secs(60));
true
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("429") || stderr.contains("try again after") {
if let Some(ts_start) = stderr.find("after ") {
let rest = &stderr[ts_start + 6..];
if let Some(ts_end) = rest.find(['.', '\n', '\r']) {
let timestamp = &rest[..ts_end];
println!(" Rate limited — waiting until {}", timestamp);
std::thread::sleep(Duration::from_secs(90));
let retry = Command::new("cargo").args(["publish", "-p", name]).output();
match retry {
Ok(r) if r.status.success() => {
println!("{} {} (retry OK)", console::style("✔").green(), name);
std::thread::sleep(Duration::from_secs(60));
return true;
}
_ => {
println!("{} {} (retry failed)", console::style("✖").red(), name);
return false;
}
}
}
}
}
println!("{}", console::style("FAIL").red());
println!("{}", stderr);
false
}
Err(e) => {
println!("{} ({})", console::style("FAIL").red(), e);
false
}
}
}
fn create_git_tag(name: &str, version: &str, dry_run: bool) -> bool {
let tag = format!("{}-v{}", name, version);
if dry_run {
println!(
" {} git tag {} (dry run)",
console::style("~").yellow(),
tag
);
return true;
}
let output = Command::new("git").args(["tag", &tag]).output();
match output {
Ok(out) if out.status.success() => {
println!(
" {} Created git tag: {}",
console::style("✔").green(),
tag
);
true
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("already exists") {
println!(
" {} Tag {} already exists",
console::style("⚠").yellow(),
tag
);
true
} else {
println!(
" {} Failed to create tag: {}",
console::style("✖").red(),
stderr.trim()
);
false
}
}
Err(e) => {
println!(" {} {}", console::style("✖").red(), e);
false
}
}
}
fn read_crate_version(name: &str) -> Option<String> {
let path = Path::new("crates").join(name).join("Cargo.toml");
if !path.exists() {
if name == "rok-cli" || name == "rok-error" || name == "rok-tui" || name == "rok-studio" {
let alt = Path::new("crates").join(name).join("Cargo.toml");
if alt.exists() {
return read_version_from_file(&alt);
}
}
return None;
}
read_version_from_file(&path)
}
fn read_version_from_file(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
for line in content.lines() {
if let Some(ver) = line.strip_prefix("version = \"") {
if let Some(end) = ver.find('"') {
return Some(ver[..end].to_string());
}
}
}
None
}
pub fn run(dry_run: bool, specific_crate: Option<&str>) {
println!(
"{}",
console::style("╔══════════════════════════════════════╗").bold()
);
println!(
"{}",
console::style("║ rok publish pipeline ║").bold()
);
println!(
"{}",
console::style("╚══════════════════════════════════════╝").bold()
);
println!();
if !clean_tree() {
eprintln!(
"{} Working tree is not clean. Commit or stash before publishing.",
console::style("error:").red().bold()
);
std::process::exit(1);
}
if !run_gates() {
eprintln!(
"{} Gates failed. Fix issues then retry.",
console::style("error:").red().bold()
);
std::process::exit(1);
}
println!();
if dry_run {
println!(
"{}",
console::style("Dry-run mode — no crates will be published").yellow()
);
println!();
}
let mut published = 0u32;
let mut failed = 0u32;
for layer in PUBLISH_ORDER {
for &crate_name in *layer {
if let Some(specific) = specific_crate {
if crate_name != specific {
continue;
}
}
println!(" {}", console::style(crate_name).bold());
if let Some(version) = read_crate_version(crate_name) {
println!(" version: {}", version);
if publish_crate(crate_name, dry_run) {
if !dry_run {
create_git_tag(crate_name, &version, dry_run);
}
published += 1;
} else {
failed += 1;
}
} else {
println!(
" {} Could not find Cargo.toml for {}",
console::style("⚠").yellow(),
crate_name
);
failed += 1;
}
}
}
println!();
if dry_run {
println!(
"{} Dry run complete — {} crates ready to publish",
console::style("Summary:").bold(),
published
);
} else if failed == 0 {
println!(
"{} All {} crates published successfully!",
console::style("✔").green().bold(),
published
);
} else {
println!(
"{} Published {}, {} failed",
console::style("⚠").yellow().bold(),
published,
failed
);
}
}