From c4c32ea72deacc04d1d728b9333cad1a4b097071 Mon Sep 17 00:00:00 2001 From: BryanFRD Date: Sun, 25 Oct 2026 09:54:48 +0100 Subject: [PATCH 1/2] fix(cascade): keep dependents of a prerelease run on the prerelease channel --- src/monorepo/run/cascade.rs | 98 ++++++++++++++++++++++++++------ src/monorepo/run/mod.rs | 2 + src/monorepo/run/plan.rs | 26 +++++---- src/monorepo/run/version_hook.rs | 3 + 4 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/monorepo/run/cascade.rs b/src/monorepo/run/cascade.rs index a6ae3fed..1a7b0499 100644 --- a/src/monorepo/run/cascade.rs +++ b/src/monorepo/run/cascade.rs @@ -12,6 +12,7 @@ use crate::versioning::compute_next_version; use super::super::types::CheckPackage; use super::super::util::tags_for_package; use super::super::version_source::VersionSource; +use super::plan::with_prerelease_suffix; use super::release_json::ReleasedPackage; use super::summary::PlannedTag; use super::version_hook::{Candidate, Resolved}; @@ -37,13 +38,7 @@ pub(super) fn run_dependency_cascade( sink: &mut CascadeSink<'_>, captured_metadata: &mut std::collections::HashMap, ) -> anyhow::Result<()> { - let (config, root, all_tags, channel, dry_run) = ( - pass.config, - pass.root, - pass.all_tags, - pass.channel, - pass.dry_run, - ); + let (config, root, channel, dry_run) = (pass.config, pass.root, pass.channel, pass.dry_run); let settled = settle(config, sink.bumped); // These packages were not in `bump_order`, so `capture` never saw them and // their commands are missing from the map. Without this they released with @@ -61,9 +56,7 @@ pub(super) fn run_dependency_cascade( .collect(); super::build_metadata::capture_more(config, &cascaded, root, dry_run, captured_metadata)?; for (pkg_idx, bump) in settled.order { - let Some(mut release) = - resolve_cascaded(config, root, all_tags, &settled.state, pkg_idx, bump) - else { + let Some(mut release) = resolve_cascaded(pass, &settled.state, pkg_idx, bump) else { continue; }; if !release.settle(pass)? { @@ -92,16 +85,16 @@ struct CascadedRelease<'a> { tag: String, dep_trigger: Vec<&'a str>, version_pending: bool, + is_prerelease: bool, } fn resolve_cascaded<'a>( - config: &'a Config, - root: &Path, - all_tags: &[String], + pass: &super::version_hook::Pass<'a>, state: &HashMap, pkg_idx: usize, bump: BumpType, ) -> Option> { + let (config, root, all_tags) = (pass.config, pass.root, pass.all_tags); let pkg = &config.packages[pkg_idx]; let vf = pkg.versioned_files.first()?; let current_version = read_version(vf, root).ok()?; @@ -110,8 +103,16 @@ fn resolve_cascaded<'a>( tags_for_package(all_tags, &pkg_tag_prefix) }); let version_template = pkg.effective_version_template(&config.workspace); - let new_version = + let base_version = compute_next_version(¤t_version, bump, strategy, version_template).ok()?; + let (new_version, is_prerelease) = with_prerelease_suffix( + pkg, + config, + pass.prerelease_ctx, + all_tags, + pass.short_hash, + base_version, + ); if current_version == new_version { return None; } @@ -135,6 +136,7 @@ fn resolve_cascaded<'a>( tag, dep_trigger, version_pending: false, + is_prerelease, }) } @@ -145,7 +147,7 @@ impl CascadedRelease<'_> { current: &self.current_version, proposed: &self.new_version, bump: self.bump, - is_prerelease: false, + is_prerelease: self.is_prerelease, commits: &[], })?; Ok(match resolved { @@ -177,7 +179,7 @@ impl CascadedRelease<'_> { bump_type: self.bump.to_string(), tag: self.tag.clone(), commit_count: 0, - prerelease: false, + prerelease: self.is_prerelease, version_source: self.version_source(), forge_release_url: None, forge_release_id: None, @@ -192,7 +194,7 @@ impl CascadedRelease<'_> { bump_type: self.bump.to_string(), tag: (!self.version_pending).then(|| self.tag.clone()), channel: channel.map(str::to_string), - prerelease: false, + prerelease: self.is_prerelease, version_source: self.version_source(), commits: vec![], } @@ -211,7 +213,7 @@ impl CascadedRelease<'_> { package: pkg.name.clone(), version: self.new_version.clone(), commit_count: 0, - is_prerelease: false, + is_prerelease: self.is_prerelease, }); sink.bumped.insert(pkg.name.clone(), self.bump); sink.bumped_versions @@ -416,6 +418,7 @@ fn push_once(files: &mut Vec, path: &str) { mod tests { use super::super::version_hook::Pass; use super::*; + use crate::prerelease::PrereleaseContext; fn workspace() -> tempfile::TempDir { let dir = tempfile::tempdir().unwrap(); @@ -580,6 +583,8 @@ mod tests { root: dir.path(), all_tags: &[], channel: None, + prerelease_ctx: &PrereleaseContext::resolve(None, "main", None).unwrap(), + short_hash: "deadbee", dry_run: false, run_commands: false, }, @@ -636,6 +641,8 @@ mod tests { root: dir.path(), all_tags: &[], channel: None, + prerelease_ctx: &PrereleaseContext::resolve(None, "main", None).unwrap(), + short_hash: "deadbee", dry_run: true, run_commands: false, }, @@ -654,6 +661,61 @@ mod tests { ); } + #[test] + fn a_cascade_in_a_prerelease_run_stays_a_prerelease() { + let dir = workspace(); + let config = Config::load(dir.path(), None).unwrap(); + let prerelease_ctx = PrereleaseContext::resolve(Some("beta"), "main", None).unwrap(); + let all_tags = vec!["cli@v1.1.0-beta.1".to_string()]; + + let mut any_bumped = false; + let mut json_packages = Vec::new(); + let mut released = Vec::new(); + let mut files_to_commit = Vec::new(); + let mut files_per_package = HashMap::new(); + let mut tags_to_create = Vec::new(); + let mut pkg_outputs = Vec::new(); + let mut bumped = HashMap::from([("core".to_string(), BumpType::Minor)]); + let mut bumped_versions = HashMap::from([("core".to_string(), "1.1.0-beta.1".to_string())]); + let mut sink = CascadeSink { + any_bumped: &mut any_bumped, + json_packages: &mut json_packages, + released: &mut released, + files_to_commit: &mut files_to_commit, + files_per_package: &mut files_per_package, + tags_to_create: &mut tags_to_create, + pkg_outputs: &mut pkg_outputs, + bumped: &mut bumped, + bumped_versions: &mut bumped_versions, + }; + + run_dependency_cascade( + &Pass { + config: &config, + root: dir.path(), + all_tags: &all_tags, + channel: Some("beta"), + prerelease_ctx: &prerelease_ctx, + short_hash: "deadbee", + dry_run: true, + run_commands: false, + }, + false, + false, + &mut sink, + &mut std::collections::HashMap::new(), + ) + .unwrap(); + + let cli = tags_to_create + .iter() + .find(|t| t.package == "cli") + .expect("cli should be cascaded"); + assert_eq!(cli.version, "1.1.0-beta.2"); + assert!(cli.is_prerelease); + assert_eq!(bumped_versions.get("cli").unwrap(), "1.1.0-beta.2"); + } + #[test] fn a_dependent_that_opts_out_of_the_cascade_keeps_its_constraint() { let dir = workspace(); diff --git a/src/monorepo/run/mod.rs b/src/monorepo/run/mod.rs index bb6b7311..a2c81ac0 100644 --- a/src/monorepo/run/mod.rs +++ b/src/monorepo/run/mod.rs @@ -214,6 +214,8 @@ fn version_pass<'a>( root, all_tags: &inputs.all_tags, channel: inputs.prerelease_ctx.channel.as_deref(), + prerelease_ctx: &inputs.prerelease_ctx, + short_hash: &inputs.short_hash, dry_run: flags.dry_run, run_commands: flags.version_commands, } diff --git a/src/monorepo/run/plan.rs b/src/monorepo/run/plan.rs index 1ce1dd46..574ee8f4 100644 --- a/src/monorepo/run/plan.rs +++ b/src/monorepo/run/plan.rs @@ -409,7 +409,14 @@ pub(super) fn compute_plan( let base_version = compute_next_version(¤t_version, bump, pkg_strategy, version_template)?; - let (new_version, is_prerelease) = with_prerelease_suffix(pkg, inputs, base_version); + let (new_version, is_prerelease) = with_prerelease_suffix( + pkg, + inputs.config, + inputs.prerelease_ctx, + inputs.all_tags, + inputs.short_hash, + base_version, + ); (new_version, is_prerelease, commits, bump) }; @@ -479,22 +486,19 @@ fn resolve_current_version( Ok((current_version, last_tag, source)) } -fn with_prerelease_suffix( +pub(super) fn with_prerelease_suffix( pkg: &PackageConfig, - inputs: &PlanInputs<'_>, + config: &Config, + prerelease_ctx: &PrereleaseContext, + all_tags: &[String], + short_hash: &str, base_version: String, ) -> (String, bool) { - if !inputs.prerelease_ctx.is_prerelease() { + if !prerelease_ctx.is_prerelease() { return (base_version, false); } - let config = inputs.config; let tag_prefix = pkg.tag_prefix(&config.workspace, config.is_monorepo()); - match inputs.prerelease_ctx.compute_identifier( - &base_version, - &tag_prefix, - inputs.all_tags, - inputs.short_hash, - ) { + match prerelease_ctx.compute_identifier(&base_version, &tag_prefix, all_tags, short_hash) { Some(resolved) => (format!("{base_version}{}", resolved.full_suffix), true), None => (base_version, false), } diff --git a/src/monorepo/run/version_hook.rs b/src/monorepo/run/version_hook.rs index 196b8ca8..e1a78c67 100644 --- a/src/monorepo/run/version_hook.rs +++ b/src/monorepo/run/version_hook.rs @@ -6,6 +6,7 @@ use crate::changelog::GitLog; use crate::config::{Config, PackageConfig, VersioningStrategy}; use crate::conventional_commits::BumpType; use crate::hooks::{HookCommit, HookContext, capture_version}; +use crate::prerelease::PrereleaseContext; use super::super::util::tags_for_package; use super::plan::{PackagePlan, SkipReason}; @@ -140,6 +141,8 @@ pub(super) struct Pass<'a> { pub root: &'a Path, pub all_tags: &'a [String], pub channel: Option<&'a str>, + pub prerelease_ctx: &'a PrereleaseContext, + pub short_hash: &'a str, pub dry_run: bool, pub run_commands: bool, } From ee6da7236e5beae290d70a7027af71e5e78ec8cd Mon Sep 17 00:00:00 2001 From: BryanFRD Date: Sun, 25 Oct 2026 11:18:10 +0100 Subject: [PATCH 2/2] refactor(cascade): derive the pass channel from the prerelease context --- src/monorepo/run/cascade.rs | 5 +---- src/monorepo/run/mod.rs | 1 - src/monorepo/run/version_hook.rs | 7 +++++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/monorepo/run/cascade.rs b/src/monorepo/run/cascade.rs index 1a7b0499..bb388fb9 100644 --- a/src/monorepo/run/cascade.rs +++ b/src/monorepo/run/cascade.rs @@ -38,7 +38,7 @@ pub(super) fn run_dependency_cascade( sink: &mut CascadeSink<'_>, captured_metadata: &mut std::collections::HashMap, ) -> anyhow::Result<()> { - let (config, root, channel, dry_run) = (pass.config, pass.root, pass.channel, pass.dry_run); + let (config, root, channel, dry_run) = (pass.config, pass.root, pass.channel(), pass.dry_run); let settled = settle(config, sink.bumped); // These packages were not in `bump_order`, so `capture` never saw them and // their commands are missing from the map. Without this they released with @@ -582,7 +582,6 @@ mod tests { config: &config, root: dir.path(), all_tags: &[], - channel: None, prerelease_ctx: &PrereleaseContext::resolve(None, "main", None).unwrap(), short_hash: "deadbee", dry_run: false, @@ -640,7 +639,6 @@ mod tests { config: &config, root: dir.path(), all_tags: &[], - channel: None, prerelease_ctx: &PrereleaseContext::resolve(None, "main", None).unwrap(), short_hash: "deadbee", dry_run: true, @@ -694,7 +692,6 @@ mod tests { config: &config, root: dir.path(), all_tags: &all_tags, - channel: Some("beta"), prerelease_ctx: &prerelease_ctx, short_hash: "deadbee", dry_run: true, diff --git a/src/monorepo/run/mod.rs b/src/monorepo/run/mod.rs index a2c81ac0..6542e6cf 100644 --- a/src/monorepo/run/mod.rs +++ b/src/monorepo/run/mod.rs @@ -213,7 +213,6 @@ fn version_pass<'a>( config, root, all_tags: &inputs.all_tags, - channel: inputs.prerelease_ctx.channel.as_deref(), prerelease_ctx: &inputs.prerelease_ctx, short_hash: &inputs.short_hash, dry_run: flags.dry_run, diff --git a/src/monorepo/run/version_hook.rs b/src/monorepo/run/version_hook.rs index e1a78c67..a82b4ebf 100644 --- a/src/monorepo/run/version_hook.rs +++ b/src/monorepo/run/version_hook.rs @@ -140,7 +140,6 @@ pub(super) struct Pass<'a> { pub config: &'a Config, pub root: &'a Path, pub all_tags: &'a [String], - pub channel: Option<&'a str>, pub prerelease_ctx: &'a PrereleaseContext, pub short_hash: &'a str, pub dry_run: bool, @@ -148,6 +147,10 @@ pub(super) struct Pass<'a> { } impl Pass<'_> { + pub(super) fn channel(&self) -> Option<&str> { + self.prerelease_ctx.channel.as_deref() + } + pub(super) fn resolve(&self, candidate: &Candidate<'_>) -> Result { let Some(command) = candidate .pkg @@ -168,7 +171,7 @@ impl Pass<'_> { proposed: candidate.proposed, bump: candidate.bump, is_prerelease: candidate.is_prerelease, - channel: self.channel, + channel: self.channel(), dry_run: self.dry_run, commits: candidate.commits, })?;