Skip to content

Commit f3aaa1f

Browse files
committed
cargo-rail: added release pipe + changelog
1 parent e231702 commit f3aaa1f

File tree

15 files changed

+1946
-19
lines changed

15 files changed

+1946
-19
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ semver = "1.0.27"
3636
# Parallelism and concurrency
3737
rayon = "1.11.0"
3838

39+
# Parsing (zero-copy, minimal deps)
40+
winnow = "0.7.13"
41+
3942
[dev-dependencies]
4043
tempfile = "3.23.0"
4144

src/commands/init.rs

Lines changed: 170 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
//! Auto-detects workspace structure, toolchain settings, and generates
44
//! a sensible .config/rail.toml with smart defaults.
55
6-
use crate::config::{PolicyConfig, RailConfig, SecurityConfig, ToolchainConfig, UnifyConfig, WorkspaceConfig};
6+
use crate::config::{
7+
CratePath, PolicyConfig, RailConfig, SecurityConfig, SplitConfig, SplitMode, ToolchainConfig, UnifyConfig,
8+
WorkspaceConfig, WorkspaceMode,
9+
};
710
use crate::error::{RailError, RailResult};
811
use crate::workspace::WorkspaceContext;
912
use std::fs;
@@ -52,6 +55,7 @@ pub fn run_init(
5255
let policy = detect_policy_config(workspace_root)?;
5356
let unify = default_unify_config();
5457
let security = default_security_config();
58+
let splits = detect_workspace_splits(ctx);
5559

5660
// 3. Display summary
5761
println!("Workspace Analysis:");
@@ -105,7 +109,7 @@ pub fn run_init(
105109
// 5. Build config
106110
println!("\n✅ Generated configuration with smart defaults\n");
107111

108-
let config = build_rail_config(workspace_root.to_path_buf(), toolchain, policy, unify, security);
112+
let config = build_rail_config(workspace_root.to_path_buf(), toolchain, policy, unify, security, splits);
109113

110114
// 6. Serialize with comments
111115
let toml_content = serialize_config_with_comments(&config)?;
@@ -291,13 +295,52 @@ fn default_security_config() -> SecurityConfig {
291295
SecurityConfig::default()
292296
}
293297

298+
/// Auto-detect workspace members and create split configs
299+
fn detect_workspace_splits(ctx: &WorkspaceContext) -> Vec<SplitConfig> {
300+
let workspace_root = ctx.workspace_root();
301+
let members = ctx.cargo.metadata().list_crates();
302+
303+
let mut splits = Vec::new();
304+
305+
for pkg in members {
306+
// Get relative path from workspace root to crate directory
307+
let crate_dir = pkg.manifest_path.parent().expect("manifest has parent");
308+
let rel_path = match crate_dir.strip_prefix(workspace_root) {
309+
Ok(p) => p.to_path_buf(),
310+
Err(_) => continue, // Skip if not under workspace root
311+
};
312+
313+
// Generate a reasonable remote URL placeholder (GitHub org/repo pattern)
314+
let remote = format!("[email protected]:org/{}.git", pkg.name);
315+
316+
// Check if crate has publish = false in Cargo.toml
317+
let publish = pkg.publish.as_ref().map(|p| !p.is_empty()).unwrap_or(true);
318+
319+
splits.push(SplitConfig {
320+
name: pkg.name.to_string(),
321+
remote,
322+
branch: "main".to_string(),
323+
mode: SplitMode::Single,
324+
workspace_mode: WorkspaceMode::default(),
325+
paths: vec![CratePath { path: rel_path.into() }],
326+
include: vec![],
327+
exclude: vec![],
328+
publish,
329+
changelog_path: None, // Use default from ReleaseConfig
330+
});
331+
}
332+
333+
splits
334+
}
335+
294336
/// Build a complete RailConfig from detected/default values
295337
fn build_rail_config(
296338
_workspace_root: PathBuf,
297339
toolchain: ToolchainConfig,
298340
policy: PolicyConfig,
299341
unify: UnifyConfig,
300342
security: SecurityConfig,
343+
splits: Vec<SplitConfig>,
301344
) -> RailConfig {
302345
RailConfig {
303346
workspace: WorkspaceConfig {
@@ -307,7 +350,8 @@ fn build_rail_config(
307350
policy,
308351
unify,
309352
security,
310-
splits: vec![],
353+
release: crate::config::ReleaseConfig::default(),
354+
splits,
311355
}
312356
}
313357

@@ -553,33 +597,138 @@ fn serialize_config_with_comments(config: &RailConfig) -> RailResult<String> {
553597

554598
output.push('\n');
555599

600+
// Release
601+
output.push_str("# ┌─────────────────────────────────────────────────────────────────────────┐\n");
602+
output.push_str("# │ Release & Publishing │\n");
603+
output.push_str("# └─────────────────────────────────────────────────────────────────────────┘\n");
604+
output.push_str("# Workspace-wide release defaults for version bumping and publishing.\n");
605+
output.push_str("# Per-crate settings are configured in [[splits]] below.\n");
606+
output.push_str("#\n");
607+
output.push_str("# Commands:\n");
608+
output.push_str("# cargo rail release plan - Preview release changes (dry-run)\n");
609+
output.push_str("# cargo rail release publish --execute - Execute release\n");
610+
output.push_str("# cargo rail release check - Validate release readiness (CI)\n");
611+
output.push_str("#\n");
612+
output.push_str("# Fields:\n");
613+
output.push_str("# tag_prefix - Prefix for git tags (default: \"v\")\n");
614+
output.push_str("# tag_format - Tag template: {crate}-v{version} for monorepos\n");
615+
output.push_str("# require_clean - Require clean working directory\n");
616+
output.push_str("# publish_delay - Seconds between crate publishes\n");
617+
output.push_str("# create_github_release - Auto-create GitHub releases via gh CLI\n");
618+
output.push_str("# sign_tags - Sign git tags with GPG/SSH\n");
619+
output.push_str("# changelog_path - Default changelog filename\n\n");
620+
621+
output.push_str("[release]\n");
622+
output.push_str(&format!(
623+
"tag_prefix = \"{}\" # Prefix for version tags\n",
624+
config.release.tag_prefix
625+
));
626+
output.push_str(&format!(
627+
"tag_format = \"{}\" # Variables: {{crate}}, {{version}}\n",
628+
config.release.tag_format
629+
));
630+
output.push_str(&format!(
631+
"require_clean = {} # Require clean working directory\n",
632+
config.release.require_clean
633+
));
634+
output.push_str(&format!(
635+
"publish_delay = {} # Seconds between publishes\n",
636+
config.release.publish_delay
637+
));
638+
output.push_str(&format!(
639+
"create_github_release = {} # Create GitHub releases\n",
640+
config.release.create_github_release
641+
));
642+
output.push_str(&format!(
643+
"sign_tags = {} # Sign tags with GPG/SSH\n",
644+
config.release.sign_tags
645+
));
646+
output.push_str(&format!(
647+
"changelog_path = \"{}\" # Default changelog file\n",
648+
config.release.changelog_path
649+
));
650+
651+
output.push('\n');
652+
556653
// Split/Sync
557654
output.push_str("# ┌─────────────────────────────────────────────────────────────────────────┐\n");
558655
output.push_str("# │ Split/Sync Configuration (Monorepo ↔ Separate Repos) │\n");
559656
output.push_str("# └─────────────────────────────────────────────────────────────────────────┘\n");
560-
output.push_str("# Configure crates to split from monorepo into standalone repositories\n");
561-
output.push_str("# with bidirectional synchronization.\n");
657+
output.push_str("# Each workspace member is auto-detected and configured for split/sync.\n");
658+
output.push_str("# Update 'remote' URLs and 'publish' flags as needed.\n");
562659
output.push_str("#\n");
563660
output.push_str("# Commands:\n");
564661
output.push_str("# cargo rail split <crate> - Extract crate to separate repo\n");
565662
output.push_str("# cargo rail sync <crate> - Bidirectional sync\n");
566663
output.push_str("# cargo rail status - Show split/sync status\n");
567664
output.push_str("#\n");
568-
output.push_str("# Example configuration:\n");
569-
output.push_str("#\n");
570-
output.push_str("# [[splits]]\n");
571-
output.push_str("# name = \"my-crate\" # Crate name\n");
572-
output.push_str("# remote = \"[email protected]:org/my-crate.git\" # Target repository\n");
573-
output.push_str("# branch = \"main\" # Branch to sync\n");
574-
output.push_str("# mode = \"single\" # \"single\" or \"multi\" (layout mode)\n");
575-
output.push_str("#\n");
576-
output.push_str("# # Paths to include in split (workspace members)\n");
577-
output.push_str("# [[splits.paths]]\n");
578-
output.push_str("# crate = \"crates/my-crate\" # Relative path from workspace root\n");
665+
output.push_str("# Fields per [[splits]] entry:\n");
666+
output.push_str("# name - Crate name\n");
667+
output.push_str("# remote - Target repository URL (update this!)\n");
668+
output.push_str("# branch - Branch to sync (default: main)\n");
669+
output.push_str("# mode - \"single\" or \"combined\" layout\n");
670+
output.push_str("# publish - Enable publishing to crates.io (default: true)\n");
671+
output.push_str("# changelog_path - Per-crate changelog override (optional)\n");
579672
output.push_str("#\n");
580-
output.push_str("# # Optional: Additional paths to sync\n");
581-
output.push_str("# [[splits.paths]]\n");
582-
output.push_str("# crate = \"crates/my-crate-macros\"\n");
673+
674+
// Serialize detected splits
675+
if config.splits.is_empty() {
676+
output.push_str("# No workspace members detected. Example:\n");
677+
output.push_str("#\n");
678+
output.push_str("# [[splits]]\n");
679+
output.push_str("# name = \"my-crate\"\n");
680+
output.push_str("# remote = \"[email protected]:org/my-crate.git\"\n");
681+
output.push_str("# branch = \"main\"\n");
682+
output.push_str("# mode = \"single\"\n");
683+
output.push_str("# publish = true\n");
684+
output.push_str("#\n");
685+
output.push_str("# [[splits.paths]]\n");
686+
output.push_str("# crate = \"crates/my-crate\"\n");
687+
} else {
688+
output.push_str(&format!(
689+
"# Auto-detected {} workspace member(s):\n\n",
690+
config.splits.len()
691+
));
692+
693+
for split in &config.splits {
694+
output.push_str("[[splits]]\n");
695+
output.push_str(&format!("name = \"{}\"\n", split.name));
696+
output.push_str(&format!(
697+
"remote = \"{}\" # TODO: Update with actual repository URL\n",
698+
split.remote
699+
));
700+
output.push_str(&format!("branch = \"{}\"\n", split.branch));
701+
output.push_str(&format!(
702+
"mode = \"{}\"\n",
703+
match split.mode {
704+
SplitMode::Single => "single",
705+
SplitMode::Combined => "combined",
706+
}
707+
));
708+
output.push_str(&format!(
709+
"publish = {} # {}\n",
710+
split.publish,
711+
if split.publish {
712+
"Enable crates.io publishing"
713+
} else {
714+
"Skip publishing (publish = false in Cargo.toml)"
715+
}
716+
));
717+
718+
if let Some(ref changelog) = split.changelog_path {
719+
output.push_str(&format!("changelog_path = \"{}\"\n", changelog.display()));
720+
}
721+
722+
output.push('\n');
723+
724+
for path in &split.paths {
725+
output.push_str("[[splits.paths]]\n");
726+
output.push_str(&format!("crate = \"{}\"\n", path.path.display()));
727+
}
728+
729+
output.push('\n');
730+
}
731+
}
583732

584733
Ok(output)
585734
}
@@ -692,6 +841,7 @@ pub fn run_init_standalone(
692841
pr_branch_pattern: "rail/sync/{crate}/{timestamp}".to_string(),
693842
protected_branches: vec!["main".to_string(), "master".to_string()],
694843
},
844+
release: crate::config::ReleaseConfig::default(),
695845
splits: vec![],
696846
};
697847

@@ -830,6 +980,7 @@ rust-version = "1.91"
830980
policy: PolicyConfig::default(),
831981
unify: UnifyConfig::default(),
832982
security: SecurityConfig::default(),
983+
release: crate::config::ReleaseConfig::default(),
833984
splits: vec![],
834985
};
835986

src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub mod affected;
2323
pub mod common;
2424
pub mod config_sync;
2525
pub mod init;
26+
pub mod release;
2627
pub mod split;
2728
pub mod status;
2829
pub mod sync;
@@ -33,6 +34,7 @@ pub mod watch;
3334
pub use affected::run_affected;
3435
pub use config_sync::run_config_sync;
3536
pub use init::{run_init, run_init_standalone};
37+
pub use release::{run_release_check, run_release_plan, run_release_publish};
3638
pub use split::run_split;
3739
pub use status::run_status;
3840
pub use sync::run_sync;

0 commit comments

Comments
 (0)