On this page
Migration tooling for transitioning monorepos to the releasable model.
#rlsbl.releasable_migration
#rlsbl.releasable_migration
Migration tooling for transitioning monorepos to the releasable model.
Provides functions for Phase 10 of the releasable model redesign:
detect_migration_state()-- analyze workspace readiness for migrationconsolidate_changelogs()-- merge per-package changelogs into per-releasableconsolidate_versions()-- write releasable version from member versionscreate_migration_tag()-- create releasable-format tag from last per-package tags
#detect_migration_state
def detect_migration_state(workspace_root)Analyze current workspace and report migration readiness.
Loads workspace.toml, checks if already in explicit mode, and for each non-dev_node project reports version, changelog presence, and entry count. Also suggests groupings for projects sharing the same version.
Args:
workspace_root: path to the monorepo root.
Returns:
- A dict with keys:
- explicit_mode (bool): whether [[releasables]] already exists - projects (list[dict]): per-project migration info, each with: - name (str) - path (str) - version (str or None) - has_changelog (bool) - unreleased_entry_count (int) - versioned_file_count (int) - dev_node (bool) - releasable (str, False, or None) - suggested_groupings (dict[str, list[str]]): version string to list of project names that share it
#_read_project_version
def _read_project_version(project_path)Read version from a project's target manifest.
Returns the version string, or None if no target is detected or version cannot be read.
#consolidate_changelogs
def consolidate_changelogs(workspace_root, releasable_name, member_projects, *, tag_format=None, version=None)Merge member packages' unreleased.jsonl into per-releasable changelog.
For each member project, reads their per-package unreleased.jsonl and merges all entries into the releasable's changes directory. Each entry gains a packages field listing which member packages are affected (derived from commit file paths via project path/watch matching).
Cross-package dedup: entries from different packages that reference the exact same set of commits are merged into a single entry, combining their packages lists. This prevents the same commit from appearing in too many entries and violating max_entries_per_commit.
Batch limit exclusions: per-package batch_limits.exclusions from each member's .rlsbl/config.json are collected. After merging, any consolidated entry that exceeds max_commits_per_entry (default 5) gets a new exclusion auto-created in the releasable's config.
Consolidation-point tag: when tag_format and version are provided, a tag is created at HEAD after writing the merged file. This resets the unreleased range so that only post-consolidation commits need coverage -- the merged entries cover pre-consolidation work.
Historical versioned JSONL files are left in place (they are read-only records of past releases).
Args:
workspace_root: path to the monorepo root.releasable_name: name of the target releasable.member_projects: list of WorkspaceProject instances in this releasable.tag_format: optional releasable tag format (e.g., "{name}@v{version}").
When provided with version, a consolidation-point tag is created at HEAD.
version: optional version string for the consolidation-point tag.
Returns:
- A dict with:
- entries_merged (int): total entries written - source_projects (list[str]): projects that had entries - dest_path (str): path to the releasable's unreleased.jsonl - duplicates_merged (int): entries merged due to identical commits - exclusions_created (int): batch limit exclusions auto-created - consolidation_tag (str or None): tag created at HEAD, if any
#_derive_packages_for_entry
def _derive_packages_for_entry(entry, member_projects, workspace_root)Derive the packages field for a changelog entry.
For each commit in the entry, checks which member projects are affected by the commit's changed files. Returns a sorted, deduplicated list of project names.
If commit files cannot be determined (e.g., not in a git repo during testing), returns an empty list.
#_dedup_entries
def _dedup_entries(entries)Deduplicate entries with identical commit sets.
Entries from different packages that reference the exact same set of commits are merged into one entry, combining their packages lists. When merging, the first user-facing entry's description and type win (non-user-facing entries contribute only their packages).
Returns (deduped_entries, count_of_merges) where count_of_merges is the number of entries that were folded into another (i.e., len(original) - len(deduped)).
#_migrate_batch_exclusions
def _migrate_batch_exclusions(workspace_root, releasable_name, member_projects, entries)Collect per-package batch exclusions and create releasable-level ones.
Per-package exclusions reference (version, line_number) tuples that become invalid after consolidation (line numbers change in the merged file). Instead of carrying over stale exclusions, this function scans the consolidated entries and auto-creates new exclusions for any entry that exceeds max_commits_per_entry.
Writes the exclusions to a config.json in the releasable's directory ().
Returns the number of exclusions created.
#_create_consolidation_tag
def _create_consolidation_tag(workspace_root, releasable_name, tag_format, version)Create a consolidation-point tag at HEAD.
The tag uses the releasable's tag format and version, marking the point where per-package changelogs were consolidated. This resets the unreleased range () so only post-consolidation commits need coverage.
Returns the tag name on success, None on failure.
#consolidate_versions
def consolidate_versions(workspace_root, releasable_name, member_projects)Write releasable version file from member versions.
Reads each member's version from their target manifest (via detect_targets + read_version). If all members share the same version, writes it to the releasable version file. If they differ, returns the conflicting versions without writing.
Args:
workspace_root: path to the monorepo root.releasable_name: name of the releasable.member_projects: list of WorkspaceProject instances.
Returns:
- A dict with:
- status (str): "ok" if all versions match and were written, "conflict" if versions differ, "empty" if no versions found - version (str or None): the common version (if ok) - versions (dict[str, str]): project name -> version mapping (always present)
#create_migration_tag
def create_migration_tag(workspace_root, releasable_name, tag_format, member_projects)Create a releasable-format tag pointing to the last per-package release.
For each member project, finds the latest per-package tag (via git describe). Picks the most recent across all members and creates a releasable-format tag pointing to the same commit.
This gives _unreleased_range() a starting point with the new tag glob.
Args:
workspace_root: path to the monorepo root.releasable_name: name of the releasable.tag_format: the releasable's tag format string (e.g., "{name}@v{version}").member_projects: list of WorkspaceProject instances.
Returns:
- A dict with:
- status (str): "created", "no_tags", or "error" - tag (str or None): the created tag name - commit (str or None): the commit the tag points to - source_tag (str or None): the per-package tag used as source - member_tags (dict[str, str]): project name -> latest tag - skipped_members (list[str]): members skipped (no scoped tag)
#_git_describe_tag
def _git_describe_tag(tag_glob, cwd)Run git describe to find the latest tag matching a glob.
Returns the tag string or None.
#_git_rev_parse
def _git_rev_parse(ref, cwd)Resolve a git ref to a full SHA.
Returns the SHA string or None.
#_find_most_recent_tag
def _find_most_recent_tag(tags, cwd)Find the most recent tag by topological commit order.
Uses git log with all tags as start points and --topo-order to determine which tag's commit appears first (most recent). This is reliable even when commits share the same timestamp.
Returns the tag name or None if comparison fails.
#_extract_version_from_tag
def _extract_version_from_tag(tag)Extract the version string from a tag.
Handles formats like:
- "v1.2.3" -> "1.2.3"
- "[email protected]" -> "1.2.3"
- "path/v1.2.3" -> "1.2.3"
Returns None if no version pattern is found.
#cmd_migrate_releasable
def cmd_migrate_releasable(workspace_root, releasable_name, *, dry_run=False, yes=False)Orchestrate the full migration of a releasable from per-package to releasable model.
Steps:
detect_migration_state()-- show current stateconsolidate_changelogs()-- merge per-package changelogs into per-releasableconsolidate_versions()-- write releasable version from member versionscreate_migration_tag()-- create releasable-format tagcleanup_per_package_release_state()-- remove orphaned per-package state
Args:
workspace_root: path to the monorepo root.releasable_name: name of the releasable to migrate.dry_run: if True, report what would happen without making changes.yes: if True, skip interactive confirmation prompts.
Returns:
- A dict with:
- releasable_name (str) - dry_run (bool) - state (dict): result of detect_migration_state - changelogs (dict or None): result of consolidate_changelogs - versions (dict or None): result of consolidate_versions - tag (dict or None): result of create_migration_tag - cleanup (list or None): paths removed by cleanup
Raises:
WorkspaceError: if workspace is not in explicit mode or releasable
not found.