On this page
Resolved-plan sidecar for monorepo batch releases that persists pre-batch base versions for idempotent, resumable release flows.
#rlsbl.commands.monorepo.batch_plan
#rlsbl.commands.monorepo.batch_plan
Resolved-plan sidecar for monorepo batch releases that persists pre-batch base versions for idempotent, resumable release flows.
A batch release file (.rlsbl-monorepo/releases/unreleased.toml) records only bump intents -- it does not persist the pre-batch base version of each item. That is fatal for idempotent, resumable batch releases: once an item has been released (its version bumped from V0 to V1 and tag(V1) created), it is observationally identical to a pending item that happens to sit at V1 with its prior tag. No function of (live version, existing tags, bump type) can tell the two apart. See the investigation notes: the base version must be persisted.
This module persists a resolved plan alongside the batch file: .rlsbl-monorepo/releases/unreleased.plan.json. It is computed exactly once, at the start of the first monorepo release run against a given unreleased.toml (before any item is released), and captures for every item:
name-- releasable or package namebase_version-- the live version at plan timetarget_version-- the version the release will produce (bump applied)tag-- the exact git tag string the release will createregistry-- the primary target registry (drives live-version reads)bump-- the bump intent (used to validate reuse against the batch file)
On any subsequent run the plan is validated and reused, never regenerated: regenerating would recompute base versions from drifted state -- the exact bug this design closes. The per-item skip predicate and the archive-as-repair gate are both computed against the persisted plan.
#BatchPlanError
Raised when a persisted plan is malformed or inconsistent with the current batch release file (item set / bump intent mismatch).
#PlanItem
A single item in a resolved batch plan, capturing the frozen base version, target version, tag, registry, and bump intent.
#BatchPlan
The full resolved plan for a batch release, containing the section type and a mapping of item names to their PlanItem entries.
#get_batch_plan_path
def get_batch_plan_path(workspace_root: str='.') -> strReturn the path to .rlsbl-monorepo/releases/unreleased.plan.json.
#plan_exists
def plan_exists(workspace_root: str) -> boolReturn True if a resolved plan sidecar exists on disk.
#write_batch_plan
def write_batch_plan(path: str, plan: BatchPlan) -> NoneAtomically write the resolved plan to path (tmp file + rename).
#read_batch_plan
def read_batch_plan(path: str) -> BatchPlanRead and parse a resolved plan JSON file.
Raises BatchPlanError on malformed content.
#compute_batch_plan
def compute_batch_plan(workspace_root, batch_config, projects)Resolve base/target/tag for every batch item and return a BatchPlan.
Delegates to the same compute_release_version used by the real release flow, so the plan captures exactly what a release started now would produce. In particular, compute_release_version raises ReleaseValidationError when a target tag already exists -- the caller treats that as evidence of a partially-executed plan-less batch.
#validate_plan_against_config
def validate_plan_against_config(plan: BatchPlan, batch_config) -> NoneValidate a persisted plan matches the current batch file's intent.
The plan is reused across runs and must never be regenerated mid-flight. Reuse is only valid when the plan describes the same set of items with the same bump intents as the batch file. Base versions may legitimately differ from live state (an item may already be partially released), so those are NOT compared here.
Raises BatchPlanError on any mismatch.
#read_live_version
def read_live_version(workspace_root, item: PlanItem, projects, section_type)Read the current on-disk version for a plan item.
Returns the version string, or None if it cannot be read (e.g. a manifest is missing). A None result makes the skip predicate treat the item as not yet released, so it proceeds through the normal release path.
#item_is_released
def item_is_released(workspace_root, item: PlanItem, projects, section_type) -> boolSkip predicate: True iff the item's release provably already happened.
An item is released iff its live version equals the plan's target_version, the plan's tag exists locally, AND the tag also exists on the remote. Any other state (version mismatch, tag absent locally or on the remote) means the item is not verifiably released and must proceed -- a genuinely inconsistent intermediate state will then fail loudly downstream.
The remote-tag requirement closes a gap where a release tagged and version- bumped locally but never pushed (push failed) would look "released" and get skipped/archived, silently dropping the publish.
#plan_all_released
def plan_all_released(workspace_root, plan: BatchPlan, projects) -> boolTrue iff every plan item satisfies the skip predicate.
#archive_plan_file
def archive_plan_file(plan_path: str, versioned_stem: str) -> list[str]Archive the plan sidecar next to the archived batch file.
versioned_stem is the batch file's archived stem (e.g. batch-20260713-101500); the plan is renamed to <stem>.plan.json and chmod'd read-only. Returns the list of changed paths (for committing), or [] if no plan file exists.