Skip to content

Commit 1e96524

Browse files
committed
cargo-rail: adding finalize && publish logic; testing the release workflow
1 parent 7cb8570 commit 1e96524

File tree

8 files changed

+733
-26
lines changed

8 files changed

+733
-26
lines changed

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,5 @@ AGENTS.md
2323
*.key
2424

2525
# Notes
26-
TODO.md
2726
TESTING_PLAN.md
28-
NEXT_STEPS.md
29-
TECHNICAL_REVIEW.md
27+
STATUS.md

crates/cargo-rail/src/checks/ssh.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use super::trait_def::{Check, CheckContext, CheckResult};
44
use crate::core::error::RailResult;
55
use std::fs;
6+
#[cfg(unix)]
67
use std::os::unix::fs::PermissionsExt;
78
use std::path::PathBuf;
89
use std::process::Command;
@@ -57,12 +58,15 @@ impl Check for SshKeyCheck {
5758
if key_path.exists() {
5859
found_keys.push(format!("{} ({})", key_name, key_desc));
5960

60-
// Check permissions (should be 600 or 400)
61-
if let Ok(metadata) = fs::metadata(&key_path) {
62-
let permissions = metadata.permissions();
63-
let mode = permissions.mode() & 0o777;
64-
if mode != 0o600 && mode != 0o400 {
65-
permission_issues.push(format!("{}: has mode {:o} (should be 600 or 400)", key_name, mode));
61+
// Check permissions (should be 600 or 400) - Unix only
62+
#[cfg(unix)]
63+
{
64+
if let Ok(metadata) = fs::metadata(&key_path) {
65+
let permissions = metadata.permissions();
66+
let mode = permissions.mode() & 0o777;
67+
if mode != 0o600 && mode != 0o400 {
68+
permission_issues.push(format!("{}: has mode {:o} (should be 600 or 400)", key_name, mode));
69+
}
6670
}
6771
}
6872
}
Lines changed: 166 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,173 @@
11
//! Git tagging and post-release sync
22
3+
use crate::commands::release::{plan, tags};
34
use crate::core::error::RailResult;
5+
use std::env;
6+
use std::path::Path;
7+
use std::process::Command;
48

59
/// Run release finalize command
6-
pub fn run_release_finalize(
7-
_crate_name: Option<&str>,
8-
_apply: bool,
9-
_no_github: bool,
10-
_no_sync: bool,
11-
) -> RailResult<()> {
12-
println!("🚧 Release finalize command (coming soon)");
10+
pub fn run_release_finalize(crate_name: Option<&str>, apply: bool, _no_github: bool, no_sync: bool) -> RailResult<()> {
11+
// Generate release plan to determine what needs to be finalized
12+
println!("📊 Analyzing workspace for release plan...");
13+
let mut full_plan = plan::generate_release_plan(true)?;
14+
println!();
15+
16+
// Filter to specific crate if requested
17+
if let Some(name) = crate_name {
18+
full_plan.crates.retain(|c| c.name == name);
19+
full_plan.publish_order.retain(|n| n == name);
20+
}
21+
22+
// Only finalize crates with changes
23+
let plan = full_plan.only_changed();
24+
25+
if plan.crates.is_empty() {
26+
if crate_name.is_some() {
27+
println!("ℹ️ No changes detected for the specified crate");
28+
} else {
29+
println!("ℹ️ No crates need to be finalized");
30+
}
31+
return Ok(());
32+
}
33+
34+
// Get current directory (workspace root)
35+
let workspace_root = env::current_dir().map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?;
36+
37+
// Open git repository
38+
let repo = gix::open(&workspace_root).map_err(|e| anyhow::anyhow!("Failed to open git repository: {}", e))?;
39+
40+
println!("🏷️ Finalizing {} crate(s)", plan.crates.len());
41+
println!();
42+
43+
// Get current HEAD commit
44+
let head = repo.head().map_err(|e| anyhow::anyhow!("Failed to get HEAD: {}", e))?;
45+
let commit = head
46+
.into_peeled_id()
47+
.map_err(|e| anyhow::anyhow!("Failed to resolve HEAD to commit: {}", e))?;
48+
let commit_id = commit.detach();
49+
50+
// Create tags for each crate
51+
for crate_plan in &plan.crates {
52+
let tag_name = tags::format_tag(&crate_plan.name, &crate_plan.next_version);
53+
54+
println!(
55+
"📌 {} (v{} → v{})",
56+
crate_plan.name, crate_plan.current_version, crate_plan.next_version
57+
);
58+
59+
// Generate tag message from changelog
60+
let tag_message = format!(
61+
"Release {} v{}\n\n{}",
62+
crate_plan.name, crate_plan.next_version, crate_plan.reason
63+
);
64+
65+
if !apply {
66+
println!(" 💡 Would create tag: {}", tag_name);
67+
println!(" 💡 Would push to origin");
68+
if !no_sync {
69+
println!(" 💡 Would sync to split repo");
70+
}
71+
} else {
72+
// Create annotated tag
73+
println!(" 🏷️ Creating tag: {}", tag_name);
74+
75+
// Check if tag already exists
76+
if repo.find_reference(&format!("refs/tags/{}", tag_name)).is_ok() {
77+
eprintln!(" ⚠️ Tag {} already exists, skipping", tag_name);
78+
continue;
79+
}
80+
81+
// Create the tag using git command (gix doesn't have a simple tag creation API)
82+
let tag_result = Command::new("git")
83+
.arg("tag")
84+
.arg("-a")
85+
.arg(&tag_name)
86+
.arg("-m")
87+
.arg(&tag_message)
88+
.arg(commit_id.to_string())
89+
.current_dir(&workspace_root)
90+
.output()
91+
.map_err(|e| anyhow::anyhow!("Failed to run git tag: {}", e))?;
92+
93+
if !tag_result.status.success() {
94+
let stderr = String::from_utf8_lossy(&tag_result.stderr);
95+
return Err(anyhow::anyhow!("Failed to create tag {}:\n{}", tag_name, stderr).into());
96+
}
97+
98+
println!(" ✅ Created tag: {}", tag_name);
99+
100+
// Push tag to origin
101+
println!(" 📤 Pushing tag to origin...");
102+
let push_result = Command::new("git")
103+
.arg("push")
104+
.arg("origin")
105+
.arg(format!("refs/tags/{}", tag_name))
106+
.current_dir(&workspace_root)
107+
.output()
108+
.map_err(|e| anyhow::anyhow!("Failed to run git push: {}", e))?;
109+
110+
if !push_result.status.success() {
111+
let stderr = String::from_utf8_lossy(&push_result.stderr);
112+
eprintln!(" ⚠️ Warning: Failed to push tag to origin:\n{}", stderr);
113+
} else {
114+
println!(" ✅ Pushed to origin");
115+
}
116+
117+
// Sync to split repo (if configured and not disabled)
118+
if !no_sync {
119+
println!(" 🔄 Syncing to split repo...");
120+
let sync_result = sync_to_split_repo(&workspace_root, &crate_plan.name)?;
121+
if sync_result {
122+
println!(" ✅ Synced to split repo");
123+
} else {
124+
println!(" ℹ️ No split repo configured");
125+
}
126+
}
127+
}
128+
129+
println!();
130+
}
131+
132+
// Summary
133+
if !apply {
134+
println!("💡 This was a dry-run. Use --apply to create and push tags.");
135+
} else {
136+
println!("🎉 Successfully finalized {} crate(s)!", plan.crates.len());
137+
println!();
138+
println!("Tags created:");
139+
for crate_plan in &plan.crates {
140+
let tag_name = tags::format_tag(&crate_plan.name, &crate_plan.next_version);
141+
println!(" - {}", tag_name);
142+
}
143+
}
144+
13145
Ok(())
14146
}
147+
148+
/// Sync a crate to its split repository
149+
fn sync_to_split_repo(workspace_root: &Path, crate_name: &str) -> RailResult<bool> {
150+
// Check if rail.toml exists and has split repo config for this crate
151+
let rail_config = workspace_root.join(".rail").join("rail.toml");
152+
if !rail_config.exists() {
153+
return Ok(false);
154+
}
155+
156+
// Try to sync using cargo rail sync command
157+
let sync_result = Command::new("cargo")
158+
.arg("rail")
159+
.arg("sync")
160+
.arg(crate_name)
161+
.arg("--apply")
162+
.current_dir(workspace_root)
163+
.output()
164+
.map_err(|e| anyhow::anyhow!("Failed to run cargo rail sync: {}", e))?;
165+
166+
if !sync_result.status.success() {
167+
let stderr = String::from_utf8_lossy(&sync_result.stderr);
168+
eprintln!(" ⚠️ Warning: Failed to sync to split repo:\n{}", stderr);
169+
return Ok(false);
170+
}
171+
172+
Ok(true)
173+
}

0 commit comments

Comments
 (0)