use std::path::Path;
use std::process::Command;
pub fn run(bump: &str, specific_crate: Option<&str>, skip_publish: bool) {
println!(
"{}",
console::style("╔══════════════════════════════════════╗").bold()
);
println!(
"{}",
console::style("║ rok release pipeline ║").bold()
);
println!(
"{}",
console::style("╚══════════════════════════════════════╝").bold()
);
println!();
match bump {
"patch" | "minor" | "major" => {}
_ => {
eprintln!(
"{} Invalid bump type '{}'. Use patch, minor, or major.",
console::style("error:").red().bold(),
bump
);
std::process::exit(1);
}
}
let root_toml = Path::new("Cargo.toml");
let content = std::fs::read_to_string(root_toml).expect("Cannot read Cargo.toml");
let current_version = content
.lines()
.find(|l| l.trim().starts_with("version = "))
.and_then(|l| l.split('"').nth(1))
.map(|v| v.to_string())
.expect("Cannot find version in Cargo.toml");
let new_version = bump_version(¤t_version, bump);
println!(
" {} {} → {}",
console::style("Version:").bold(),
current_version,
console::style(&new_version).cyan().bold()
);
println!();
if let Some(name) = specific_crate {
bump_crate_version(name, ¤t_version, &new_version);
} else {
bump_workspace_version(¤t_version, &new_version);
}
println!();
let commit_msg = format!("bump version to {}", new_version);
println!(
" {} git commit -m \"{}\"",
console::style("~").yellow(),
&commit_msg
);
let commit = Command::new("git")
.args(["commit", "-am", &commit_msg])
.output();
match commit {
Ok(out) if out.status.success() => {
println!(" {} Committed", console::style("✔").green());
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("nothing to commit") {
println!(
" {} Nothing to commit (already up to date)",
console::style("⚠").yellow()
);
} else {
println!(
" {} Commit failed: {}",
console::style("✖").red(),
stderr.trim()
);
return;
}
}
Err(e) => {
println!(" {} {}", console::style("✖").red(), e);
return;
}
}
let tag = format!("v{}", new_version);
println!(" {} git tag {}", console::style("~").yellow(), &tag);
let tag_result = Command::new("git").args(["tag", &tag]).output();
match tag_result {
Ok(out) if out.status.success() => {
println!(" {} Created tag: {}", console::style("✔").green(), &tag);
}
_ => {
println!(
" {} Tag {} may already exist",
console::style("⚠").yellow(),
&tag
);
}
}
println!(" {} git push --tags", console::style("~").yellow());
let push = Command::new("git").args(["push", "--tags"]).output();
match push {
Ok(out) if out.status.success() => {
println!(" {} Pushed tags", console::style("✔").green());
}
Ok(out) => {
println!(
" {} Push failed (may need remote setup): {}",
console::style("⚠").yellow(),
String::from_utf8_lossy(&out.stderr)
.lines()
.next()
.unwrap_or("")
);
}
Err(_) => {
println!(
" {} Git not available — tags created locally",
console::style("⚠").yellow()
);
}
}
println!(
" {} gh release create {} --generate-notes",
console::style("~").yellow(),
&tag
);
let gh = Command::new("gh")
.args(["release", "create", &tag, "--generate-notes"])
.output();
match gh {
Ok(out) if out.status.success() => {
println!(
" {} Created GitHub Release: {}",
console::style("✔").green(),
&tag
);
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("not authenticated") || stderr.contains("not found") {
println!(" {} GitHub CLI not configured — tag created locally. Create release manually:", console::style("⚠").yellow());
println!(" gh release create {} --generate-notes", &tag);
} else {
println!(" {}", console::style("⚠").yellow());
}
}
Err(_) => {
println!(
" {} gh CLI not found — install: https://cli.github.com",
console::style("⚠").yellow()
);
}
}
println!();
println!(
" {} Release v{} complete!",
console::style("✔").green().bold(),
&new_version
);
if !skip_publish {
println!();
println!(" Running publish step...");
super::publish::run(false, specific_crate);
}
}
fn bump_version(version: &str, bump: &str) -> String {
let parts: Vec<&str> = version.split('.').collect();
let major: u32 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let minor: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let patch: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
match bump {
"major" => format!("{}.{}.{}", major + 1, 0, 0),
"minor" => format!("{}.{}.{}", major, minor + 1, 0),
"patch" => format!("{}.{}.{}", major, minor, patch + 1),
_ => version.to_string(),
}
}
fn bump_workspace_version(old: &str, new: &str) {
bump_file_version("Cargo.toml", old, new);
let crate_dirs = [
"crates/rok-cli",
"crates/rok-tui",
"crates/rok-studio",
"crates/rok-auth",
"crates/rok-auth-macros",
"crates/rok-auth-basic",
"crates/rok-auth-session",
"crates/rok-auth-social",
"crates/rok-orm",
"crates/rok-orm-core",
"crates/rok-orm-macros",
"crates/rok-orm-migrate",
"crates/rok-orm-factory",
"crates/rok-validate",
"crates/rok-validate-macros",
"crates/rok-config",
"crates/rok-config-macros",
"crates/rok-mail",
"crates/rok-cache",
"crates/rok-bouncer",
"crates/rok-hash",
"crates/rok-encrypt",
"crates/rok-lock",
"crates/rok-cors",
"crates/rok-shield",
"crates/rok-rate-limit",
"crates/rok-events",
"crates/rok-events-macros",
"crates/rok-testing",
"crates/rok-ids",
"crates/rok-queue-macros",
"crates/rok-queue",
"crates/rok-schedule",
"crates/rok-storage",
"crates/rok-notification",
"crates/rok-websocket",
"crates/rok-telemetry",
"crates/rok-search-macros",
"crates/rok-search",
"crates/rok-feature",
"crates/rok-i18n",
"crates/rok-problem",
"crates/rok-media",
"crates/rok-audit",
"crates/rok-audit-macros",
"crates/rok-acl",
"crates/rok-acl-macros",
"crates/rok-health",
"crates/rok-router",
"crates/rok-error",
];
for dir in &crate_dirs {
let path = format!("{}/Cargo.toml", dir);
if Path::new(&path).exists() {
bump_file_version(&path, old, new);
}
}
}
fn bump_crate_version(name: &str, old: &str, new: &str) {
let path = format!("crates/{}/Cargo.toml", name);
if Path::new(&path).exists() {
bump_file_version(&path, old, new);
}
let crate_dirs = [
"Cargo.toml",
"crates/rok-cli/Cargo.toml",
"crates/rok-tui/Cargo.toml",
];
for file in &crate_dirs {
if Path::new(file).exists() {
update_inter_crate_dep(file, name, new);
}
}
}
fn bump_file_version(path: &str, old: &str, new: &str) {
let content = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Cannot read {}", path));
let old_line = format!("version = \"{}\"", old);
let new_line = format!("version = \"{}\"", new);
if content.contains(&old_line) {
let updated = content.replace(&old_line, &new_line);
std::fs::write(path, updated).unwrap_or_else(|_| panic!("Cannot write {}", path));
println!(" {} Updated {}", console::style("~").cyan(), path);
}
}
fn update_inter_crate_dep(path: &str, crate_name: &str, new_version: &str) {
let content = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Cannot read {}", path));
let dep_pattern = format!("{crate_name} = {{ path = \"");
if content.contains(&dep_pattern) {
let mut updated = content.clone();
let mut search_from = 0;
while let Some(start) = updated[search_from..].find(&dep_pattern) {
let abs_start = search_from + start;
if let Some(ver_start) = updated[abs_start..].find("version = \"") {
let abs_ver_start = abs_start + ver_start + 10; if let Some(ver_end) = updated[abs_ver_start..].find('"') {
let abs_ver_end = abs_ver_start + ver_end;
let old_ver = &updated[abs_ver_start..abs_ver_end];
if old_ver.contains('.') {
let new_short = if new_version.matches('.').count() >= 2 {
let parts: Vec<&str> = new_version.split('.').collect();
format!("{}.{}", parts[0], parts[1])
} else {
new_version.to_string()
};
updated.replace_range(abs_ver_start..abs_ver_end, &new_short);
search_from = abs_ver_start + new_short.len();
println!(
" {} Updated dep {} in {}",
console::style("~").cyan(),
crate_name,
path
);
continue;
}
}
}
search_from = abs_start + 1;
}
if updated != content {
std::fs::write(path, updated).unwrap_or_else(|_| panic!("Cannot write {}", path));
}
}
}