rlsbl v0.113.0 /rlsbl.commands.monorepo.batch_plan
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 name
  • base_version -- the live version at plan time
  • target_version -- the version the release will produce (bump applied)
  • tag -- the exact git tag string the release will create
  • registry -- 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

python
def get_batch_plan_path(workspace_root: str='.') -> str

Return the path to .rlsbl-monorepo/releases/unreleased.plan.json.

#plan_exists

python
def plan_exists(workspace_root: str) -> bool

Return True if a resolved plan sidecar exists on disk.

#write_batch_plan

python
def write_batch_plan(path: str, plan: BatchPlan) -> None

Atomically write the resolved plan to path (tmp file + rename).

#read_batch_plan

python
def read_batch_plan(path: str) -> BatchPlan

Read and parse a resolved plan JSON file.

Raises BatchPlanError on malformed content.

#compute_batch_plan

python
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

python
def validate_plan_against_config(plan: BatchPlan, batch_config) -> None

Validate 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

python
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

python
def item_is_released(workspace_root, item: PlanItem, projects, section_type) -> bool

Skip 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

python
def plan_all_released(workspace_root, plan: BatchPlan, projects) -> bool

True iff every plan item satisfies the skip predicate.

#archive_plan_file

python
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.

Search