@@ -6,7 +6,8 @@ use crate::types::{
66 ReleaseOutput , ReleasedPackage , SpecResolution , Workspace , format_ambiguity_options,
77} ;
88use crate :: {
9- changeset:: ChangesetInfo , config:: Config , current_branch, detect_github_repo_slug_with_config,
9+ changeset:: { parse_changeset, render_changeset_markdown_with_tags, ChangesetInfo } ,
10+ config:: Config , current_branch, detect_github_repo_slug_with_config,
1011 discover_workspace, enrich_changeset_message, get_commit_hash_for_path, load_changesets,
1112} ;
1213use chrono:: { DateTime , FixedOffset , Local , Utc } ;
@@ -507,15 +508,10 @@ pub fn run_release(root: &std::path::Path, dry_run: bool) -> Result<ReleaseOutpu
507508 } ) ;
508509 }
509510
510- let workspace_in_prerelease = workspace. members . iter ( ) . any ( |m| {
511- Version :: parse ( & m. version )
512- . map ( |v| !v. pre . is_empty ( ) )
513- . unwrap_or ( false )
514- } ) ;
515- if workspace_in_prerelease {
511+ if all_preserved_targets_in_prerelease ( & preserved_changesets, & workspace) ? {
516512 println ! (
517- "No new changesets found. Preserved changesets exist but workspace \
518- is in pre-release mode; skipping to avoid duplicate bump."
513+ "No new changesets found. Preserved changesets exist but all referenced \
514+ packages are in pre-release mode; skipping to avoid duplicate bump."
519515 ) ;
520516 return Ok ( ReleaseOutput {
521517 released_packages : vec ! [ ] ,
@@ -560,10 +556,17 @@ pub fn run_release(root: &std::path::Path, dry_run: bool) -> Result<ReleaseOutpu
560556 let mut final_changesets;
561557 let plan_state = if using_preserved {
562558 if dry_run {
559+ let filtered_preserved =
560+ filter_prerelease_entries ( preserved_changesets, & workspace) ?;
563561 final_changesets = current_changesets;
564- final_changesets. extend ( preserved_changesets ) ;
562+ final_changesets. extend ( filtered_preserved ) ;
565563 } else {
566- restore_prerelease_changesets ( & prerelease_dir, & changesets_dir) ?;
564+ restore_stable_preserved_changesets (
565+ & prerelease_dir,
566+ & changesets_dir,
567+ & workspace,
568+ & config. changesets_tags ,
569+ ) ?;
567570 final_changesets = load_changesets ( & changesets_dir, & config. changesets_tags ) ?;
568571 }
569572
@@ -705,6 +708,42 @@ fn releases_include_prerelease(releases: &ReleasePlan) -> bool {
705708 } )
706709}
707710
711+ fn is_spec_in_prerelease ( workspace : & Workspace , spec : & PackageSpecifier ) -> Result < bool > {
712+ let info = resolve_package_spec ( workspace, spec) ?;
713+
714+ let version = Version :: parse ( & info. version ) . map_err ( |e| {
715+ SampoError :: Release ( format ! (
716+ "failed to parse version '{}' for package spec {}: {}" ,
717+ info. version, spec, e
718+ ) )
719+ } ) ?;
720+
721+ Ok ( !version. pre . is_empty ( ) )
722+ }
723+
724+ fn all_preserved_targets_in_prerelease (
725+ changesets : & [ ChangesetInfo ] ,
726+ workspace : & Workspace ,
727+ ) -> Result < bool > {
728+ let specs: Vec < & PackageSpecifier > = changesets
729+ . iter ( )
730+ . flat_map ( |cs| cs. entries . iter ( ) . map ( |( spec, _, _) | spec) )
731+ . collect ( ) ;
732+
733+ if specs. is_empty ( ) {
734+ return Ok ( false ) ;
735+ }
736+
737+ for spec in & specs {
738+ if !is_spec_in_prerelease ( workspace, spec) ? {
739+ return Ok ( false ) ;
740+ }
741+ }
742+ Ok ( true )
743+ }
744+
745+ /// Move all preserved changeset files from the prerelease directory to the
746+ /// changesets directory without filtering. Used when exiting prerelease mode.
708747pub ( crate ) fn restore_prerelease_changesets (
709748 prerelease_dir : & Path ,
710749 changesets_dir : & Path ,
@@ -730,6 +769,107 @@ pub(crate) fn restore_prerelease_changesets(
730769 Ok ( ( ) )
731770}
732771
772+ /// Restore preserved changesets to the changesets directory, filtering out
773+ /// entries that target packages currently in prerelease.
774+ ///
775+ /// For each preserved changeset file:
776+ /// - If all entries target stable packages: move the entire file to changesets dir
777+ /// - If all entries target prerelease packages: leave untouched in prerelease dir
778+ /// - If mixed: write stable entries to a new file in changesets dir, rewrite
779+ /// the prerelease dir file with only the prerelease entries
780+ ///
781+ /// The mixed case writes the stable split first, then rewrites the prerelease
782+ /// file in place to remove the stable entries.
783+ fn restore_stable_preserved_changesets (
784+ prerelease_dir : & Path ,
785+ changesets_dir : & Path ,
786+ workspace : & Workspace ,
787+ allowed_tags : & [ String ] ,
788+ ) -> Result < ( ) > {
789+ if !prerelease_dir. exists ( ) {
790+ return Ok ( ( ) ) ;
791+ }
792+
793+ for entry in fs:: read_dir ( prerelease_dir) ? {
794+ let entry = entry?;
795+ let path = entry. path ( ) ;
796+ if !path. is_file ( ) {
797+ continue ;
798+ }
799+ if path. extension ( ) . and_then ( |ext| ext. to_str ( ) ) != Some ( "md" ) {
800+ continue ;
801+ }
802+
803+ let text = fs:: read_to_string ( & path)
804+ . map_err ( |e| SampoError :: Io ( io_error_with_path ( e, & path) ) ) ?;
805+ let parsed = match parse_changeset ( & text, & path, allowed_tags) ? {
806+ Some ( cs) => cs,
807+ None => continue ,
808+ } ;
809+
810+ let mut stable_entries = Vec :: new ( ) ;
811+ let mut prerelease_entries = Vec :: new ( ) ;
812+ for entry in parsed. entries . iter ( ) . cloned ( ) {
813+ if is_spec_in_prerelease ( workspace, & entry. 0 ) ? {
814+ prerelease_entries. push ( entry) ;
815+ } else {
816+ stable_entries. push ( entry) ;
817+ }
818+ }
819+
820+ if prerelease_entries. is_empty ( ) {
821+ // All entries target stable packages — move entire file
822+ let _ = move_changeset_file ( & path, changesets_dir) ?;
823+ } else if stable_entries. is_empty ( ) {
824+ // All entries target prerelease packages — leave untouched
825+ } else {
826+ // Mixed: write stable entries to changesets dir, rewrite prerelease file
827+ fs:: create_dir_all ( changesets_dir) ?;
828+ let file_name = path
829+ . file_name ( )
830+ . ok_or_else ( || SampoError :: Changeset ( "Invalid changeset file name" . to_string ( ) ) ) ?;
831+ let mut stable_path = changesets_dir. join ( file_name) ;
832+ if stable_path. exists ( ) {
833+ stable_path = unique_destination_path ( changesets_dir, file_name) ;
834+ }
835+
836+ let stable_content =
837+ render_changeset_markdown_with_tags ( & stable_entries, & parsed. message ) ;
838+ fs:: write ( & stable_path, & stable_content)
839+ . map_err ( |e| SampoError :: Io ( io_error_with_path ( e, & stable_path) ) ) ?;
840+
841+ let prerelease_content =
842+ render_changeset_markdown_with_tags ( & prerelease_entries, & parsed. message ) ;
843+ fs:: write ( & path, prerelease_content)
844+ . map_err ( |e| SampoError :: Io ( io_error_with_path ( e, & path) ) ) ?;
845+ }
846+ }
847+
848+ Ok ( ( ) )
849+ }
850+
851+ /// Filter preserved changesets in memory for the dry-run path, removing entries
852+ /// that target packages currently in prerelease. Drops changesets that become empty.
853+ fn filter_prerelease_entries (
854+ changesets : Vec < ChangesetInfo > ,
855+ workspace : & Workspace ,
856+ ) -> Result < Vec < ChangesetInfo > > {
857+ let mut result = Vec :: new ( ) ;
858+ for mut cs in changesets {
859+ let mut kept = Vec :: new ( ) ;
860+ for entry in cs. entries {
861+ if !is_spec_in_prerelease ( workspace, & entry. 0 ) ? {
862+ kept. push ( entry) ;
863+ }
864+ }
865+ cs. entries = kept;
866+ if !cs. entries . is_empty ( ) {
867+ result. push ( cs) ;
868+ }
869+ }
870+ Ok ( result)
871+ }
872+
733873fn finalize_consumed_changesets (
734874 used_paths : BTreeSet < PathBuf > ,
735875 workspace_root : & Path ,
0 commit comments