Skip to content

Commit c8b2389

Browse files
fix(core): scope prerelease guard to referenced packages only (#204)
* fix(core): scope prerelease guard to referenced packages only * chore: apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore(core): apply comments from github copilot. * chore(core): apply comments from github copilot. * chore(core): apply comments from github copilot. * chore(core): expand test assertions. * chore(core): recover .tmp files. * chore: apply suggestions. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: undo tmp and use Result instead. * docs(core): update docs to remove tmp. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent dbc649d commit c8b2389

File tree

3 files changed

+544
-25
lines changed

3 files changed

+544
-25
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
cargo/sampo-core: patch
3+
---
4+
5+
Fixed prerelease guard in release to only skip preserved changesets when all referenced packages are in prerelease, not when any workspace member is.

crates/sampo-core/src/release.rs

Lines changed: 151 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use crate::types::{
66
ReleaseOutput, ReleasedPackage, SpecResolution, Workspace, format_ambiguity_options,
77
};
88
use 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
};
1213
use 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.
708747
pub(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+
733873
fn finalize_consumed_changesets(
734874
used_paths: BTreeSet<PathBuf>,
735875
workspace_root: &Path,

0 commit comments

Comments
 (0)